-
-
Notifications
You must be signed in to change notification settings - Fork 217
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
1 parent
25bbec5
commit 7ebd7bf
Showing
4 changed files
with
322 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { Inject, Injectable } from '@angular/core'; | ||
import { DOCUMENT } from '@angular/common'; | ||
|
||
import { DbService } from '@mm-services/db.service'; | ||
|
||
@Injectable({ | ||
providedIn: 'root' | ||
}) | ||
export class IndexedDbService { | ||
private readonly LOCAL_DOC = '_local/indexeddb-placeholder'; | ||
private loadingLocalDoc: Promise<IndexedDBDoc> | undefined; | ||
private hasDatabasesFn: boolean; | ||
private isUpdatingLocalDoc: boolean; | ||
|
||
constructor( | ||
private dbService: DbService, | ||
@Inject(DOCUMENT) private document: Document | ||
) { | ||
// Firefox doesn't support the databases() function. Issue: https://github.com/medic/cht-core/issues/8777 | ||
this.hasDatabasesFn = !!this.document.defaultView?.indexedDB?.databases; | ||
} | ||
|
||
async getDatabaseNames() { | ||
if (this.hasDatabasesFn) { | ||
const databases = await this.document.defaultView?.indexedDB?.databases(); | ||
return databases?.map(db => db.name); | ||
} | ||
|
||
const doc = await this.getLocalDoc(); | ||
return doc?.db_names || []; | ||
} | ||
|
||
async saveDatabaseName(name: string) { | ||
if (this.hasDatabasesFn || this.isUpdatingLocalDoc) { | ||
return; | ||
} | ||
|
||
this.isUpdatingLocalDoc = true; | ||
const localDoc = await this.getLocalDoc(); | ||
if (localDoc.db_names.indexOf(name) > -1) { | ||
this.isUpdatingLocalDoc = false; | ||
return; | ||
} | ||
|
||
localDoc.db_names.push(name); | ||
await this.updateLocalDoc(localDoc); | ||
this.isUpdatingLocalDoc = false; | ||
} | ||
|
||
async deleteDatabaseName(name: string) { | ||
if (this.hasDatabasesFn || this.isUpdatingLocalDoc) { | ||
return; | ||
} | ||
|
||
this.isUpdatingLocalDoc = true; | ||
const localDoc = await this.getLocalDoc(); | ||
const dbNameIdx = localDoc.db_names.indexOf(name); | ||
if (dbNameIdx === -1) { | ||
this.isUpdatingLocalDoc = false; | ||
return; | ||
} | ||
|
||
localDoc.db_names.splice(dbNameIdx, 1); | ||
await this.updateLocalDoc(localDoc); | ||
this.isUpdatingLocalDoc = false; | ||
} | ||
|
||
private async updateLocalDoc(localDoc) { | ||
await this.dbService | ||
.get({ meta: true }) | ||
.put(localDoc); | ||
this.loadingLocalDoc = undefined; // To fetch again and get the new doc's _rev number. | ||
} | ||
|
||
private async getLocalDoc(): Promise<IndexedDBDoc> { | ||
// Avoids "Failed to execute transaction on IDBDatabase" exception. | ||
if (!this.loadingLocalDoc) { | ||
this.loadingLocalDoc = this.dbService | ||
.get({ meta: true }) | ||
.get(this.LOCAL_DOC); | ||
} | ||
|
||
let localDoc; | ||
try { | ||
localDoc = await this.loadingLocalDoc; | ||
} catch (error) { | ||
if (error.status !== 404) { | ||
throw error; | ||
} | ||
console.debug('IndexedDbService :: Local doc not created yet. Ignoring error.'); | ||
} | ||
return localDoc ? { ...localDoc } : { _id: this.LOCAL_DOC, db_names: [] }; | ||
} | ||
} | ||
|
||
interface IndexedDBDoc { | ||
_id: string; | ||
db_names: string[]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
179 changes: 179 additions & 0 deletions
179
webapp/tests/karma/ts/services/indexed-db.service.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
import { TestBed } from '@angular/core/testing'; | ||
import { DOCUMENT } from '@angular/common'; | ||
import sinon from 'sinon'; | ||
import { expect } from 'chai'; | ||
|
||
import { DbService } from '@mm-services/db.service'; | ||
import { IndexedDbService } from '@mm-services/indexed-db.service'; | ||
|
||
describe('IndexedDbService', () => { | ||
let service: IndexedDbService; | ||
let dbService; | ||
let metaDb; | ||
let documentMock; | ||
|
||
beforeEach(() => { | ||
metaDb = { | ||
put: sinon.stub(), | ||
get: sinon.stub(), | ||
}; | ||
dbService = { | ||
get: sinon.stub().withArgs({ meta: true }).returns(metaDb) | ||
}; | ||
documentMock = { | ||
defaultView: { | ||
indexedDB: { databases: null }, | ||
}, | ||
querySelectorAll: sinon.stub().returns([]), | ||
}; | ||
|
||
TestBed.configureTestingModule({ | ||
providers: [ | ||
{ provide: DbService, useValue: dbService }, | ||
{ provide: DOCUMENT, useValue: documentMock }, | ||
], | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
sinon.restore(); | ||
}); | ||
|
||
describe('getDatabaseNames', () => { | ||
it('should do return list of database names when browser supports databases() function', async () => { | ||
documentMock.defaultView.indexedDB.databases = sinon.stub(); | ||
documentMock.defaultView.indexedDB.databases.resolves([ | ||
{ name: 'db-1' }, | ||
{ name: 'db-2' }, | ||
]); | ||
service = TestBed.inject(IndexedDbService); | ||
|
||
const dbNames = await service.getDatabaseNames(); | ||
|
||
expect(documentMock.defaultView.indexedDB.databases.calledOnce).to.be.true; | ||
expect(dbNames).to.have.members([ 'db-1', 'db-2' ]); | ||
}); | ||
|
||
it('should return list of database names', async () => { | ||
metaDb.get.resolves({ | ||
db_names: [ 'db-a', 'db-b', 'db-c' ], | ||
}); | ||
service = TestBed.inject(IndexedDbService); | ||
|
||
const dbNames = await service.getDatabaseNames(); | ||
|
||
expect(dbNames).to.have.members([ 'db-a', 'db-b', 'db-c' ]); | ||
}); | ||
|
||
it('should return empty array if no db_names in local doc', async () => { | ||
metaDb.get.resolves({ db_names: [] }); | ||
service = TestBed.inject(IndexedDbService); | ||
|
||
const dbNames = await service.getDatabaseNames(); | ||
|
||
expect(dbNames?.length).to.equal(0); | ||
}); | ||
}); | ||
|
||
describe('saveDatabaseName', () => { | ||
it('should do nothing when browser supports databases() function', async () => { | ||
documentMock.defaultView.indexedDB.databases = sinon.stub(); | ||
documentMock.defaultView.indexedDB.databases.resolves([ | ||
{ name: 'db-1' }, | ||
{ name: 'db-2' }, | ||
]); | ||
service = TestBed.inject(IndexedDbService); | ||
|
||
await service.saveDatabaseName('db-new'); | ||
|
||
expect(documentMock.defaultView.indexedDB.databases.notCalled).to.be.true; | ||
expect(metaDb.get.notCalled).to.be.true; | ||
expect(metaDb.put.notCalled).to.be.true; | ||
}); | ||
|
||
it('should do nothing when name already exists', async () => { | ||
metaDb.get.resolves({ | ||
db_names: [ 'db-a', 'db-b', 'db-c' ], | ||
}); | ||
service = TestBed.inject(IndexedDbService); | ||
|
||
await service.saveDatabaseName('db-b'); | ||
|
||
expect(metaDb.get.calledOnce).to.be.true; | ||
expect(metaDb.put.notCalled).to.be.true; | ||
}); | ||
|
||
it('should save name when it is new', async () => { | ||
metaDb.get.resolves({ | ||
_id: '_local/indexeddb-placeholder', | ||
db_names: [ 'db-a', 'db-b', 'db-c' ], | ||
}); | ||
service = TestBed.inject(IndexedDbService); | ||
|
||
await service.saveDatabaseName('db-new'); | ||
|
||
expect(metaDb.get.calledOnce).to.be.true; | ||
expect(metaDb.put.calledOnce).to.be.true; | ||
expect(metaDb.put.args[0][0]).to.deep.equal({ | ||
_id: '_local/indexeddb-placeholder', | ||
db_names: [ 'db-a', 'db-b', 'db-c', 'db-new' ], | ||
}); | ||
}); | ||
}); | ||
|
||
describe('deleteDatabaseName', () => { | ||
it('should do nothing when browser supports databases() function', async () => { | ||
documentMock.defaultView.indexedDB.databases = sinon.stub(); | ||
documentMock.defaultView.indexedDB.databases.resolves([ | ||
{ name: 'db-1' }, | ||
{ name: 'db-2' }, | ||
]); | ||
service = TestBed.inject(IndexedDbService); | ||
|
||
await service.deleteDatabaseName('db-1'); | ||
|
||
expect(documentMock.defaultView.indexedDB.databases.notCalled).to.be.true; | ||
expect(metaDb.get.notCalled).to.be.true; | ||
expect(metaDb.put.notCalled).to.be.true; | ||
}); | ||
|
||
it('should do nothing when name not found', async () => { | ||
metaDb.get.resolves({ | ||
db_names: [ 'db-a', 'db-b', 'db-c' ], | ||
}); | ||
service = TestBed.inject(IndexedDbService); | ||
|
||
await service.deleteDatabaseName('db-other'); | ||
|
||
expect(metaDb.get.calledOnce).to.be.true; | ||
expect(metaDb.put.notCalled).to.be.true; | ||
}); | ||
|
||
it('should do nothing when db_names in local doc is empty', async () => { | ||
metaDb.get.resolves({ db_names: [] }); | ||
service = TestBed.inject(IndexedDbService); | ||
|
||
await service.deleteDatabaseName('db-b'); | ||
|
||
expect(metaDb.get.calledOnce).to.be.true; | ||
expect(metaDb.put.notCalled).to.be.true; | ||
}); | ||
|
||
it('should delete name when it exists in db_names', async () => { | ||
metaDb.get.resolves({ | ||
_id: '_local/indexeddb-placeholder', | ||
db_names: [ 'db-a', 'db-b', 'db-c' ], | ||
}); | ||
service = TestBed.inject(IndexedDbService); | ||
|
||
await service.deleteDatabaseName('db-b'); | ||
|
||
expect(metaDb.get.calledOnce).to.be.true; | ||
expect(metaDb.put.calledOnce).to.be.true; | ||
expect(metaDb.put.args[0][0]).to.deep.equal({ | ||
_id: '_local/indexeddb-placeholder', | ||
db_names: [ 'db-a', 'db-c' ], | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.