Skip to content

Commit

Permalink
Merge pull request #1189 from XYOracleNetwork/feature/indexed-db-init…
Browse files Browse the repository at this point in the history
…ialization-state-detection

Indexed DB Initialization State Detection
  • Loading branch information
JoelBCarter authored Jan 12, 2024
2 parents 27277e9 + 7cda1de commit 84eca9a
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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:~",
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -62,17 +61,14 @@ export class IndexedDbBoundWitnessDiviner<
return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName
}

private get db(): IDBPDatabase<BoundWitnessStore> {
return assertEx(this._db, 'DB not initialized')
}

protected override async divineHandler(payloads?: Payload[]): Promise<BoundWitness[]> {
const query = assertEx(payloads?.filter(isBoundWitnessDivinerQueryPayload)?.pop(), 'Missing query payload')
const query = payloads?.filter(isBoundWitnessDivinerQueryPayload)?.pop()
if (!query) return []
this._db = await openDB<BoundWitnessStore>(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
Expand Down Expand Up @@ -124,4 +120,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<IDBPDatabase<BoundWitnessStore> | 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<BoundWitnessStore>(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
}
}
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,14 @@ export class IndexedDbPayloadDiviner<
return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName
}

private get db(): IDBPDatabase<PayloadStore> {
return assertEx(this._db, 'DB not initialized')
}

protected override async divineHandler(payloads?: TIn[]): Promise<TOut[]> {
const query = assertEx(payloads?.filter(isPayloadDivinerQueryPayload)?.pop(), 'Missing query payload')
const query = payloads?.filter(isPayloadDivinerQueryPayload)?.pop()
if (!query) return []
this._db = await openDB<PayloadStore>(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
Expand Down Expand Up @@ -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<IDBPDatabase<PayloadStore> | 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<PayloadStore>(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
}
}
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
})
1 change: 0 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 84eca9a

Please sign in to comment.