From 52baaf2ce92c2edfa107ad29aa9fd50cccbdc908 Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Fri, 12 Jan 2024 13:34:54 -0600 Subject: [PATCH 1/5] tests for all error cases --- .../payload/src/spec/Diviner.Error.spec.ts | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 packages/modules/packages/diviner/packages/indexeddb/packages/payload/src/spec/Diviner.Error.spec.ts diff --git a/packages/modules/packages/diviner/packages/indexeddb/packages/payload/src/spec/Diviner.Error.spec.ts b/packages/modules/packages/diviner/packages/indexeddb/packages/payload/src/spec/Diviner.Error.spec.ts new file mode 100644 index 00000000000..df1ec92ba00 --- /dev/null +++ b/packages/modules/packages/diviner/packages/indexeddb/packages/payload/src/spec/Diviner.Error.spec.ts @@ -0,0 +1,112 @@ +/** + * @jest-environment jsdom + */ +/* eslint-disable max-nested-callbacks */ +import { Account } from '@xyo-network/account' +import { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb' +import { PayloadDivinerQuerySchema } from '@xyo-network/diviner-payload-model' +import { MemoryNode } from '@xyo-network/node-memory' +import { + IDBCursor, + IDBCursorWithValue, + IDBDatabase, + IDBFactory, + IDBIndex, + IDBKeyRange, + IDBObjectStore, + IDBOpenDBRequest, + IDBRequest, + IDBTransaction, + IDBVersionChangeEvent, + indexedDB, +} from 'fake-indexeddb' + +import { IndexedDbPayloadDiviner } from '../Diviner' + +// Augment window with prototypes to ensure instance of comparisons work +window.IDBCursor = IDBCursor +window.IDBCursorWithValue = IDBCursorWithValue +window.IDBDatabase = IDBDatabase +window.IDBFactory = IDBFactory +window.IDBIndex = IDBIndex +window.IDBKeyRange = IDBKeyRange +window.IDBObjectStore = IDBObjectStore +window.IDBOpenDBRequest = IDBOpenDBRequest +window.IDBRequest = IDBRequest +window.IDBTransaction = IDBTransaction +window.IDBVersionChangeEvent = IDBVersionChangeEvent +window.indexedDB = indexedDB + +/** + * @group module + * @group diviner + */ +describe('IndexedDbPayloadDiviner.Errors', () => { + const dbName = 'testDb' + const storeName = 'testStore' + let sut: IndexedDbPayloadDiviner + const values = [ + { + schema: 'network.xyo.test', + url: 'https://xyo.network', + }, + ] + describe('divine', () => { + const createTestNode = async (testDbName = 'INCORRECT-DB-NAME', testStoreName = 'INCORRECT-STORE-NAME') => { + const archivist = await IndexedDbArchivist.create({ + account: Account.randomSync(), + config: { dbName, schema: IndexedDbArchivist.configSchema, storeName }, + }) + await archivist.insert(values) + const sut = await IndexedDbPayloadDiviner.create({ + account: Account.randomSync(), + config: { + archivist: archivist.address, + dbName: testDbName, + schema: IndexedDbPayloadDiviner.configSchema, + storeName: testStoreName, + }, + }) + const node = await MemoryNode.create({ + account: Account.randomSync(), + config: { schema: MemoryNode.configSchema }, + }) + const modules = [archivist, sut] + await node.start() + await Promise.all( + modules.map(async (mod) => { + await node.register(mod) + await node.attach(mod.address, true) + }), + ) + return sut + } + describe('when DB and store do not exist', () => { + beforeAll(async () => { + sut = await createTestNode('INCORRECT-DB-NAME', 'INCORRECT-STORE-NAME') + }) + it('returns empty array', async () => { + const result = await sut.divine([{ schema: PayloadDivinerQuerySchema }]) + expect(result).toEqual([]) + }) + }) + describe('when DB exists but store does not exist', () => { + beforeAll(async () => { + sut = await createTestNode(dbName, 'INCORRECT-STORE-NAME') + }) + it('returns empty array', async () => { + const result = await sut.divine([{ schema: PayloadDivinerQuerySchema }]) + expect(result).toEqual([]) + }) + }) + describe('when DB and store exist', () => { + beforeAll(async () => { + sut = await createTestNode(dbName, storeName) + }) + it('returns values', async () => { + const result = await sut.divine([{ schema: PayloadDivinerQuerySchema }]) + expect(result).toEqual(values) + }) + }) + }) +}) From 3bcea0a61c996f9a5534a8b0d531fae7dc00db66 Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Fri, 12 Jan 2024 13:35:12 -0600 Subject: [PATCH 2/5] try initialize DBs before access --- .../packages/boundwitness/src/Diviner.ts | 34 +++++++++++++++---- .../indexeddb/packages/payload/src/Diviner.ts | 34 +++++++++++++++---- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/src/Diviner.ts b/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/src/Diviner.ts index e58f8902400..54056a0b635 100644 --- a/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/src/Diviner.ts +++ b/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/src/Diviner.ts @@ -62,17 +62,14 @@ export class IndexedDbBoundWitnessDiviner< return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName } - private get db(): IDBPDatabase { - return assertEx(this._db, 'DB not initialized') - } - protected override async divineHandler(payloads?: Payload[]): Promise { const query = assertEx(payloads?.filter(isBoundWitnessDivinerQueryPayload)?.pop(), 'Missing query payload') if (!query) return [] - this._db = await openDB(this.dbName, this.dbVersion) + const db = await this.tryGetInitializedDb() + if (!db) return [] // eslint-disable-next-line @typescript-eslint/no-unused-vars const { addresses, payload_hashes, payload_schemas, limit, offset, order } = query - const tx = this.db.transaction(this.storeName, 'readonly') + const tx = db.transaction(this.storeName, 'readonly') const store = tx.objectStore(this.storeName) const results: BoundWitness[] = [] let parsedOffset = offset ?? 0 @@ -124,4 +121,29 @@ export class IndexedDbBoundWitnessDiviner< await super.startHandler() return true } + + /** + * Checks that the desired DB/Store exists and is initialized + * @returns The initialized DB or undefined if it does not exist + */ + private async tryGetInitializedDb(): Promise | undefined> { + // If we've already checked and found a successfully initialized + // db and objectStore, return the cached value + if (this._db) return this._db + // Enumerate the DBs + const dbs = await indexedDB.databases() + const dbExists = dbs.some((db) => { + // Check for the desired name/version + return db.name === this.dbName && db.version === this.dbVersion + }) + // If the DB does not exist at the desired version, return undefined + if (!dbExists) return + // If the db does exist, open it + const db = await openDB(this.dbName, this.dbVersion) + // Check that the desired objectStore exists + const storeExists = db.objectStoreNames.contains(this.storeName) + // If the correct db/store exists, cache it for future calls + if (storeExists) this._db = db + return this._db + } } diff --git a/packages/modules/packages/diviner/packages/indexeddb/packages/payload/src/Diviner.ts b/packages/modules/packages/diviner/packages/indexeddb/packages/payload/src/Diviner.ts index 544f263ce09..e5f478cafdb 100644 --- a/packages/modules/packages/diviner/packages/indexeddb/packages/payload/src/Diviner.ts +++ b/packages/modules/packages/diviner/packages/indexeddb/packages/payload/src/Diviner.ts @@ -68,17 +68,14 @@ export class IndexedDbPayloadDiviner< return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName } - private get db(): IDBPDatabase { - return assertEx(this._db, 'DB not initialized') - } - protected override async divineHandler(payloads?: TIn[]): Promise { const query = assertEx(payloads?.filter(isPayloadDivinerQueryPayload)?.pop(), 'Missing query payload') if (!query) return [] - this._db = await openDB(this.dbName, this.dbVersion) + const db = await this.tryGetInitializedDb() + if (!db) return [] // eslint-disable-next-line @typescript-eslint/no-unused-vars const { schemas, limit, offset, hash, order, schema: _schema, sources, ...props } = query as unknown as TIn & { sources?: string[] } - const tx = this.db.transaction(this.storeName, 'readonly') + const tx = db.transaction(this.storeName, 'readonly') const store = tx.objectStore(this.storeName) const results: TOut[] = [] let parsedOffset = offset ?? 0 @@ -178,4 +175,29 @@ export class IndexedDbPayloadDiviner< } return bestMatch.matchCount > 0 ? bestMatch.indexName : null } + + /** + * Checks that the desired DB/Store exists and is initialized + * @returns The initialized DB or undefined if it does not exist + */ + private async tryGetInitializedDb(): Promise | undefined> { + // If we've already checked and found a successfully initialized + // db and objectStore, return the cached value + if (this._db) return this._db + // Enumerate the DBs + const dbs = await indexedDB.databases() + const dbExists = dbs.some((db) => { + // Check for the desired name/version + return db.name === this.dbName && db.version === this.dbVersion + }) + // If the DB does not exist at the desired version, return undefined + if (!dbExists) return + // If the db does exist, open it + const db = await openDB(this.dbName, this.dbVersion) + // Check that the desired objectStore exists + const storeExists = db.objectStoreNames.contains(this.storeName) + // If the correct db/store exists, cache it for future calls + if (storeExists) this._db = db + return this._db + } } From aa585fac29075ce553a111cba481c878f8af6c75 Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Fri, 12 Jan 2024 13:41:22 -0600 Subject: [PATCH 3/5] Diviner error case tests --- .../src/spec/Diviner.Error.spec.ts | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/src/spec/Diviner.Error.spec.ts diff --git a/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/src/spec/Diviner.Error.spec.ts b/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/src/spec/Diviner.Error.spec.ts new file mode 100644 index 00000000000..3c6a3d52be0 --- /dev/null +++ b/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/src/spec/Diviner.Error.spec.ts @@ -0,0 +1,111 @@ +/** + * @jest-environment jsdom + */ +/* eslint-disable max-nested-callbacks */ +import { Account } from '@xyo-network/account' +import { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb' +import { BoundWitnessBuilder } from '@xyo-network/boundwitness-builder' +import { BoundWitness } from '@xyo-network/boundwitness-model' +import { BoundWitnessDivinerQuerySchema } from '@xyo-network/diviner-boundwitness-model' +import { MemoryNode } from '@xyo-network/node-memory' +import { + IDBCursor, + IDBCursorWithValue, + IDBDatabase, + IDBFactory, + IDBIndex, + IDBKeyRange, + IDBObjectStore, + IDBOpenDBRequest, + IDBRequest, + IDBTransaction, + IDBVersionChangeEvent, + indexedDB, +} from 'fake-indexeddb' + +import { IndexedDbBoundWitnessDiviner } from '../Diviner' + +// Augment window with prototypes to ensure instance of comparisons work +window.IDBCursor = IDBCursor +window.IDBCursorWithValue = IDBCursorWithValue +window.IDBDatabase = IDBDatabase +window.IDBFactory = IDBFactory +window.IDBIndex = IDBIndex +window.IDBKeyRange = IDBKeyRange +window.IDBObjectStore = IDBObjectStore +window.IDBOpenDBRequest = IDBOpenDBRequest +window.IDBRequest = IDBRequest +window.IDBTransaction = IDBTransaction +window.IDBVersionChangeEvent = IDBVersionChangeEvent +window.indexedDB = indexedDB + +/** + * @group module + * @group diviner + */ +describe('IndexedDbBoundWitnessDiviner.Errors', () => { + const dbName = 'testDb' + const storeName = 'testStore' + let sut: IndexedDbBoundWitnessDiviner + const values: BoundWitness[] = [] + describe('divine', () => { + const createTestNode = async (testDbName = 'INCORRECT-DB-NAME', testStoreName = 'INCORRECT-STORE-NAME') => { + const archivist = await IndexedDbArchivist.create({ + account: Account.randomSync(), + config: { dbName, schema: IndexedDbArchivist.configSchema, storeName }, + }) + const [bw] = await new BoundWitnessBuilder().build() + values.push(bw) + await archivist.insert(values) + const sut = await IndexedDbBoundWitnessDiviner.create({ + account: Account.randomSync(), + config: { + archivist: archivist.address, + dbName: testDbName, + schema: IndexedDbBoundWitnessDiviner.configSchema, + storeName: testStoreName, + }, + }) + const node = await MemoryNode.create({ + account: Account.randomSync(), + config: { schema: MemoryNode.configSchema }, + }) + const modules = [archivist, sut] + await node.start() + await Promise.all( + modules.map(async (mod) => { + await node.register(mod) + await node.attach(mod.address, true) + }), + ) + return sut + } + describe('when DB and store do not exist', () => { + beforeAll(async () => { + sut = await createTestNode('INCORRECT-DB-NAME', 'INCORRECT-STORE-NAME') + }) + it('returns empty array', async () => { + const result = await sut.divine([{ schema: BoundWitnessDivinerQuerySchema }]) + expect(result).toEqual([]) + }) + }) + describe('when DB exists but store does not exist', () => { + beforeAll(async () => { + sut = await createTestNode(dbName, 'INCORRECT-STORE-NAME') + }) + it('returns empty array', async () => { + const result = await sut.divine([{ schema: BoundWitnessDivinerQuerySchema }]) + expect(result).toEqual([]) + }) + }) + describe('when DB and store exist', () => { + beforeAll(async () => { + sut = await createTestNode(dbName, storeName) + }) + it('returns values', async () => { + const result = await sut.divine([{ schema: BoundWitnessDivinerQuerySchema }]) + expect(result).toEqual(values) + }) + }) + }) +}) From 48f52a3ea49964430c68f1746d581cdbcefa41af Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Fri, 12 Jan 2024 13:42:08 -0600 Subject: [PATCH 4/5] Allow diviner to no-op when no valid queries presented --- .../packages/indexeddb/packages/boundwitness/src/Diviner.ts | 2 +- .../diviner/packages/indexeddb/packages/payload/src/Diviner.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/src/Diviner.ts b/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/src/Diviner.ts index 54056a0b635..cac877d2353 100644 --- a/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/src/Diviner.ts +++ b/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/src/Diviner.ts @@ -63,7 +63,7 @@ export class IndexedDbBoundWitnessDiviner< } protected override async divineHandler(payloads?: Payload[]): Promise { - const query = assertEx(payloads?.filter(isBoundWitnessDivinerQueryPayload)?.pop(), 'Missing query payload') + const query = payloads?.filter(isBoundWitnessDivinerQueryPayload)?.pop() if (!query) return [] const db = await this.tryGetInitializedDb() if (!db) return [] diff --git a/packages/modules/packages/diviner/packages/indexeddb/packages/payload/src/Diviner.ts b/packages/modules/packages/diviner/packages/indexeddb/packages/payload/src/Diviner.ts index e5f478cafdb..dec9cbfecc0 100644 --- a/packages/modules/packages/diviner/packages/indexeddb/packages/payload/src/Diviner.ts +++ b/packages/modules/packages/diviner/packages/indexeddb/packages/payload/src/Diviner.ts @@ -69,7 +69,7 @@ export class IndexedDbPayloadDiviner< } protected override async divineHandler(payloads?: TIn[]): Promise { - const query = assertEx(payloads?.filter(isPayloadDivinerQueryPayload)?.pop(), 'Missing query payload') + const query = payloads?.filter(isPayloadDivinerQueryPayload)?.pop() if (!query) return [] const db = await this.tryGetInitializedDb() if (!db) return [] From 7cda1de5bdfd90321c2802c927068f07ae82f957 Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Fri, 12 Jan 2024 13:49:26 -0600 Subject: [PATCH 5/5] Remove unused imports --- .../packages/indexeddb/packages/boundwitness/package.json | 1 - .../packages/indexeddb/packages/boundwitness/src/Diviner.ts | 1 - yarn.lock | 1 - 3 files changed, 3 deletions(-) diff --git a/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/package.json b/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/package.json index 9842c78ce89..45410a54430 100644 --- a/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/package.json +++ b/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/package.json @@ -11,7 +11,6 @@ }, "dependencies": { "@xylabs/array": "^2.13.23", - "@xylabs/assert": "^2.13.23", "@xylabs/exists": "^2.13.23", "@xyo-network/archivist-model": "workspace:~", "@xyo-network/boundwitness-model": "workspace:~", diff --git a/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/src/Diviner.ts b/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/src/Diviner.ts index cac877d2353..20ae8db8365 100644 --- a/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/src/Diviner.ts +++ b/packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness/src/Diviner.ts @@ -1,5 +1,4 @@ import { containsAll } from '@xylabs/array' -import { assertEx } from '@xylabs/assert' import { exists } from '@xylabs/exists' import { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb' import { BoundWitness, BoundWitnessSchema, isBoundWitness } from '@xyo-network/boundwitness-model' diff --git a/yarn.lock b/yarn.lock index fe9b07e9fce..3081e98042f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3654,7 +3654,6 @@ __metadata: resolution: "@xyo-network/diviner-boundwitness-indexeddb@workspace:packages/modules/packages/diviner/packages/indexeddb/packages/boundwitness" dependencies: "@xylabs/array": "npm:^2.13.23" - "@xylabs/assert": "npm:^2.13.23" "@xylabs/exists": "npm:^2.13.23" "@xylabs/ts-scripts-yarn3": "npm:^3.2.33" "@xylabs/tsconfig": "npm:^3.2.33"