@xyo-network/diviner-payload-indexeddb 2.110.19 → 2.111.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -117,10 +117,7 @@ var IndexedDbPayloadDiviner = class extends import_diviner_payload_abstract.Payl
117
117
  const suggestedIndex = this.selectBestIndex(filter, store);
118
118
  const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter);
119
119
  const valueFilters = props ? Object.entries(props).map(([key, value]) => payloadValueFilter(key, value)).filter(import_exists.exists) : [];
120
- let cursor = suggestedIndex ? (
121
- // Conditionally filter on schemas
122
- await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)
123
- ) : await store.openCursor(suggestedIndex, direction);
120
+ let cursor = suggestedIndex ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction) : await store.openCursor(suggestedIndex, direction);
124
121
  while (cursor && parsedOffset > 0) {
125
122
  cursor = await cursor.advance(parsedOffset);
126
123
  parsedOffset = 0;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/index.ts","../../src/Schema.ts","../../src/Config.ts","../../src/Diviner.ts"],"sourcesContent":["export * from './Config.ts'\nexport * from './Diviner.ts'\nexport * from './Params.ts'\nexport * from './Schema.ts'\n","import { PayloadDivinerSchema } from '@xyo-network/diviner-payload-model'\n\nexport const IndexedDbPayloadDivinerSchema = `${PayloadDivinerSchema}.indexeddb` as const\nexport type IndexedDbPayloadDivinerSchema = typeof IndexedDbPayloadDivinerSchema\n","import { IndexDescription } from '@xyo-network/archivist-model'\nimport { DivinerConfig } from '@xyo-network/diviner-model'\n\nimport { IndexedDbPayloadDivinerSchema } from './Schema.ts'\n\nexport const IndexedDbPayloadDivinerConfigSchema = `${IndexedDbPayloadDivinerSchema}.config` as const\nexport type IndexedDbPayloadDivinerConfigSchema = typeof IndexedDbPayloadDivinerConfigSchema\n\nexport type IndexedDbPayloadDivinerConfig = DivinerConfig<{\n /**\n * The database name\n */\n dbName?: string\n /**\n * The version of the DB, defaults to 1\n */\n dbVersion?: number\n schema: IndexedDbPayloadDivinerConfigSchema\n /**\n * The storage configuration\n * // TODO: Hoist to main diviner config\n */\n storage?: {\n /**\n * The indexes to create on the object store\n */\n indexes?: IndexDescription[]\n }\n /**\n * The name of the object store\n */\n storeName?: string\n}>\n","import { containsAll } from '@xylabs/array'\nimport { assertEx } from '@xylabs/assert'\nimport { exists } from '@xylabs/exists'\nimport { Hash } from '@xylabs/hex'\nimport { AnyObject, removeFields } from '@xylabs/object'\nimport { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'\nimport { IndexSeparator } from '@xyo-network/archivist-model'\nimport { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'\nimport { isPayloadDivinerQueryPayload, PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport { PayloadBuilder } from '@xyo-network/payload-builder'\nimport { Payload, Schema, WithMeta } from '@xyo-network/payload-model'\nimport { IDBPDatabase, IDBPObjectStore, openDB } from 'idb'\n\nimport { IndexedDbPayloadDivinerConfigSchema } from './Config.ts'\nimport { IndexedDbPayloadDivinerParams } from './Params.ts'\n\ninterface PayloadStore {\n [s: string]: Payload\n}\n\ntype AnyPayload = Payload<Record<string, unknown>>\n\ntype ValueFilter = (payload?: AnyPayload | null) => boolean\n\nconst payloadValueFilter = (key: keyof AnyPayload, value?: unknown | unknown[]): ValueFilter | undefined => {\n if (!value) return undefined\n return (payload) => {\n if (!payload) return false\n const sourceValue = payload?.[key]\n if (sourceValue === undefined) return false\n return Array.isArray(sourceValue) && Array.isArray(value) ? containsAll(sourceValue, value) : sourceValue == value\n }\n}\n\n// Function to extract fields from an index name\nconst extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map((field) => field.toLowerCase())\n}\n\nexport class IndexedDbPayloadDiviner<\n TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams,\n TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload,\n TOut extends Payload = Payload,\n TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<\n DivinerInstance<TParams, TIn, TOut>,\n TIn,\n TOut\n >,\n> extends PayloadDiviner<TParams, TIn, TOut, TEventData> {\n static override readonly configSchemas: Schema[] = [...super.configSchemas, IndexedDbPayloadDivinerConfigSchema]\n static override readonly defaultConfigSchema: Schema = IndexedDbPayloadDivinerConfigSchema\n\n private _db: IDBPDatabase<PayloadStore> | undefined\n\n /**\n * The database name. If not supplied via config, it defaults\n * to the archivist's name and if archivist's name is not supplied,\n * it defaults to `archivist`. This behavior\n * biases towards a single, isolated DB per archivist which seems to\n * make the most sense for 99% of use cases.\n */\n get dbName() {\n return this.config?.dbName ?? this.config?.archivist ?? IndexedDbArchivist.defaultDbName\n }\n\n /**\n * The database version. If not supplied via config, it defaults to the archivist default version.\n */\n get dbVersion() {\n return this.config?.dbVersion ?? IndexedDbArchivist.defaultDbVersion\n }\n\n /**\n * The name of the object store. If not supplied via config, it defaults\n * to `payloads`.\n */\n get storeName() {\n return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName\n }\n\n protected override async divineHandler(payloads?: TIn[]): Promise<TOut[]> {\n const query = payloads?.find(isPayloadDivinerQueryPayload) as TIn\n if (!query) return []\n const result = await this.tryUseDb(async (db) => {\n const { schemas, limit, offset, order, ...props } = removeFields(query as unknown as WithMeta<TIn> & { sources?: Hash[] }, [\n 'hash',\n 'schema',\n '$meta',\n '$hash',\n ])\n const tx = db.transaction(this.storeName, 'readonly')\n const store = tx.objectStore(this.storeName)\n const results: TOut[] = []\n let parsedOffset = offset ?? 0\n const parsedLimit = limit ?? 10\n assertEx((schemas?.length ?? 1) === 1, () => 'IndexedDbPayloadDiviner: Only one filter schema supported')\n const filterSchema = schemas?.[0]\n const filter = filterSchema ? { schema: filterSchema, ...props } : { ...props }\n const direction: IDBCursorDirection = order === 'desc' ? 'prev' : 'next'\n const suggestedIndex = this.selectBestIndex(filter, store)\n const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter)\n const valueFilters: ValueFilter[] =\n props ?\n Object.entries(props)\n .map(([key, value]) => payloadValueFilter(key, value))\n .filter(exists)\n : []\n let cursor =\n suggestedIndex ?\n // Conditionally filter on schemas\n await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)\n // Just iterate all records\n : await store.openCursor(suggestedIndex, direction)\n\n // Skip records until the offset is reached\n while (cursor && parsedOffset > 0) {\n cursor = await cursor.advance(parsedOffset)\n parsedOffset = 0 // Reset offset after skipping\n }\n // Collect results up to the limit\n while (cursor && results.length < parsedLimit) {\n const value = cursor.value\n if (value) {\n // If we're filtering on more than just the schema\n if (valueFilters.length > 0) {\n // Ensure all filters pass\n if (valueFilters.every((filter) => filter(value))) {\n // Then save the value\n results.push(value)\n }\n } else {\n // Otherwise just save the value\n results.push(value)\n }\n }\n try {\n cursor = await cursor.continue()\n } catch {\n break\n }\n }\n await tx.done\n // Remove any metadata before returning to the client\n return await Promise.all(results.map((payload) => PayloadBuilder.build(payload)))\n })\n return result ?? []\n }\n\n protected override async startHandler() {\n await super.startHandler()\n return true\n }\n\n private getKeyRangeValue(indexName: string | null, query: AnyObject): unknown | unknown[] {\n if (!indexName) return []\n\n // Extracting the relevant fields from the index name\n const indexFields = extractFields(indexName)\n\n // Collecting the values for these fields from the query object\n const keyRangeValue = indexFields.map((field) => query[field as keyof AnyObject])\n return keyRangeValue.length === 1 ? keyRangeValue[0] : keyRangeValue\n }\n\n private selectBestIndex(query: AnyObject, store: IDBPObjectStore<PayloadStore>): string | null {\n // List of available indexes\n const { indexNames } = store\n\n // Convert query object keys to a set for easier comparison\n const queryKeys = new Set(Object.keys(query).map((key) => key.toLowerCase()))\n\n // Find the best matching index\n let bestMatch: { indexName: string; matchCount: number } = { indexName: '', matchCount: 0 }\n\n for (const indexName of indexNames) {\n const indexFields = extractFields(indexName)\n const matchCount = indexFields.filter((field) => queryKeys.has(field)).length\n if (matchCount > bestMatch.matchCount) {\n bestMatch = { indexName, matchCount }\n }\n }\n return bestMatch.matchCount > 0 ? bestMatch.indexName : null\n }\n\n /**\n * Checks that the desired DB/objectStore exists and is initialized to the correct version\n * @returns The initialized DB or undefined if it does not exist in the desired state\n */\n private async tryGetInitializedDb(): Promise<IDBPDatabase<PayloadStore> | undefined> {\n // Enumerate the DBs\n const dbs = await indexedDB.databases()\n // Check that the DB exists at the desired version\n const dbExists = dbs.some((db) => {\n return db.name === this.dbName && db.version === this.dbVersion\n })\n // If the DB exists at the desired version\n if (dbExists) {\n // If the db does exist, open it\n const db = await openDB<PayloadStore>(this.dbName, this.dbVersion)\n // Check that the desired objectStore exists\n const storeExists = db.objectStoreNames.contains(this.storeName)\n // If the correct db/version/objectStore exists\n if (storeExists) {\n return db\n } else {\n // Otherwise close the db so the process that is going to update the\n // db can open it\n db.close()\n }\n }\n }\n\n /**\n * Executes a callback with the initialized DB and then closes the db\n * @param callback The method to execute with the initialized DB\n * @returns\n */\n private async tryUseDb<T>(callback: (db: IDBPDatabase<PayloadStore>) => Promise<T> | T): Promise<T | undefined> {\n // Get the initialized DB\n const db = await this.tryGetInitializedDb()\n if (db) {\n try {\n // Perform the callback\n return await callback(db)\n } finally {\n // Close the DB\n db.close()\n }\n }\n return undefined\n }\n}\n"],"mappings":"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","names":["IndexedDbPayloadDivinerSchema","PayloadDivinerSchema","IndexedDbPayloadDivinerConfigSchema","IndexedDbPayloadDivinerSchema","import_diviner_payload_model","payloadValueFilter","key","value","undefined","payload","sourceValue","Array","isArray","containsAll","extractFields","indexName","slice","split","IndexSeparator","map","field","toLowerCase","IndexedDbPayloadDiviner","PayloadDiviner","configSchemas","IndexedDbPayloadDivinerConfigSchema","defaultConfigSchema","_db","dbName","config","archivist","IndexedDbArchivist","defaultDbName","dbVersion","defaultDbVersion","storeName","defaultStoreName","divineHandler","payloads","query","find","isPayloadDivinerQueryPayload","result","tryUseDb","db","schemas","limit","offset","order","props","removeFields","tx","transaction","store","objectStore","results","parsedOffset","parsedLimit","assertEx","length","filterSchema","filter","schema","direction","suggestedIndex","selectBestIndex","keyRangeValue","getKeyRangeValue","valueFilters","Object","entries","exists","cursor","index","openCursor","IDBKeyRange","only","advance","every","push","continue","done","Promise","all","PayloadBuilder","build","startHandler","indexFields","indexNames","queryKeys","Set","keys","bestMatch","matchCount","has","tryGetInitializedDb","dbs","indexedDB","databases","dbExists","some","name","version","openDB","storeExists","objectStoreNames","contains","close","callback"]}
1
+ {"version":3,"sources":["../../src/index.ts","../../src/Schema.ts","../../src/Config.ts","../../src/Diviner.ts"],"sourcesContent":["export * from './Config.ts'\nexport * from './Diviner.ts'\nexport * from './Params.ts'\nexport * from './Schema.ts'\n","import { PayloadDivinerSchema } from '@xyo-network/diviner-payload-model'\n\nexport const IndexedDbPayloadDivinerSchema = `${PayloadDivinerSchema}.indexeddb` as const\nexport type IndexedDbPayloadDivinerSchema = typeof IndexedDbPayloadDivinerSchema\n","import { IndexDescription } from '@xyo-network/archivist-model'\nimport { DivinerConfig } from '@xyo-network/diviner-model'\n\nimport { IndexedDbPayloadDivinerSchema } from './Schema.ts'\n\nexport const IndexedDbPayloadDivinerConfigSchema = `${IndexedDbPayloadDivinerSchema}.config` as const\nexport type IndexedDbPayloadDivinerConfigSchema = typeof IndexedDbPayloadDivinerConfigSchema\n\nexport type IndexedDbPayloadDivinerConfig = DivinerConfig<{\n /**\n * The database name\n */\n dbName?: string\n /**\n * The version of the DB, defaults to 1\n */\n dbVersion?: number\n schema: IndexedDbPayloadDivinerConfigSchema\n /**\n * The storage configuration\n * // TODO: Hoist to main diviner config\n */\n storage?: {\n /**\n * The indexes to create on the object store\n */\n indexes?: IndexDescription[]\n }\n /**\n * The name of the object store\n */\n storeName?: string\n}>\n","import { containsAll } from '@xylabs/array'\nimport { assertEx } from '@xylabs/assert'\nimport { exists } from '@xylabs/exists'\nimport { Hash } from '@xylabs/hex'\nimport { AnyObject, removeFields } from '@xylabs/object'\nimport { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'\nimport { IndexSeparator } from '@xyo-network/archivist-model'\nimport { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'\nimport { isPayloadDivinerQueryPayload, PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport { PayloadBuilder } from '@xyo-network/payload-builder'\nimport { Payload, Schema, WithMeta } from '@xyo-network/payload-model'\nimport { IDBPDatabase, IDBPObjectStore, openDB } from 'idb'\n\nimport { IndexedDbPayloadDivinerConfigSchema } from './Config.ts'\nimport { IndexedDbPayloadDivinerParams } from './Params.ts'\n\ninterface PayloadStore {\n [s: string]: Payload\n}\n\ntype AnyPayload = Payload<Record<string, unknown>>\n\ntype ValueFilter = (payload?: AnyPayload | null) => boolean\n\nconst payloadValueFilter = (key: keyof AnyPayload, value?: unknown | unknown[]): ValueFilter | undefined => {\n if (!value) return undefined\n return (payload) => {\n if (!payload) return false\n const sourceValue = payload?.[key]\n if (sourceValue === undefined) return false\n return Array.isArray(sourceValue) && Array.isArray(value) ? containsAll(sourceValue, value) : sourceValue == value\n }\n}\n\n// Function to extract fields from an index name\nconst extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map(field => field.toLowerCase())\n}\n\nexport class IndexedDbPayloadDiviner<\n TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams,\n TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload,\n TOut extends Payload = Payload,\n TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<\n DivinerInstance<TParams, TIn, TOut>,\n TIn,\n TOut\n >,\n> extends PayloadDiviner<TParams, TIn, TOut, TEventData> {\n static override readonly configSchemas: Schema[] = [...super.configSchemas, IndexedDbPayloadDivinerConfigSchema]\n static override readonly defaultConfigSchema: Schema = IndexedDbPayloadDivinerConfigSchema\n\n private _db: IDBPDatabase<PayloadStore> | undefined\n\n /**\n * The database name. If not supplied via config, it defaults\n * to the archivist's name and if archivist's name is not supplied,\n * it defaults to `archivist`. This behavior\n * biases towards a single, isolated DB per archivist which seems to\n * make the most sense for 99% of use cases.\n */\n get dbName() {\n return this.config?.dbName ?? this.config?.archivist ?? IndexedDbArchivist.defaultDbName\n }\n\n /**\n * The database version. If not supplied via config, it defaults to the archivist default version.\n */\n get dbVersion() {\n return this.config?.dbVersion ?? IndexedDbArchivist.defaultDbVersion\n }\n\n /**\n * The name of the object store. If not supplied via config, it defaults\n * to `payloads`.\n */\n get storeName() {\n return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName\n }\n\n protected override async divineHandler(payloads?: TIn[]): Promise<TOut[]> {\n const query = payloads?.find(isPayloadDivinerQueryPayload) as TIn\n if (!query) return []\n const result = await this.tryUseDb(async (db) => {\n const { schemas, limit, offset, order, ...props } = removeFields(query as unknown as WithMeta<TIn> & { sources?: Hash[] }, [\n 'hash',\n 'schema',\n '$meta',\n '$hash',\n ])\n const tx = db.transaction(this.storeName, 'readonly')\n const store = tx.objectStore(this.storeName)\n const results: TOut[] = []\n let parsedOffset = offset ?? 0\n const parsedLimit = limit ?? 10\n assertEx((schemas?.length ?? 1) === 1, () => 'IndexedDbPayloadDiviner: Only one filter schema supported')\n const filterSchema = schemas?.[0]\n const filter = filterSchema ? { schema: filterSchema, ...props } : { ...props }\n const direction: IDBCursorDirection = order === 'desc' ? 'prev' : 'next'\n const suggestedIndex = this.selectBestIndex(filter, store)\n const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter)\n const valueFilters: ValueFilter[]\n = props\n ? Object.entries(props)\n .map(([key, value]) => payloadValueFilter(key, value))\n .filter(exists)\n : []\n let cursor\n = suggestedIndex\n // Conditionally filter on schemas\n ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)\n // Just iterate all records\n : await store.openCursor(suggestedIndex, direction)\n\n // Skip records until the offset is reached\n while (cursor && parsedOffset > 0) {\n cursor = await cursor.advance(parsedOffset)\n parsedOffset = 0 // Reset offset after skipping\n }\n // Collect results up to the limit\n while (cursor && results.length < parsedLimit) {\n const value = cursor.value\n if (value) {\n // If we're filtering on more than just the schema\n if (valueFilters.length > 0) {\n // Ensure all filters pass\n if (valueFilters.every(filter => filter(value))) {\n // Then save the value\n results.push(value)\n }\n } else {\n // Otherwise just save the value\n results.push(value)\n }\n }\n try {\n cursor = await cursor.continue()\n } catch {\n break\n }\n }\n await tx.done\n // Remove any metadata before returning to the client\n return await Promise.all(results.map(payload => PayloadBuilder.build(payload)))\n })\n return result ?? []\n }\n\n protected override async startHandler() {\n await super.startHandler()\n return true\n }\n\n private getKeyRangeValue(indexName: string | null, query: AnyObject): unknown | unknown[] {\n if (!indexName) return []\n\n // Extracting the relevant fields from the index name\n const indexFields = extractFields(indexName)\n\n // Collecting the values for these fields from the query object\n const keyRangeValue = indexFields.map(field => query[field as keyof AnyObject])\n return keyRangeValue.length === 1 ? keyRangeValue[0] : keyRangeValue\n }\n\n private selectBestIndex(query: AnyObject, store: IDBPObjectStore<PayloadStore>): string | null {\n // List of available indexes\n const { indexNames } = store\n\n // Convert query object keys to a set for easier comparison\n const queryKeys = new Set(Object.keys(query).map(key => key.toLowerCase()))\n\n // Find the best matching index\n let bestMatch: { indexName: string; matchCount: number } = { indexName: '', matchCount: 0 }\n\n for (const indexName of indexNames) {\n const indexFields = extractFields(indexName)\n const matchCount = indexFields.filter(field => queryKeys.has(field)).length\n if (matchCount > bestMatch.matchCount) {\n bestMatch = { indexName, matchCount }\n }\n }\n return bestMatch.matchCount > 0 ? bestMatch.indexName : null\n }\n\n /**\n * Checks that the desired DB/objectStore exists and is initialized to the correct version\n * @returns The initialized DB or undefined if it does not exist in the desired state\n */\n private async tryGetInitializedDb(): Promise<IDBPDatabase<PayloadStore> | undefined> {\n // Enumerate the DBs\n const dbs = await indexedDB.databases()\n // Check that the DB exists at the desired version\n const dbExists = dbs.some((db) => {\n return db.name === this.dbName && db.version === this.dbVersion\n })\n // If the DB exists at the desired version\n if (dbExists) {\n // If the db does exist, open it\n const db = await openDB<PayloadStore>(this.dbName, this.dbVersion)\n // Check that the desired objectStore exists\n const storeExists = db.objectStoreNames.contains(this.storeName)\n // If the correct db/version/objectStore exists\n if (storeExists) {\n return db\n } else {\n // Otherwise close the db so the process that is going to update the\n // db can open it\n db.close()\n }\n }\n }\n\n /**\n * Executes a callback with the initialized DB and then closes the db\n * @param callback The method to execute with the initialized DB\n * @returns\n */\n private async tryUseDb<T>(callback: (db: IDBPDatabase<PayloadStore>) => Promise<T> | T): Promise<T | undefined> {\n // Get the initialized DB\n const db = await this.tryGetInitializedDb()\n if (db) {\n try {\n // Perform the callback\n return await callback(db)\n } finally {\n // Close the DB\n db.close()\n }\n }\n return undefined\n }\n}\n"],"mappings":"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","names":["IndexedDbPayloadDivinerSchema","PayloadDivinerSchema","IndexedDbPayloadDivinerConfigSchema","IndexedDbPayloadDivinerSchema","import_diviner_payload_model","payloadValueFilter","key","value","undefined","payload","sourceValue","Array","isArray","containsAll","extractFields","indexName","slice","split","IndexSeparator","map","field","toLowerCase","IndexedDbPayloadDiviner","PayloadDiviner","configSchemas","IndexedDbPayloadDivinerConfigSchema","defaultConfigSchema","_db","dbName","config","archivist","IndexedDbArchivist","defaultDbName","dbVersion","defaultDbVersion","storeName","defaultStoreName","divineHandler","payloads","query","find","isPayloadDivinerQueryPayload","result","tryUseDb","db","schemas","limit","offset","order","props","removeFields","tx","transaction","store","objectStore","results","parsedOffset","parsedLimit","assertEx","length","filterSchema","filter","schema","direction","suggestedIndex","selectBestIndex","keyRangeValue","getKeyRangeValue","valueFilters","Object","entries","exists","cursor","index","openCursor","IDBKeyRange","only","advance","every","push","continue","done","Promise","all","PayloadBuilder","build","startHandler","indexFields","indexNames","queryKeys","Set","keys","bestMatch","matchCount","has","tryGetInitializedDb","dbs","indexedDB","databases","dbExists","some","name","version","openDB","storeExists","objectStoreNames","contains","close","callback"]}
@@ -91,10 +91,7 @@ var IndexedDbPayloadDiviner = class extends PayloadDiviner {
91
91
  const suggestedIndex = this.selectBestIndex(filter, store);
92
92
  const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter);
93
93
  const valueFilters = props ? Object.entries(props).map(([key, value]) => payloadValueFilter(key, value)).filter(exists) : [];
94
- let cursor = suggestedIndex ? (
95
- // Conditionally filter on schemas
96
- await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)
97
- ) : await store.openCursor(suggestedIndex, direction);
94
+ let cursor = suggestedIndex ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction) : await store.openCursor(suggestedIndex, direction);
98
95
  while (cursor && parsedOffset > 0) {
99
96
  cursor = await cursor.advance(parsedOffset);
100
97
  parsedOffset = 0;
@@ -191,4 +188,4 @@ export {
191
188
  IndexedDbPayloadDivinerConfigSchema,
192
189
  IndexedDbPayloadDivinerSchema
193
190
  };
194
- //# sourceMappingURL=index.js.map
191
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/Schema.ts","../../src/Config.ts","../../src/Diviner.ts"],"sourcesContent":["import { PayloadDivinerSchema } from '@xyo-network/diviner-payload-model'\n\nexport const IndexedDbPayloadDivinerSchema = `${PayloadDivinerSchema}.indexeddb` as const\nexport type IndexedDbPayloadDivinerSchema = typeof IndexedDbPayloadDivinerSchema\n","import { IndexDescription } from '@xyo-network/archivist-model'\nimport { DivinerConfig } from '@xyo-network/diviner-model'\n\nimport { IndexedDbPayloadDivinerSchema } from './Schema.ts'\n\nexport const IndexedDbPayloadDivinerConfigSchema = `${IndexedDbPayloadDivinerSchema}.config` as const\nexport type IndexedDbPayloadDivinerConfigSchema = typeof IndexedDbPayloadDivinerConfigSchema\n\nexport type IndexedDbPayloadDivinerConfig = DivinerConfig<{\n /**\n * The database name\n */\n dbName?: string\n /**\n * The version of the DB, defaults to 1\n */\n dbVersion?: number\n schema: IndexedDbPayloadDivinerConfigSchema\n /**\n * The storage configuration\n * // TODO: Hoist to main diviner config\n */\n storage?: {\n /**\n * The indexes to create on the object store\n */\n indexes?: IndexDescription[]\n }\n /**\n * The name of the object store\n */\n storeName?: string\n}>\n","import { containsAll } from '@xylabs/array'\nimport { assertEx } from '@xylabs/assert'\nimport { exists } from '@xylabs/exists'\nimport { Hash } from '@xylabs/hex'\nimport { AnyObject, removeFields } from '@xylabs/object'\nimport { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'\nimport { IndexSeparator } from '@xyo-network/archivist-model'\nimport { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'\nimport { isPayloadDivinerQueryPayload, PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport { PayloadBuilder } from '@xyo-network/payload-builder'\nimport { Payload, Schema, WithMeta } from '@xyo-network/payload-model'\nimport { IDBPDatabase, IDBPObjectStore, openDB } from 'idb'\n\nimport { IndexedDbPayloadDivinerConfigSchema } from './Config.ts'\nimport { IndexedDbPayloadDivinerParams } from './Params.ts'\n\ninterface PayloadStore {\n [s: string]: Payload\n}\n\ntype AnyPayload = Payload<Record<string, unknown>>\n\ntype ValueFilter = (payload?: AnyPayload | null) => boolean\n\nconst payloadValueFilter = (key: keyof AnyPayload, value?: unknown | unknown[]): ValueFilter | undefined => {\n if (!value) return undefined\n return (payload) => {\n if (!payload) return false\n const sourceValue = payload?.[key]\n if (sourceValue === undefined) return false\n return Array.isArray(sourceValue) && Array.isArray(value) ? containsAll(sourceValue, value) : sourceValue == value\n }\n}\n\n// Function to extract fields from an index name\nconst extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map(field => field.toLowerCase())\n}\n\nexport class IndexedDbPayloadDiviner<\n TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams,\n TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload,\n TOut extends Payload = Payload,\n TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<\n DivinerInstance<TParams, TIn, TOut>,\n TIn,\n TOut\n >,\n> extends PayloadDiviner<TParams, TIn, TOut, TEventData> {\n static override readonly configSchemas: Schema[] = [...super.configSchemas, IndexedDbPayloadDivinerConfigSchema]\n static override readonly defaultConfigSchema: Schema = IndexedDbPayloadDivinerConfigSchema\n\n private _db: IDBPDatabase<PayloadStore> | undefined\n\n /**\n * The database name. If not supplied via config, it defaults\n * to the archivist's name and if archivist's name is not supplied,\n * it defaults to `archivist`. This behavior\n * biases towards a single, isolated DB per archivist which seems to\n * make the most sense for 99% of use cases.\n */\n get dbName() {\n return this.config?.dbName ?? this.config?.archivist ?? IndexedDbArchivist.defaultDbName\n }\n\n /**\n * The database version. If not supplied via config, it defaults to the archivist default version.\n */\n get dbVersion() {\n return this.config?.dbVersion ?? IndexedDbArchivist.defaultDbVersion\n }\n\n /**\n * The name of the object store. If not supplied via config, it defaults\n * to `payloads`.\n */\n get storeName() {\n return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName\n }\n\n protected override async divineHandler(payloads?: TIn[]): Promise<TOut[]> {\n const query = payloads?.find(isPayloadDivinerQueryPayload) as TIn\n if (!query) return []\n const result = await this.tryUseDb(async (db) => {\n const { schemas, limit, offset, order, ...props } = removeFields(query as unknown as WithMeta<TIn> & { sources?: Hash[] }, [\n 'hash',\n 'schema',\n '$meta',\n '$hash',\n ])\n const tx = db.transaction(this.storeName, 'readonly')\n const store = tx.objectStore(this.storeName)\n const results: TOut[] = []\n let parsedOffset = offset ?? 0\n const parsedLimit = limit ?? 10\n assertEx((schemas?.length ?? 1) === 1, () => 'IndexedDbPayloadDiviner: Only one filter schema supported')\n const filterSchema = schemas?.[0]\n const filter = filterSchema ? { schema: filterSchema, ...props } : { ...props }\n const direction: IDBCursorDirection = order === 'desc' ? 'prev' : 'next'\n const suggestedIndex = this.selectBestIndex(filter, store)\n const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter)\n const valueFilters: ValueFilter[]\n = props\n ? Object.entries(props)\n .map(([key, value]) => payloadValueFilter(key, value))\n .filter(exists)\n : []\n let cursor\n = suggestedIndex\n // Conditionally filter on schemas\n ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)\n // Just iterate all records\n : await store.openCursor(suggestedIndex, direction)\n\n // Skip records until the offset is reached\n while (cursor && parsedOffset > 0) {\n cursor = await cursor.advance(parsedOffset)\n parsedOffset = 0 // Reset offset after skipping\n }\n // Collect results up to the limit\n while (cursor && results.length < parsedLimit) {\n const value = cursor.value\n if (value) {\n // If we're filtering on more than just the schema\n if (valueFilters.length > 0) {\n // Ensure all filters pass\n if (valueFilters.every(filter => filter(value))) {\n // Then save the value\n results.push(value)\n }\n } else {\n // Otherwise just save the value\n results.push(value)\n }\n }\n try {\n cursor = await cursor.continue()\n } catch {\n break\n }\n }\n await tx.done\n // Remove any metadata before returning to the client\n return await Promise.all(results.map(payload => PayloadBuilder.build(payload)))\n })\n return result ?? []\n }\n\n protected override async startHandler() {\n await super.startHandler()\n return true\n }\n\n private getKeyRangeValue(indexName: string | null, query: AnyObject): unknown | unknown[] {\n if (!indexName) return []\n\n // Extracting the relevant fields from the index name\n const indexFields = extractFields(indexName)\n\n // Collecting the values for these fields from the query object\n const keyRangeValue = indexFields.map(field => query[field as keyof AnyObject])\n return keyRangeValue.length === 1 ? keyRangeValue[0] : keyRangeValue\n }\n\n private selectBestIndex(query: AnyObject, store: IDBPObjectStore<PayloadStore>): string | null {\n // List of available indexes\n const { indexNames } = store\n\n // Convert query object keys to a set for easier comparison\n const queryKeys = new Set(Object.keys(query).map(key => key.toLowerCase()))\n\n // Find the best matching index\n let bestMatch: { indexName: string; matchCount: number } = { indexName: '', matchCount: 0 }\n\n for (const indexName of indexNames) {\n const indexFields = extractFields(indexName)\n const matchCount = indexFields.filter(field => queryKeys.has(field)).length\n if (matchCount > bestMatch.matchCount) {\n bestMatch = { indexName, matchCount }\n }\n }\n return bestMatch.matchCount > 0 ? bestMatch.indexName : null\n }\n\n /**\n * Checks that the desired DB/objectStore exists and is initialized to the correct version\n * @returns The initialized DB or undefined if it does not exist in the desired state\n */\n private async tryGetInitializedDb(): Promise<IDBPDatabase<PayloadStore> | undefined> {\n // Enumerate the DBs\n const dbs = await indexedDB.databases()\n // Check that the DB exists at the desired version\n const dbExists = dbs.some((db) => {\n return db.name === this.dbName && db.version === this.dbVersion\n })\n // If the DB exists at the desired version\n if (dbExists) {\n // If the db does exist, open it\n const db = await openDB<PayloadStore>(this.dbName, this.dbVersion)\n // Check that the desired objectStore exists\n const storeExists = db.objectStoreNames.contains(this.storeName)\n // If the correct db/version/objectStore exists\n if (storeExists) {\n return db\n } else {\n // Otherwise close the db so the process that is going to update the\n // db can open it\n db.close()\n }\n }\n }\n\n /**\n * Executes a callback with the initialized DB and then closes the db\n * @param callback The method to execute with the initialized DB\n * @returns\n */\n private async tryUseDb<T>(callback: (db: IDBPDatabase<PayloadStore>) => Promise<T> | T): Promise<T | undefined> {\n // Get the initialized DB\n const db = await this.tryGetInitializedDb()\n if (db) {\n try {\n // Perform the callback\n return await callback(db)\n } finally {\n // Close the DB\n db.close()\n }\n }\n return undefined\n }\n}\n"],"mappings":"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","names":["PayloadDivinerSchema","IndexedDbPayloadDivinerSchema","IndexedDbPayloadDivinerConfigSchema","IndexedDbPayloadDivinerSchema","containsAll","assertEx","exists","removeFields","IndexedDbArchivist","IndexSeparator","PayloadDiviner","isPayloadDivinerQueryPayload","PayloadBuilder","openDB","payloadValueFilter","key","value","undefined","payload","sourceValue","Array","isArray","containsAll","extractFields","indexName","slice","split","IndexSeparator","map","field","toLowerCase","IndexedDbPayloadDiviner","PayloadDiviner","configSchemas","IndexedDbPayloadDivinerConfigSchema","defaultConfigSchema","_db","dbName","config","archivist","IndexedDbArchivist","defaultDbName","dbVersion","defaultDbVersion","storeName","defaultStoreName","divineHandler","payloads","query","find","isPayloadDivinerQueryPayload","result","tryUseDb","db","schemas","limit","offset","order","props","removeFields","tx","transaction","store","objectStore","results","parsedOffset","parsedLimit","assertEx","length","filterSchema","filter","schema","direction","suggestedIndex","selectBestIndex","keyRangeValue","getKeyRangeValue","valueFilters","Object","entries","exists","cursor","index","openCursor","IDBKeyRange","only","advance","every","push","continue","done","Promise","all","PayloadBuilder","build","startHandler","indexFields","indexNames","queryKeys","Set","keys","bestMatch","matchCount","has","tryGetInitializedDb","dbs","indexedDB","databases","dbExists","some","name","version","openDB","storeExists","objectStoreNames","contains","close","callback"]}
@@ -117,10 +117,7 @@ var IndexedDbPayloadDiviner = class extends import_diviner_payload_abstract.Payl
117
117
  const suggestedIndex = this.selectBestIndex(filter, store);
118
118
  const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter);
119
119
  const valueFilters = props ? Object.entries(props).map(([key, value]) => payloadValueFilter(key, value)).filter(import_exists.exists) : [];
120
- let cursor = suggestedIndex ? (
121
- // Conditionally filter on schemas
122
- await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)
123
- ) : await store.openCursor(suggestedIndex, direction);
120
+ let cursor = suggestedIndex ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction) : await store.openCursor(suggestedIndex, direction);
124
121
  while (cursor && parsedOffset > 0) {
125
122
  cursor = await cursor.advance(parsedOffset);
126
123
  parsedOffset = 0;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/index.ts","../../src/Schema.ts","../../src/Config.ts","../../src/Diviner.ts"],"sourcesContent":["export * from './Config.ts'\nexport * from './Diviner.ts'\nexport * from './Params.ts'\nexport * from './Schema.ts'\n","import { PayloadDivinerSchema } from '@xyo-network/diviner-payload-model'\n\nexport const IndexedDbPayloadDivinerSchema = `${PayloadDivinerSchema}.indexeddb` as const\nexport type IndexedDbPayloadDivinerSchema = typeof IndexedDbPayloadDivinerSchema\n","import { IndexDescription } from '@xyo-network/archivist-model'\nimport { DivinerConfig } from '@xyo-network/diviner-model'\n\nimport { IndexedDbPayloadDivinerSchema } from './Schema.ts'\n\nexport const IndexedDbPayloadDivinerConfigSchema = `${IndexedDbPayloadDivinerSchema}.config` as const\nexport type IndexedDbPayloadDivinerConfigSchema = typeof IndexedDbPayloadDivinerConfigSchema\n\nexport type IndexedDbPayloadDivinerConfig = DivinerConfig<{\n /**\n * The database name\n */\n dbName?: string\n /**\n * The version of the DB, defaults to 1\n */\n dbVersion?: number\n schema: IndexedDbPayloadDivinerConfigSchema\n /**\n * The storage configuration\n * // TODO: Hoist to main diviner config\n */\n storage?: {\n /**\n * The indexes to create on the object store\n */\n indexes?: IndexDescription[]\n }\n /**\n * The name of the object store\n */\n storeName?: string\n}>\n","import { containsAll } from '@xylabs/array'\nimport { assertEx } from '@xylabs/assert'\nimport { exists } from '@xylabs/exists'\nimport { Hash } from '@xylabs/hex'\nimport { AnyObject, removeFields } from '@xylabs/object'\nimport { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'\nimport { IndexSeparator } from '@xyo-network/archivist-model'\nimport { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'\nimport { isPayloadDivinerQueryPayload, PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport { PayloadBuilder } from '@xyo-network/payload-builder'\nimport { Payload, Schema, WithMeta } from '@xyo-network/payload-model'\nimport { IDBPDatabase, IDBPObjectStore, openDB } from 'idb'\n\nimport { IndexedDbPayloadDivinerConfigSchema } from './Config.ts'\nimport { IndexedDbPayloadDivinerParams } from './Params.ts'\n\ninterface PayloadStore {\n [s: string]: Payload\n}\n\ntype AnyPayload = Payload<Record<string, unknown>>\n\ntype ValueFilter = (payload?: AnyPayload | null) => boolean\n\nconst payloadValueFilter = (key: keyof AnyPayload, value?: unknown | unknown[]): ValueFilter | undefined => {\n if (!value) return undefined\n return (payload) => {\n if (!payload) return false\n const sourceValue = payload?.[key]\n if (sourceValue === undefined) return false\n return Array.isArray(sourceValue) && Array.isArray(value) ? containsAll(sourceValue, value) : sourceValue == value\n }\n}\n\n// Function to extract fields from an index name\nconst extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map((field) => field.toLowerCase())\n}\n\nexport class IndexedDbPayloadDiviner<\n TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams,\n TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload,\n TOut extends Payload = Payload,\n TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<\n DivinerInstance<TParams, TIn, TOut>,\n TIn,\n TOut\n >,\n> extends PayloadDiviner<TParams, TIn, TOut, TEventData> {\n static override readonly configSchemas: Schema[] = [...super.configSchemas, IndexedDbPayloadDivinerConfigSchema]\n static override readonly defaultConfigSchema: Schema = IndexedDbPayloadDivinerConfigSchema\n\n private _db: IDBPDatabase<PayloadStore> | undefined\n\n /**\n * The database name. If not supplied via config, it defaults\n * to the archivist's name and if archivist's name is not supplied,\n * it defaults to `archivist`. This behavior\n * biases towards a single, isolated DB per archivist which seems to\n * make the most sense for 99% of use cases.\n */\n get dbName() {\n return this.config?.dbName ?? this.config?.archivist ?? IndexedDbArchivist.defaultDbName\n }\n\n /**\n * The database version. If not supplied via config, it defaults to the archivist default version.\n */\n get dbVersion() {\n return this.config?.dbVersion ?? IndexedDbArchivist.defaultDbVersion\n }\n\n /**\n * The name of the object store. If not supplied via config, it defaults\n * to `payloads`.\n */\n get storeName() {\n return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName\n }\n\n protected override async divineHandler(payloads?: TIn[]): Promise<TOut[]> {\n const query = payloads?.find(isPayloadDivinerQueryPayload) as TIn\n if (!query) return []\n const result = await this.tryUseDb(async (db) => {\n const { schemas, limit, offset, order, ...props } = removeFields(query as unknown as WithMeta<TIn> & { sources?: Hash[] }, [\n 'hash',\n 'schema',\n '$meta',\n '$hash',\n ])\n const tx = db.transaction(this.storeName, 'readonly')\n const store = tx.objectStore(this.storeName)\n const results: TOut[] = []\n let parsedOffset = offset ?? 0\n const parsedLimit = limit ?? 10\n assertEx((schemas?.length ?? 1) === 1, () => 'IndexedDbPayloadDiviner: Only one filter schema supported')\n const filterSchema = schemas?.[0]\n const filter = filterSchema ? { schema: filterSchema, ...props } : { ...props }\n const direction: IDBCursorDirection = order === 'desc' ? 'prev' : 'next'\n const suggestedIndex = this.selectBestIndex(filter, store)\n const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter)\n const valueFilters: ValueFilter[] =\n props ?\n Object.entries(props)\n .map(([key, value]) => payloadValueFilter(key, value))\n .filter(exists)\n : []\n let cursor =\n suggestedIndex ?\n // Conditionally filter on schemas\n await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)\n // Just iterate all records\n : await store.openCursor(suggestedIndex, direction)\n\n // Skip records until the offset is reached\n while (cursor && parsedOffset > 0) {\n cursor = await cursor.advance(parsedOffset)\n parsedOffset = 0 // Reset offset after skipping\n }\n // Collect results up to the limit\n while (cursor && results.length < parsedLimit) {\n const value = cursor.value\n if (value) {\n // If we're filtering on more than just the schema\n if (valueFilters.length > 0) {\n // Ensure all filters pass\n if (valueFilters.every((filter) => filter(value))) {\n // Then save the value\n results.push(value)\n }\n } else {\n // Otherwise just save the value\n results.push(value)\n }\n }\n try {\n cursor = await cursor.continue()\n } catch {\n break\n }\n }\n await tx.done\n // Remove any metadata before returning to the client\n return await Promise.all(results.map((payload) => PayloadBuilder.build(payload)))\n })\n return result ?? []\n }\n\n protected override async startHandler() {\n await super.startHandler()\n return true\n }\n\n private getKeyRangeValue(indexName: string | null, query: AnyObject): unknown | unknown[] {\n if (!indexName) return []\n\n // Extracting the relevant fields from the index name\n const indexFields = extractFields(indexName)\n\n // Collecting the values for these fields from the query object\n const keyRangeValue = indexFields.map((field) => query[field as keyof AnyObject])\n return keyRangeValue.length === 1 ? keyRangeValue[0] : keyRangeValue\n }\n\n private selectBestIndex(query: AnyObject, store: IDBPObjectStore<PayloadStore>): string | null {\n // List of available indexes\n const { indexNames } = store\n\n // Convert query object keys to a set for easier comparison\n const queryKeys = new Set(Object.keys(query).map((key) => key.toLowerCase()))\n\n // Find the best matching index\n let bestMatch: { indexName: string; matchCount: number } = { indexName: '', matchCount: 0 }\n\n for (const indexName of indexNames) {\n const indexFields = extractFields(indexName)\n const matchCount = indexFields.filter((field) => queryKeys.has(field)).length\n if (matchCount > bestMatch.matchCount) {\n bestMatch = { indexName, matchCount }\n }\n }\n return bestMatch.matchCount > 0 ? bestMatch.indexName : null\n }\n\n /**\n * Checks that the desired DB/objectStore exists and is initialized to the correct version\n * @returns The initialized DB or undefined if it does not exist in the desired state\n */\n private async tryGetInitializedDb(): Promise<IDBPDatabase<PayloadStore> | undefined> {\n // Enumerate the DBs\n const dbs = await indexedDB.databases()\n // Check that the DB exists at the desired version\n const dbExists = dbs.some((db) => {\n return db.name === this.dbName && db.version === this.dbVersion\n })\n // If the DB exists at the desired version\n if (dbExists) {\n // If the db does exist, open it\n const db = await openDB<PayloadStore>(this.dbName, this.dbVersion)\n // Check that the desired objectStore exists\n const storeExists = db.objectStoreNames.contains(this.storeName)\n // If the correct db/version/objectStore exists\n if (storeExists) {\n return db\n } else {\n // Otherwise close the db so the process that is going to update the\n // db can open it\n db.close()\n }\n }\n }\n\n /**\n * Executes a callback with the initialized DB and then closes the db\n * @param callback The method to execute with the initialized DB\n * @returns\n */\n private async tryUseDb<T>(callback: (db: IDBPDatabase<PayloadStore>) => Promise<T> | T): Promise<T | undefined> {\n // Get the initialized DB\n const db = await this.tryGetInitializedDb()\n if (db) {\n try {\n // Perform the callback\n return await callback(db)\n } finally {\n // Close the DB\n db.close()\n }\n }\n return undefined\n }\n}\n"],"mappings":"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","names":["IndexedDbPayloadDivinerSchema","PayloadDivinerSchema","IndexedDbPayloadDivinerConfigSchema","IndexedDbPayloadDivinerSchema","import_diviner_payload_model","payloadValueFilter","key","value","undefined","payload","sourceValue","Array","isArray","containsAll","extractFields","indexName","slice","split","IndexSeparator","map","field","toLowerCase","IndexedDbPayloadDiviner","PayloadDiviner","configSchemas","IndexedDbPayloadDivinerConfigSchema","defaultConfigSchema","_db","dbName","config","archivist","IndexedDbArchivist","defaultDbName","dbVersion","defaultDbVersion","storeName","defaultStoreName","divineHandler","payloads","query","find","isPayloadDivinerQueryPayload","result","tryUseDb","db","schemas","limit","offset","order","props","removeFields","tx","transaction","store","objectStore","results","parsedOffset","parsedLimit","assertEx","length","filterSchema","filter","schema","direction","suggestedIndex","selectBestIndex","keyRangeValue","getKeyRangeValue","valueFilters","Object","entries","exists","cursor","index","openCursor","IDBKeyRange","only","advance","every","push","continue","done","Promise","all","PayloadBuilder","build","startHandler","indexFields","indexNames","queryKeys","Set","keys","bestMatch","matchCount","has","tryGetInitializedDb","dbs","indexedDB","databases","dbExists","some","name","version","openDB","storeExists","objectStoreNames","contains","close","callback"]}
1
+ {"version":3,"sources":["../../src/index.ts","../../src/Schema.ts","../../src/Config.ts","../../src/Diviner.ts"],"sourcesContent":["export * from './Config.ts'\nexport * from './Diviner.ts'\nexport * from './Params.ts'\nexport * from './Schema.ts'\n","import { PayloadDivinerSchema } from '@xyo-network/diviner-payload-model'\n\nexport const IndexedDbPayloadDivinerSchema = `${PayloadDivinerSchema}.indexeddb` as const\nexport type IndexedDbPayloadDivinerSchema = typeof IndexedDbPayloadDivinerSchema\n","import { IndexDescription } from '@xyo-network/archivist-model'\nimport { DivinerConfig } from '@xyo-network/diviner-model'\n\nimport { IndexedDbPayloadDivinerSchema } from './Schema.ts'\n\nexport const IndexedDbPayloadDivinerConfigSchema = `${IndexedDbPayloadDivinerSchema}.config` as const\nexport type IndexedDbPayloadDivinerConfigSchema = typeof IndexedDbPayloadDivinerConfigSchema\n\nexport type IndexedDbPayloadDivinerConfig = DivinerConfig<{\n /**\n * The database name\n */\n dbName?: string\n /**\n * The version of the DB, defaults to 1\n */\n dbVersion?: number\n schema: IndexedDbPayloadDivinerConfigSchema\n /**\n * The storage configuration\n * // TODO: Hoist to main diviner config\n */\n storage?: {\n /**\n * The indexes to create on the object store\n */\n indexes?: IndexDescription[]\n }\n /**\n * The name of the object store\n */\n storeName?: string\n}>\n","import { containsAll } from '@xylabs/array'\nimport { assertEx } from '@xylabs/assert'\nimport { exists } from '@xylabs/exists'\nimport { Hash } from '@xylabs/hex'\nimport { AnyObject, removeFields } from '@xylabs/object'\nimport { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'\nimport { IndexSeparator } from '@xyo-network/archivist-model'\nimport { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'\nimport { isPayloadDivinerQueryPayload, PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport { PayloadBuilder } from '@xyo-network/payload-builder'\nimport { Payload, Schema, WithMeta } from '@xyo-network/payload-model'\nimport { IDBPDatabase, IDBPObjectStore, openDB } from 'idb'\n\nimport { IndexedDbPayloadDivinerConfigSchema } from './Config.ts'\nimport { IndexedDbPayloadDivinerParams } from './Params.ts'\n\ninterface PayloadStore {\n [s: string]: Payload\n}\n\ntype AnyPayload = Payload<Record<string, unknown>>\n\ntype ValueFilter = (payload?: AnyPayload | null) => boolean\n\nconst payloadValueFilter = (key: keyof AnyPayload, value?: unknown | unknown[]): ValueFilter | undefined => {\n if (!value) return undefined\n return (payload) => {\n if (!payload) return false\n const sourceValue = payload?.[key]\n if (sourceValue === undefined) return false\n return Array.isArray(sourceValue) && Array.isArray(value) ? containsAll(sourceValue, value) : sourceValue == value\n }\n}\n\n// Function to extract fields from an index name\nconst extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map(field => field.toLowerCase())\n}\n\nexport class IndexedDbPayloadDiviner<\n TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams,\n TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload,\n TOut extends Payload = Payload,\n TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<\n DivinerInstance<TParams, TIn, TOut>,\n TIn,\n TOut\n >,\n> extends PayloadDiviner<TParams, TIn, TOut, TEventData> {\n static override readonly configSchemas: Schema[] = [...super.configSchemas, IndexedDbPayloadDivinerConfigSchema]\n static override readonly defaultConfigSchema: Schema = IndexedDbPayloadDivinerConfigSchema\n\n private _db: IDBPDatabase<PayloadStore> | undefined\n\n /**\n * The database name. If not supplied via config, it defaults\n * to the archivist's name and if archivist's name is not supplied,\n * it defaults to `archivist`. This behavior\n * biases towards a single, isolated DB per archivist which seems to\n * make the most sense for 99% of use cases.\n */\n get dbName() {\n return this.config?.dbName ?? this.config?.archivist ?? IndexedDbArchivist.defaultDbName\n }\n\n /**\n * The database version. If not supplied via config, it defaults to the archivist default version.\n */\n get dbVersion() {\n return this.config?.dbVersion ?? IndexedDbArchivist.defaultDbVersion\n }\n\n /**\n * The name of the object store. If not supplied via config, it defaults\n * to `payloads`.\n */\n get storeName() {\n return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName\n }\n\n protected override async divineHandler(payloads?: TIn[]): Promise<TOut[]> {\n const query = payloads?.find(isPayloadDivinerQueryPayload) as TIn\n if (!query) return []\n const result = await this.tryUseDb(async (db) => {\n const { schemas, limit, offset, order, ...props } = removeFields(query as unknown as WithMeta<TIn> & { sources?: Hash[] }, [\n 'hash',\n 'schema',\n '$meta',\n '$hash',\n ])\n const tx = db.transaction(this.storeName, 'readonly')\n const store = tx.objectStore(this.storeName)\n const results: TOut[] = []\n let parsedOffset = offset ?? 0\n const parsedLimit = limit ?? 10\n assertEx((schemas?.length ?? 1) === 1, () => 'IndexedDbPayloadDiviner: Only one filter schema supported')\n const filterSchema = schemas?.[0]\n const filter = filterSchema ? { schema: filterSchema, ...props } : { ...props }\n const direction: IDBCursorDirection = order === 'desc' ? 'prev' : 'next'\n const suggestedIndex = this.selectBestIndex(filter, store)\n const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter)\n const valueFilters: ValueFilter[]\n = props\n ? Object.entries(props)\n .map(([key, value]) => payloadValueFilter(key, value))\n .filter(exists)\n : []\n let cursor\n = suggestedIndex\n // Conditionally filter on schemas\n ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)\n // Just iterate all records\n : await store.openCursor(suggestedIndex, direction)\n\n // Skip records until the offset is reached\n while (cursor && parsedOffset > 0) {\n cursor = await cursor.advance(parsedOffset)\n parsedOffset = 0 // Reset offset after skipping\n }\n // Collect results up to the limit\n while (cursor && results.length < parsedLimit) {\n const value = cursor.value\n if (value) {\n // If we're filtering on more than just the schema\n if (valueFilters.length > 0) {\n // Ensure all filters pass\n if (valueFilters.every(filter => filter(value))) {\n // Then save the value\n results.push(value)\n }\n } else {\n // Otherwise just save the value\n results.push(value)\n }\n }\n try {\n cursor = await cursor.continue()\n } catch {\n break\n }\n }\n await tx.done\n // Remove any metadata before returning to the client\n return await Promise.all(results.map(payload => PayloadBuilder.build(payload)))\n })\n return result ?? []\n }\n\n protected override async startHandler() {\n await super.startHandler()\n return true\n }\n\n private getKeyRangeValue(indexName: string | null, query: AnyObject): unknown | unknown[] {\n if (!indexName) return []\n\n // Extracting the relevant fields from the index name\n const indexFields = extractFields(indexName)\n\n // Collecting the values for these fields from the query object\n const keyRangeValue = indexFields.map(field => query[field as keyof AnyObject])\n return keyRangeValue.length === 1 ? keyRangeValue[0] : keyRangeValue\n }\n\n private selectBestIndex(query: AnyObject, store: IDBPObjectStore<PayloadStore>): string | null {\n // List of available indexes\n const { indexNames } = store\n\n // Convert query object keys to a set for easier comparison\n const queryKeys = new Set(Object.keys(query).map(key => key.toLowerCase()))\n\n // Find the best matching index\n let bestMatch: { indexName: string; matchCount: number } = { indexName: '', matchCount: 0 }\n\n for (const indexName of indexNames) {\n const indexFields = extractFields(indexName)\n const matchCount = indexFields.filter(field => queryKeys.has(field)).length\n if (matchCount > bestMatch.matchCount) {\n bestMatch = { indexName, matchCount }\n }\n }\n return bestMatch.matchCount > 0 ? bestMatch.indexName : null\n }\n\n /**\n * Checks that the desired DB/objectStore exists and is initialized to the correct version\n * @returns The initialized DB or undefined if it does not exist in the desired state\n */\n private async tryGetInitializedDb(): Promise<IDBPDatabase<PayloadStore> | undefined> {\n // Enumerate the DBs\n const dbs = await indexedDB.databases()\n // Check that the DB exists at the desired version\n const dbExists = dbs.some((db) => {\n return db.name === this.dbName && db.version === this.dbVersion\n })\n // If the DB exists at the desired version\n if (dbExists) {\n // If the db does exist, open it\n const db = await openDB<PayloadStore>(this.dbName, this.dbVersion)\n // Check that the desired objectStore exists\n const storeExists = db.objectStoreNames.contains(this.storeName)\n // If the correct db/version/objectStore exists\n if (storeExists) {\n return db\n } else {\n // Otherwise close the db so the process that is going to update the\n // db can open it\n db.close()\n }\n }\n }\n\n /**\n * Executes a callback with the initialized DB and then closes the db\n * @param callback The method to execute with the initialized DB\n * @returns\n */\n private async tryUseDb<T>(callback: (db: IDBPDatabase<PayloadStore>) => Promise<T> | T): Promise<T | undefined> {\n // Get the initialized DB\n const db = await this.tryGetInitializedDb()\n if (db) {\n try {\n // Perform the callback\n return await callback(db)\n } finally {\n // Close the DB\n db.close()\n }\n }\n return undefined\n }\n}\n"],"mappings":"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","names":["IndexedDbPayloadDivinerSchema","PayloadDivinerSchema","IndexedDbPayloadDivinerConfigSchema","IndexedDbPayloadDivinerSchema","import_diviner_payload_model","payloadValueFilter","key","value","undefined","payload","sourceValue","Array","isArray","containsAll","extractFields","indexName","slice","split","IndexSeparator","map","field","toLowerCase","IndexedDbPayloadDiviner","PayloadDiviner","configSchemas","IndexedDbPayloadDivinerConfigSchema","defaultConfigSchema","_db","dbName","config","archivist","IndexedDbArchivist","defaultDbName","dbVersion","defaultDbVersion","storeName","defaultStoreName","divineHandler","payloads","query","find","isPayloadDivinerQueryPayload","result","tryUseDb","db","schemas","limit","offset","order","props","removeFields","tx","transaction","store","objectStore","results","parsedOffset","parsedLimit","assertEx","length","filterSchema","filter","schema","direction","suggestedIndex","selectBestIndex","keyRangeValue","getKeyRangeValue","valueFilters","Object","entries","exists","cursor","index","openCursor","IDBKeyRange","only","advance","every","push","continue","done","Promise","all","PayloadBuilder","build","startHandler","indexFields","indexNames","queryKeys","Set","keys","bestMatch","matchCount","has","tryGetInitializedDb","dbs","indexedDB","databases","dbExists","some","name","version","openDB","storeExists","objectStoreNames","contains","close","callback"]}
@@ -91,10 +91,7 @@ var IndexedDbPayloadDiviner = class extends PayloadDiviner {
91
91
  const suggestedIndex = this.selectBestIndex(filter, store);
92
92
  const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter);
93
93
  const valueFilters = props ? Object.entries(props).map(([key, value]) => payloadValueFilter(key, value)).filter(exists) : [];
94
- let cursor = suggestedIndex ? (
95
- // Conditionally filter on schemas
96
- await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)
97
- ) : await store.openCursor(suggestedIndex, direction);
94
+ let cursor = suggestedIndex ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction) : await store.openCursor(suggestedIndex, direction);
98
95
  while (cursor && parsedOffset > 0) {
99
96
  cursor = await cursor.advance(parsedOffset);
100
97
  parsedOffset = 0;
@@ -191,4 +188,4 @@ export {
191
188
  IndexedDbPayloadDivinerConfigSchema,
192
189
  IndexedDbPayloadDivinerSchema
193
190
  };
194
- //# sourceMappingURL=index.js.map
191
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/Schema.ts","../../src/Config.ts","../../src/Diviner.ts"],"sourcesContent":["import { PayloadDivinerSchema } from '@xyo-network/diviner-payload-model'\n\nexport const IndexedDbPayloadDivinerSchema = `${PayloadDivinerSchema}.indexeddb` as const\nexport type IndexedDbPayloadDivinerSchema = typeof IndexedDbPayloadDivinerSchema\n","import { IndexDescription } from '@xyo-network/archivist-model'\nimport { DivinerConfig } from '@xyo-network/diviner-model'\n\nimport { IndexedDbPayloadDivinerSchema } from './Schema.ts'\n\nexport const IndexedDbPayloadDivinerConfigSchema = `${IndexedDbPayloadDivinerSchema}.config` as const\nexport type IndexedDbPayloadDivinerConfigSchema = typeof IndexedDbPayloadDivinerConfigSchema\n\nexport type IndexedDbPayloadDivinerConfig = DivinerConfig<{\n /**\n * The database name\n */\n dbName?: string\n /**\n * The version of the DB, defaults to 1\n */\n dbVersion?: number\n schema: IndexedDbPayloadDivinerConfigSchema\n /**\n * The storage configuration\n * // TODO: Hoist to main diviner config\n */\n storage?: {\n /**\n * The indexes to create on the object store\n */\n indexes?: IndexDescription[]\n }\n /**\n * The name of the object store\n */\n storeName?: string\n}>\n","import { containsAll } from '@xylabs/array'\nimport { assertEx } from '@xylabs/assert'\nimport { exists } from '@xylabs/exists'\nimport { Hash } from '@xylabs/hex'\nimport { AnyObject, removeFields } from '@xylabs/object'\nimport { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'\nimport { IndexSeparator } from '@xyo-network/archivist-model'\nimport { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'\nimport { isPayloadDivinerQueryPayload, PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport { PayloadBuilder } from '@xyo-network/payload-builder'\nimport { Payload, Schema, WithMeta } from '@xyo-network/payload-model'\nimport { IDBPDatabase, IDBPObjectStore, openDB } from 'idb'\n\nimport { IndexedDbPayloadDivinerConfigSchema } from './Config.ts'\nimport { IndexedDbPayloadDivinerParams } from './Params.ts'\n\ninterface PayloadStore {\n [s: string]: Payload\n}\n\ntype AnyPayload = Payload<Record<string, unknown>>\n\ntype ValueFilter = (payload?: AnyPayload | null) => boolean\n\nconst payloadValueFilter = (key: keyof AnyPayload, value?: unknown | unknown[]): ValueFilter | undefined => {\n if (!value) return undefined\n return (payload) => {\n if (!payload) return false\n const sourceValue = payload?.[key]\n if (sourceValue === undefined) return false\n return Array.isArray(sourceValue) && Array.isArray(value) ? containsAll(sourceValue, value) : sourceValue == value\n }\n}\n\n// Function to extract fields from an index name\nconst extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map(field => field.toLowerCase())\n}\n\nexport class IndexedDbPayloadDiviner<\n TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams,\n TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload,\n TOut extends Payload = Payload,\n TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<\n DivinerInstance<TParams, TIn, TOut>,\n TIn,\n TOut\n >,\n> extends PayloadDiviner<TParams, TIn, TOut, TEventData> {\n static override readonly configSchemas: Schema[] = [...super.configSchemas, IndexedDbPayloadDivinerConfigSchema]\n static override readonly defaultConfigSchema: Schema = IndexedDbPayloadDivinerConfigSchema\n\n private _db: IDBPDatabase<PayloadStore> | undefined\n\n /**\n * The database name. If not supplied via config, it defaults\n * to the archivist's name and if archivist's name is not supplied,\n * it defaults to `archivist`. This behavior\n * biases towards a single, isolated DB per archivist which seems to\n * make the most sense for 99% of use cases.\n */\n get dbName() {\n return this.config?.dbName ?? this.config?.archivist ?? IndexedDbArchivist.defaultDbName\n }\n\n /**\n * The database version. If not supplied via config, it defaults to the archivist default version.\n */\n get dbVersion() {\n return this.config?.dbVersion ?? IndexedDbArchivist.defaultDbVersion\n }\n\n /**\n * The name of the object store. If not supplied via config, it defaults\n * to `payloads`.\n */\n get storeName() {\n return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName\n }\n\n protected override async divineHandler(payloads?: TIn[]): Promise<TOut[]> {\n const query = payloads?.find(isPayloadDivinerQueryPayload) as TIn\n if (!query) return []\n const result = await this.tryUseDb(async (db) => {\n const { schemas, limit, offset, order, ...props } = removeFields(query as unknown as WithMeta<TIn> & { sources?: Hash[] }, [\n 'hash',\n 'schema',\n '$meta',\n '$hash',\n ])\n const tx = db.transaction(this.storeName, 'readonly')\n const store = tx.objectStore(this.storeName)\n const results: TOut[] = []\n let parsedOffset = offset ?? 0\n const parsedLimit = limit ?? 10\n assertEx((schemas?.length ?? 1) === 1, () => 'IndexedDbPayloadDiviner: Only one filter schema supported')\n const filterSchema = schemas?.[0]\n const filter = filterSchema ? { schema: filterSchema, ...props } : { ...props }\n const direction: IDBCursorDirection = order === 'desc' ? 'prev' : 'next'\n const suggestedIndex = this.selectBestIndex(filter, store)\n const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter)\n const valueFilters: ValueFilter[]\n = props\n ? Object.entries(props)\n .map(([key, value]) => payloadValueFilter(key, value))\n .filter(exists)\n : []\n let cursor\n = suggestedIndex\n // Conditionally filter on schemas\n ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)\n // Just iterate all records\n : await store.openCursor(suggestedIndex, direction)\n\n // Skip records until the offset is reached\n while (cursor && parsedOffset > 0) {\n cursor = await cursor.advance(parsedOffset)\n parsedOffset = 0 // Reset offset after skipping\n }\n // Collect results up to the limit\n while (cursor && results.length < parsedLimit) {\n const value = cursor.value\n if (value) {\n // If we're filtering on more than just the schema\n if (valueFilters.length > 0) {\n // Ensure all filters pass\n if (valueFilters.every(filter => filter(value))) {\n // Then save the value\n results.push(value)\n }\n } else {\n // Otherwise just save the value\n results.push(value)\n }\n }\n try {\n cursor = await cursor.continue()\n } catch {\n break\n }\n }\n await tx.done\n // Remove any metadata before returning to the client\n return await Promise.all(results.map(payload => PayloadBuilder.build(payload)))\n })\n return result ?? []\n }\n\n protected override async startHandler() {\n await super.startHandler()\n return true\n }\n\n private getKeyRangeValue(indexName: string | null, query: AnyObject): unknown | unknown[] {\n if (!indexName) return []\n\n // Extracting the relevant fields from the index name\n const indexFields = extractFields(indexName)\n\n // Collecting the values for these fields from the query object\n const keyRangeValue = indexFields.map(field => query[field as keyof AnyObject])\n return keyRangeValue.length === 1 ? keyRangeValue[0] : keyRangeValue\n }\n\n private selectBestIndex(query: AnyObject, store: IDBPObjectStore<PayloadStore>): string | null {\n // List of available indexes\n const { indexNames } = store\n\n // Convert query object keys to a set for easier comparison\n const queryKeys = new Set(Object.keys(query).map(key => key.toLowerCase()))\n\n // Find the best matching index\n let bestMatch: { indexName: string; matchCount: number } = { indexName: '', matchCount: 0 }\n\n for (const indexName of indexNames) {\n const indexFields = extractFields(indexName)\n const matchCount = indexFields.filter(field => queryKeys.has(field)).length\n if (matchCount > bestMatch.matchCount) {\n bestMatch = { indexName, matchCount }\n }\n }\n return bestMatch.matchCount > 0 ? bestMatch.indexName : null\n }\n\n /**\n * Checks that the desired DB/objectStore exists and is initialized to the correct version\n * @returns The initialized DB or undefined if it does not exist in the desired state\n */\n private async tryGetInitializedDb(): Promise<IDBPDatabase<PayloadStore> | undefined> {\n // Enumerate the DBs\n const dbs = await indexedDB.databases()\n // Check that the DB exists at the desired version\n const dbExists = dbs.some((db) => {\n return db.name === this.dbName && db.version === this.dbVersion\n })\n // If the DB exists at the desired version\n if (dbExists) {\n // If the db does exist, open it\n const db = await openDB<PayloadStore>(this.dbName, this.dbVersion)\n // Check that the desired objectStore exists\n const storeExists = db.objectStoreNames.contains(this.storeName)\n // If the correct db/version/objectStore exists\n if (storeExists) {\n return db\n } else {\n // Otherwise close the db so the process that is going to update the\n // db can open it\n db.close()\n }\n }\n }\n\n /**\n * Executes a callback with the initialized DB and then closes the db\n * @param callback The method to execute with the initialized DB\n * @returns\n */\n private async tryUseDb<T>(callback: (db: IDBPDatabase<PayloadStore>) => Promise<T> | T): Promise<T | undefined> {\n // Get the initialized DB\n const db = await this.tryGetInitializedDb()\n if (db) {\n try {\n // Perform the callback\n return await callback(db)\n } finally {\n // Close the DB\n db.close()\n }\n }\n return undefined\n }\n}\n"],"mappings":"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","names":["PayloadDivinerSchema","IndexedDbPayloadDivinerSchema","IndexedDbPayloadDivinerConfigSchema","IndexedDbPayloadDivinerSchema","containsAll","assertEx","exists","removeFields","IndexedDbArchivist","IndexSeparator","PayloadDiviner","isPayloadDivinerQueryPayload","PayloadBuilder","openDB","payloadValueFilter","key","value","undefined","payload","sourceValue","Array","isArray","containsAll","extractFields","indexName","slice","split","IndexSeparator","map","field","toLowerCase","IndexedDbPayloadDiviner","PayloadDiviner","configSchemas","IndexedDbPayloadDivinerConfigSchema","defaultConfigSchema","_db","dbName","config","archivist","IndexedDbArchivist","defaultDbName","dbVersion","defaultDbVersion","storeName","defaultStoreName","divineHandler","payloads","query","find","isPayloadDivinerQueryPayload","result","tryUseDb","db","schemas","limit","offset","order","props","removeFields","tx","transaction","store","objectStore","results","parsedOffset","parsedLimit","assertEx","length","filterSchema","filter","schema","direction","suggestedIndex","selectBestIndex","keyRangeValue","getKeyRangeValue","valueFilters","Object","entries","exists","cursor","index","openCursor","IDBKeyRange","only","advance","every","push","continue","done","Promise","all","PayloadBuilder","build","startHandler","indexFields","indexNames","queryKeys","Set","keys","bestMatch","matchCount","has","tryGetInitializedDb","dbs","indexedDB","databases","dbExists","some","name","version","openDB","storeExists","objectStoreNames","contains","close","callback"]}
@@ -117,10 +117,7 @@ var _IndexedDbPayloadDiviner = class _IndexedDbPayloadDiviner extends import_div
117
117
  const suggestedIndex = this.selectBestIndex(filter, store);
118
118
  const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter);
119
119
  const valueFilters = props ? Object.entries(props).map(([key, value]) => payloadValueFilter(key, value)).filter(import_exists.exists) : [];
120
- let cursor = suggestedIndex ? (
121
- // Conditionally filter on schemas
122
- await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)
123
- ) : await store.openCursor(suggestedIndex, direction);
120
+ let cursor = suggestedIndex ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction) : await store.openCursor(suggestedIndex, direction);
124
121
  while (cursor && parsedOffset > 0) {
125
122
  cursor = await cursor.advance(parsedOffset);
126
123
  parsedOffset = 0;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/index.ts","../../src/Schema.ts","../../src/Config.ts","../../src/Diviner.ts"],"sourcesContent":["export * from './Config.ts'\nexport * from './Diviner.ts'\nexport * from './Params.ts'\nexport * from './Schema.ts'\n","import { PayloadDivinerSchema } from '@xyo-network/diviner-payload-model'\n\nexport const IndexedDbPayloadDivinerSchema = `${PayloadDivinerSchema}.indexeddb` as const\nexport type IndexedDbPayloadDivinerSchema = typeof IndexedDbPayloadDivinerSchema\n","import { IndexDescription } from '@xyo-network/archivist-model'\nimport { DivinerConfig } from '@xyo-network/diviner-model'\n\nimport { IndexedDbPayloadDivinerSchema } from './Schema.ts'\n\nexport const IndexedDbPayloadDivinerConfigSchema = `${IndexedDbPayloadDivinerSchema}.config` as const\nexport type IndexedDbPayloadDivinerConfigSchema = typeof IndexedDbPayloadDivinerConfigSchema\n\nexport type IndexedDbPayloadDivinerConfig = DivinerConfig<{\n /**\n * The database name\n */\n dbName?: string\n /**\n * The version of the DB, defaults to 1\n */\n dbVersion?: number\n schema: IndexedDbPayloadDivinerConfigSchema\n /**\n * The storage configuration\n * // TODO: Hoist to main diviner config\n */\n storage?: {\n /**\n * The indexes to create on the object store\n */\n indexes?: IndexDescription[]\n }\n /**\n * The name of the object store\n */\n storeName?: string\n}>\n","import { containsAll } from '@xylabs/array'\nimport { assertEx } from '@xylabs/assert'\nimport { exists } from '@xylabs/exists'\nimport { Hash } from '@xylabs/hex'\nimport { AnyObject, removeFields } from '@xylabs/object'\nimport { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'\nimport { IndexSeparator } from '@xyo-network/archivist-model'\nimport { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'\nimport { isPayloadDivinerQueryPayload, PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport { PayloadBuilder } from '@xyo-network/payload-builder'\nimport { Payload, Schema, WithMeta } from '@xyo-network/payload-model'\nimport { IDBPDatabase, IDBPObjectStore, openDB } from 'idb'\n\nimport { IndexedDbPayloadDivinerConfigSchema } from './Config.ts'\nimport { IndexedDbPayloadDivinerParams } from './Params.ts'\n\ninterface PayloadStore {\n [s: string]: Payload\n}\n\ntype AnyPayload = Payload<Record<string, unknown>>\n\ntype ValueFilter = (payload?: AnyPayload | null) => boolean\n\nconst payloadValueFilter = (key: keyof AnyPayload, value?: unknown | unknown[]): ValueFilter | undefined => {\n if (!value) return undefined\n return (payload) => {\n if (!payload) return false\n const sourceValue = payload?.[key]\n if (sourceValue === undefined) return false\n return Array.isArray(sourceValue) && Array.isArray(value) ? containsAll(sourceValue, value) : sourceValue == value\n }\n}\n\n// Function to extract fields from an index name\nconst extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map((field) => field.toLowerCase())\n}\n\nexport class IndexedDbPayloadDiviner<\n TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams,\n TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload,\n TOut extends Payload = Payload,\n TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<\n DivinerInstance<TParams, TIn, TOut>,\n TIn,\n TOut\n >,\n> extends PayloadDiviner<TParams, TIn, TOut, TEventData> {\n static override readonly configSchemas: Schema[] = [...super.configSchemas, IndexedDbPayloadDivinerConfigSchema]\n static override readonly defaultConfigSchema: Schema = IndexedDbPayloadDivinerConfigSchema\n\n private _db: IDBPDatabase<PayloadStore> | undefined\n\n /**\n * The database name. If not supplied via config, it defaults\n * to the archivist's name and if archivist's name is not supplied,\n * it defaults to `archivist`. This behavior\n * biases towards a single, isolated DB per archivist which seems to\n * make the most sense for 99% of use cases.\n */\n get dbName() {\n return this.config?.dbName ?? this.config?.archivist ?? IndexedDbArchivist.defaultDbName\n }\n\n /**\n * The database version. If not supplied via config, it defaults to the archivist default version.\n */\n get dbVersion() {\n return this.config?.dbVersion ?? IndexedDbArchivist.defaultDbVersion\n }\n\n /**\n * The name of the object store. If not supplied via config, it defaults\n * to `payloads`.\n */\n get storeName() {\n return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName\n }\n\n protected override async divineHandler(payloads?: TIn[]): Promise<TOut[]> {\n const query = payloads?.find(isPayloadDivinerQueryPayload) as TIn\n if (!query) return []\n const result = await this.tryUseDb(async (db) => {\n const { schemas, limit, offset, order, ...props } = removeFields(query as unknown as WithMeta<TIn> & { sources?: Hash[] }, [\n 'hash',\n 'schema',\n '$meta',\n '$hash',\n ])\n const tx = db.transaction(this.storeName, 'readonly')\n const store = tx.objectStore(this.storeName)\n const results: TOut[] = []\n let parsedOffset = offset ?? 0\n const parsedLimit = limit ?? 10\n assertEx((schemas?.length ?? 1) === 1, () => 'IndexedDbPayloadDiviner: Only one filter schema supported')\n const filterSchema = schemas?.[0]\n const filter = filterSchema ? { schema: filterSchema, ...props } : { ...props }\n const direction: IDBCursorDirection = order === 'desc' ? 'prev' : 'next'\n const suggestedIndex = this.selectBestIndex(filter, store)\n const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter)\n const valueFilters: ValueFilter[] =\n props ?\n Object.entries(props)\n .map(([key, value]) => payloadValueFilter(key, value))\n .filter(exists)\n : []\n let cursor =\n suggestedIndex ?\n // Conditionally filter on schemas\n await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)\n // Just iterate all records\n : await store.openCursor(suggestedIndex, direction)\n\n // Skip records until the offset is reached\n while (cursor && parsedOffset > 0) {\n cursor = await cursor.advance(parsedOffset)\n parsedOffset = 0 // Reset offset after skipping\n }\n // Collect results up to the limit\n while (cursor && results.length < parsedLimit) {\n const value = cursor.value\n if (value) {\n // If we're filtering on more than just the schema\n if (valueFilters.length > 0) {\n // Ensure all filters pass\n if (valueFilters.every((filter) => filter(value))) {\n // Then save the value\n results.push(value)\n }\n } else {\n // Otherwise just save the value\n results.push(value)\n }\n }\n try {\n cursor = await cursor.continue()\n } catch {\n break\n }\n }\n await tx.done\n // Remove any metadata before returning to the client\n return await Promise.all(results.map((payload) => PayloadBuilder.build(payload)))\n })\n return result ?? []\n }\n\n protected override async startHandler() {\n await super.startHandler()\n return true\n }\n\n private getKeyRangeValue(indexName: string | null, query: AnyObject): unknown | unknown[] {\n if (!indexName) return []\n\n // Extracting the relevant fields from the index name\n const indexFields = extractFields(indexName)\n\n // Collecting the values for these fields from the query object\n const keyRangeValue = indexFields.map((field) => query[field as keyof AnyObject])\n return keyRangeValue.length === 1 ? keyRangeValue[0] : keyRangeValue\n }\n\n private selectBestIndex(query: AnyObject, store: IDBPObjectStore<PayloadStore>): string | null {\n // List of available indexes\n const { indexNames } = store\n\n // Convert query object keys to a set for easier comparison\n const queryKeys = new Set(Object.keys(query).map((key) => key.toLowerCase()))\n\n // Find the best matching index\n let bestMatch: { indexName: string; matchCount: number } = { indexName: '', matchCount: 0 }\n\n for (const indexName of indexNames) {\n const indexFields = extractFields(indexName)\n const matchCount = indexFields.filter((field) => queryKeys.has(field)).length\n if (matchCount > bestMatch.matchCount) {\n bestMatch = { indexName, matchCount }\n }\n }\n return bestMatch.matchCount > 0 ? bestMatch.indexName : null\n }\n\n /**\n * Checks that the desired DB/objectStore exists and is initialized to the correct version\n * @returns The initialized DB or undefined if it does not exist in the desired state\n */\n private async tryGetInitializedDb(): Promise<IDBPDatabase<PayloadStore> | undefined> {\n // Enumerate the DBs\n const dbs = await indexedDB.databases()\n // Check that the DB exists at the desired version\n const dbExists = dbs.some((db) => {\n return db.name === this.dbName && db.version === this.dbVersion\n })\n // If the DB exists at the desired version\n if (dbExists) {\n // If the db does exist, open it\n const db = await openDB<PayloadStore>(this.dbName, this.dbVersion)\n // Check that the desired objectStore exists\n const storeExists = db.objectStoreNames.contains(this.storeName)\n // If the correct db/version/objectStore exists\n if (storeExists) {\n return db\n } else {\n // Otherwise close the db so the process that is going to update the\n // db can open it\n db.close()\n }\n }\n }\n\n /**\n * Executes a callback with the initialized DB and then closes the db\n * @param callback The method to execute with the initialized DB\n * @returns\n */\n private async tryUseDb<T>(callback: (db: IDBPDatabase<PayloadStore>) => Promise<T> | T): Promise<T | undefined> {\n // Get the initialized DB\n const db = await this.tryGetInitializedDb()\n if (db) {\n try {\n // Perform the callback\n return await callback(db)\n } finally {\n // Close the DB\n db.close()\n }\n }\n return undefined\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;ACAA,mCAAqC;AAE9B,IAAMA,gCAAgC,GAAGC,iDAAAA;;;ACGzC,IAAMC,sCAAsC,GAAGC,6BAAAA;;;ACLtD,mBAA4B;AAC5B,oBAAyB;AACzB,oBAAuB;AAEvB,oBAAwC;AACxC,iCAAmC;AACnC,6BAA+B;AAE/B,sCAA+B;AAC/B,IAAAC,gCAAyE;AACzE,6BAA+B;AAE/B,iBAAsD;AAatD,IAAMC,qBAAqB,wBAACC,KAAuBC,UAAAA;AACjD,MAAI,CAACA,MAAO,QAAOC;AACnB,SAAO,CAACC,YAAAA;AACN,QAAI,CAACA,QAAS,QAAO;AACrB,UAAMC,cAAcD,mCAAUH;AAC9B,QAAII,gBAAgBF,OAAW,QAAO;AACtC,WAAOG,MAAMC,QAAQF,WAAAA,KAAgBC,MAAMC,QAAQL,KAAAA,QAASM,0BAAYH,aAAaH,KAAAA,IAASG,eAAeH;EAC/G;AACF,GAR2B;AAW3B,IAAMO,gBAAgB,wBAACC,cAAAA;AACrB,SAAOA,UACJC,MAAM,CAAA,EACNC,MAAMC,qCAAAA,EACNC,IAAI,CAACC,UAAUA,MAAMC,YAAW,CAAA;AACrC,GALsB;AAOf,IAAMC,2BAAN,MAAMA,iCASHC,+CAAAA;EAIAC;;;;;;;;EASR,IAAIC,SAAS;AAjEf;AAkEI,aAAO,UAAKC,WAAL,mBAAaD,aAAU,UAAKC,WAAL,mBAAaC,cAAaC,8CAAmBC;EAC7E;;;;EAKA,IAAIC,YAAY;AAxElB;AAyEI,aAAO,UAAKJ,WAAL,mBAAaI,cAAaF,8CAAmBG;EACtD;;;;;EAMA,IAAIC,YAAY;AAhFlB;AAiFI,aAAO,UAAKN,WAAL,mBAAaM,cAAaJ,8CAAmBK;EACtD;EAEA,MAAyBC,cAAcC,UAAmC;AACxE,UAAMC,QAAQD,qCAAUE,KAAKC;AAC7B,QAAI,CAACF,MAAO,QAAO,CAAA;AACnB,UAAMG,SAAS,MAAM,KAAKC,SAAS,OAAOC,OAAAA;AACxC,YAAM,EAAEC,SAASC,OAAOC,QAAQC,OAAO,GAAGC,MAAAA,QAAUC,4BAAaX,OAA0D;QACzH;QACA;QACA;QACA;OACD;AACD,YAAMY,KAAKP,GAAGQ,YAAY,KAAKjB,WAAW,UAAA;AAC1C,YAAMkB,QAAQF,GAAGG,YAAY,KAAKnB,SAAS;AAC3C,YAAMoB,UAAkB,CAAA;AACxB,UAAIC,eAAeT,UAAU;AAC7B,YAAMU,cAAcX,SAAS;AAC7BY,oCAAUb,mCAASc,WAAU,OAAO,GAAG,MAAM,2DAAA;AAC7C,YAAMC,eAAef,mCAAU;AAC/B,YAAMgB,SAASD,eAAe;QAAEE,QAAQF;QAAc,GAAGX;MAAM,IAAI;QAAE,GAAGA;MAAM;AAC9E,YAAMc,YAAgCf,UAAU,SAAS,SAAS;AAClE,YAAMgB,iBAAiB,KAAKC,gBAAgBJ,QAAQR,KAAAA;AACpD,YAAMa,gBAAgB,KAAKC,iBAAiBH,gBAAgBH,MAAAA;AAC5D,YAAMO,eACJnB,QACEoB,OAAOC,QAAQrB,KAAAA,EACZ3B,IAAI,CAAC,CAACb,KAAKC,KAAAA,MAAWF,mBAAmBC,KAAKC,KAAAA,CAAAA,EAC9CmD,OAAOU,oBAAAA,IACV,CAAA;AACJ,UAAIC,SACFR;;QAEE,MAAMX,MAAMoB,MAAMT,cAAAA,EAAgBU,WAAWC,YAAYC,KAAKV,aAAAA,GAAgBH,SAAAA;UAE9E,MAAMV,MAAMqB,WAAWV,gBAAgBD,SAAAA;AAG3C,aAAOS,UAAUhB,eAAe,GAAG;AACjCgB,iBAAS,MAAMA,OAAOK,QAAQrB,YAAAA;AAC9BA,uBAAe;MACjB;AAEA,aAAOgB,UAAUjB,QAAQI,SAASF,aAAa;AAC7C,cAAM/C,QAAQ8D,OAAO9D;AACrB,YAAIA,OAAO;AAET,cAAI0D,aAAaT,SAAS,GAAG;AAE3B,gBAAIS,aAAaU,MAAM,CAACjB,YAAWA,QAAOnD,KAAAA,CAAAA,GAAS;AAEjD6C,sBAAQwB,KAAKrE,KAAAA;YACf;UACF,OAAO;AAEL6C,oBAAQwB,KAAKrE,KAAAA;UACf;QACF;AACA,YAAI;AACF8D,mBAAS,MAAMA,OAAOQ,SAAQ;QAChC,QAAQ;AACN;QACF;MACF;AACA,YAAM7B,GAAG8B;AAET,aAAO,MAAMC,QAAQC,IAAI5B,QAAQjC,IAAI,CAACV,YAAYwE,sCAAeC,MAAMzE,OAAAA,CAAAA,CAAAA;IACzE,CAAA;AACA,WAAO8B,UAAU,CAAA;EACnB;EAEA,MAAyB4C,eAAe;AACtC,UAAM,MAAMA,aAAAA;AACZ,WAAO;EACT;EAEQnB,iBAAiBjD,WAA0BqB,OAAuC;AACxF,QAAI,CAACrB,UAAW,QAAO,CAAA;AAGvB,UAAMqE,cAActE,cAAcC,SAAAA;AAGlC,UAAMgD,gBAAgBqB,YAAYjE,IAAI,CAACC,UAAUgB,MAAMhB,KAAAA,CAAyB;AAChF,WAAO2C,cAAcP,WAAW,IAAIO,cAAc,CAAA,IAAKA;EACzD;EAEQD,gBAAgB1B,OAAkBc,OAAqD;AAE7F,UAAM,EAAEmC,WAAU,IAAKnC;AAGvB,UAAMoC,YAAY,IAAIC,IAAIrB,OAAOsB,KAAKpD,KAAAA,EAAOjB,IAAI,CAACb,QAAQA,IAAIe,YAAW,CAAA,CAAA;AAGzE,QAAIoE,YAAuD;MAAE1E,WAAW;MAAI2E,YAAY;IAAE;AAE1F,eAAW3E,aAAasE,YAAY;AAClC,YAAMD,cAActE,cAAcC,SAAAA;AAClC,YAAM2E,aAAaN,YAAY1B,OAAO,CAACtC,UAAUkE,UAAUK,IAAIvE,KAAAA,CAAAA,EAAQoC;AACvE,UAAIkC,aAAaD,UAAUC,YAAY;AACrCD,oBAAY;UAAE1E;UAAW2E;QAAW;MACtC;IACF;AACA,WAAOD,UAAUC,aAAa,IAAID,UAAU1E,YAAY;EAC1D;;;;;EAMA,MAAc6E,sBAAuE;AAEnF,UAAMC,MAAM,MAAMC,UAAUC,UAAS;AAErC,UAAMC,WAAWH,IAAII,KAAK,CAACxD,OAAAA;AACzB,aAAOA,GAAGyD,SAAS,KAAKzE,UAAUgB,GAAG0D,YAAY,KAAKrE;IACxD,CAAA;AAEA,QAAIkE,UAAU;AAEZ,YAAMvD,KAAK,UAAM2D,mBAAqB,KAAK3E,QAAQ,KAAKK,SAAS;AAEjE,YAAMuE,cAAc5D,GAAG6D,iBAAiBC,SAAS,KAAKvE,SAAS;AAE/D,UAAIqE,aAAa;AACf,eAAO5D;MACT,OAAO;AAGLA,WAAG+D,MAAK;MACV;IACF;EACF;;;;;;EAOA,MAAchE,SAAYiE,UAAsF;AAE9G,UAAMhE,KAAK,MAAM,KAAKmD,oBAAmB;AACzC,QAAInD,IAAI;AACN,UAAI;AAEF,eAAO,MAAMgE,SAAShE,EAAAA;MACxB,UAAA;AAEEA,WAAG+D,MAAK;MACV;IACF;AACA,WAAOhG;EACT;AACF;AAvLUe;AACR,cAVWD,0BAUcoF,iBAA0B;KAAI,+DAAMA;EAAeC;;AAC5E,cAXWrF,0BAWcsF,uBAA8BD;AAXlD,IAAMrF,0BAAN;","names":["IndexedDbPayloadDivinerSchema","PayloadDivinerSchema","IndexedDbPayloadDivinerConfigSchema","IndexedDbPayloadDivinerSchema","import_diviner_payload_model","payloadValueFilter","key","value","undefined","payload","sourceValue","Array","isArray","containsAll","extractFields","indexName","slice","split","IndexSeparator","map","field","toLowerCase","IndexedDbPayloadDiviner","PayloadDiviner","_db","dbName","config","archivist","IndexedDbArchivist","defaultDbName","dbVersion","defaultDbVersion","storeName","defaultStoreName","divineHandler","payloads","query","find","isPayloadDivinerQueryPayload","result","tryUseDb","db","schemas","limit","offset","order","props","removeFields","tx","transaction","store","objectStore","results","parsedOffset","parsedLimit","assertEx","length","filterSchema","filter","schema","direction","suggestedIndex","selectBestIndex","keyRangeValue","getKeyRangeValue","valueFilters","Object","entries","exists","cursor","index","openCursor","IDBKeyRange","only","advance","every","push","continue","done","Promise","all","PayloadBuilder","build","startHandler","indexFields","indexNames","queryKeys","Set","keys","bestMatch","matchCount","has","tryGetInitializedDb","dbs","indexedDB","databases","dbExists","some","name","version","openDB","storeExists","objectStoreNames","contains","close","callback","configSchemas","IndexedDbPayloadDivinerConfigSchema","defaultConfigSchema"]}
1
+ {"version":3,"sources":["../../src/index.ts","../../src/Schema.ts","../../src/Config.ts","../../src/Diviner.ts"],"sourcesContent":["export * from './Config.ts'\nexport * from './Diviner.ts'\nexport * from './Params.ts'\nexport * from './Schema.ts'\n","import { PayloadDivinerSchema } from '@xyo-network/diviner-payload-model'\n\nexport const IndexedDbPayloadDivinerSchema = `${PayloadDivinerSchema}.indexeddb` as const\nexport type IndexedDbPayloadDivinerSchema = typeof IndexedDbPayloadDivinerSchema\n","import { IndexDescription } from '@xyo-network/archivist-model'\nimport { DivinerConfig } from '@xyo-network/diviner-model'\n\nimport { IndexedDbPayloadDivinerSchema } from './Schema.ts'\n\nexport const IndexedDbPayloadDivinerConfigSchema = `${IndexedDbPayloadDivinerSchema}.config` as const\nexport type IndexedDbPayloadDivinerConfigSchema = typeof IndexedDbPayloadDivinerConfigSchema\n\nexport type IndexedDbPayloadDivinerConfig = DivinerConfig<{\n /**\n * The database name\n */\n dbName?: string\n /**\n * The version of the DB, defaults to 1\n */\n dbVersion?: number\n schema: IndexedDbPayloadDivinerConfigSchema\n /**\n * The storage configuration\n * // TODO: Hoist to main diviner config\n */\n storage?: {\n /**\n * The indexes to create on the object store\n */\n indexes?: IndexDescription[]\n }\n /**\n * The name of the object store\n */\n storeName?: string\n}>\n","import { containsAll } from '@xylabs/array'\nimport { assertEx } from '@xylabs/assert'\nimport { exists } from '@xylabs/exists'\nimport { Hash } from '@xylabs/hex'\nimport { AnyObject, removeFields } from '@xylabs/object'\nimport { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'\nimport { IndexSeparator } from '@xyo-network/archivist-model'\nimport { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'\nimport { isPayloadDivinerQueryPayload, PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport { PayloadBuilder } from '@xyo-network/payload-builder'\nimport { Payload, Schema, WithMeta } from '@xyo-network/payload-model'\nimport { IDBPDatabase, IDBPObjectStore, openDB } from 'idb'\n\nimport { IndexedDbPayloadDivinerConfigSchema } from './Config.ts'\nimport { IndexedDbPayloadDivinerParams } from './Params.ts'\n\ninterface PayloadStore {\n [s: string]: Payload\n}\n\ntype AnyPayload = Payload<Record<string, unknown>>\n\ntype ValueFilter = (payload?: AnyPayload | null) => boolean\n\nconst payloadValueFilter = (key: keyof AnyPayload, value?: unknown | unknown[]): ValueFilter | undefined => {\n if (!value) return undefined\n return (payload) => {\n if (!payload) return false\n const sourceValue = payload?.[key]\n if (sourceValue === undefined) return false\n return Array.isArray(sourceValue) && Array.isArray(value) ? containsAll(sourceValue, value) : sourceValue == value\n }\n}\n\n// Function to extract fields from an index name\nconst extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map(field => field.toLowerCase())\n}\n\nexport class IndexedDbPayloadDiviner<\n TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams,\n TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload,\n TOut extends Payload = Payload,\n TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<\n DivinerInstance<TParams, TIn, TOut>,\n TIn,\n TOut\n >,\n> extends PayloadDiviner<TParams, TIn, TOut, TEventData> {\n static override readonly configSchemas: Schema[] = [...super.configSchemas, IndexedDbPayloadDivinerConfigSchema]\n static override readonly defaultConfigSchema: Schema = IndexedDbPayloadDivinerConfigSchema\n\n private _db: IDBPDatabase<PayloadStore> | undefined\n\n /**\n * The database name. If not supplied via config, it defaults\n * to the archivist's name and if archivist's name is not supplied,\n * it defaults to `archivist`. This behavior\n * biases towards a single, isolated DB per archivist which seems to\n * make the most sense for 99% of use cases.\n */\n get dbName() {\n return this.config?.dbName ?? this.config?.archivist ?? IndexedDbArchivist.defaultDbName\n }\n\n /**\n * The database version. If not supplied via config, it defaults to the archivist default version.\n */\n get dbVersion() {\n return this.config?.dbVersion ?? IndexedDbArchivist.defaultDbVersion\n }\n\n /**\n * The name of the object store. If not supplied via config, it defaults\n * to `payloads`.\n */\n get storeName() {\n return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName\n }\n\n protected override async divineHandler(payloads?: TIn[]): Promise<TOut[]> {\n const query = payloads?.find(isPayloadDivinerQueryPayload) as TIn\n if (!query) return []\n const result = await this.tryUseDb(async (db) => {\n const { schemas, limit, offset, order, ...props } = removeFields(query as unknown as WithMeta<TIn> & { sources?: Hash[] }, [\n 'hash',\n 'schema',\n '$meta',\n '$hash',\n ])\n const tx = db.transaction(this.storeName, 'readonly')\n const store = tx.objectStore(this.storeName)\n const results: TOut[] = []\n let parsedOffset = offset ?? 0\n const parsedLimit = limit ?? 10\n assertEx((schemas?.length ?? 1) === 1, () => 'IndexedDbPayloadDiviner: Only one filter schema supported')\n const filterSchema = schemas?.[0]\n const filter = filterSchema ? { schema: filterSchema, ...props } : { ...props }\n const direction: IDBCursorDirection = order === 'desc' ? 'prev' : 'next'\n const suggestedIndex = this.selectBestIndex(filter, store)\n const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter)\n const valueFilters: ValueFilter[]\n = props\n ? Object.entries(props)\n .map(([key, value]) => payloadValueFilter(key, value))\n .filter(exists)\n : []\n let cursor\n = suggestedIndex\n // Conditionally filter on schemas\n ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)\n // Just iterate all records\n : await store.openCursor(suggestedIndex, direction)\n\n // Skip records until the offset is reached\n while (cursor && parsedOffset > 0) {\n cursor = await cursor.advance(parsedOffset)\n parsedOffset = 0 // Reset offset after skipping\n }\n // Collect results up to the limit\n while (cursor && results.length < parsedLimit) {\n const value = cursor.value\n if (value) {\n // If we're filtering on more than just the schema\n if (valueFilters.length > 0) {\n // Ensure all filters pass\n if (valueFilters.every(filter => filter(value))) {\n // Then save the value\n results.push(value)\n }\n } else {\n // Otherwise just save the value\n results.push(value)\n }\n }\n try {\n cursor = await cursor.continue()\n } catch {\n break\n }\n }\n await tx.done\n // Remove any metadata before returning to the client\n return await Promise.all(results.map(payload => PayloadBuilder.build(payload)))\n })\n return result ?? []\n }\n\n protected override async startHandler() {\n await super.startHandler()\n return true\n }\n\n private getKeyRangeValue(indexName: string | null, query: AnyObject): unknown | unknown[] {\n if (!indexName) return []\n\n // Extracting the relevant fields from the index name\n const indexFields = extractFields(indexName)\n\n // Collecting the values for these fields from the query object\n const keyRangeValue = indexFields.map(field => query[field as keyof AnyObject])\n return keyRangeValue.length === 1 ? keyRangeValue[0] : keyRangeValue\n }\n\n private selectBestIndex(query: AnyObject, store: IDBPObjectStore<PayloadStore>): string | null {\n // List of available indexes\n const { indexNames } = store\n\n // Convert query object keys to a set for easier comparison\n const queryKeys = new Set(Object.keys(query).map(key => key.toLowerCase()))\n\n // Find the best matching index\n let bestMatch: { indexName: string; matchCount: number } = { indexName: '', matchCount: 0 }\n\n for (const indexName of indexNames) {\n const indexFields = extractFields(indexName)\n const matchCount = indexFields.filter(field => queryKeys.has(field)).length\n if (matchCount > bestMatch.matchCount) {\n bestMatch = { indexName, matchCount }\n }\n }\n return bestMatch.matchCount > 0 ? bestMatch.indexName : null\n }\n\n /**\n * Checks that the desired DB/objectStore exists and is initialized to the correct version\n * @returns The initialized DB or undefined if it does not exist in the desired state\n */\n private async tryGetInitializedDb(): Promise<IDBPDatabase<PayloadStore> | undefined> {\n // Enumerate the DBs\n const dbs = await indexedDB.databases()\n // Check that the DB exists at the desired version\n const dbExists = dbs.some((db) => {\n return db.name === this.dbName && db.version === this.dbVersion\n })\n // If the DB exists at the desired version\n if (dbExists) {\n // If the db does exist, open it\n const db = await openDB<PayloadStore>(this.dbName, this.dbVersion)\n // Check that the desired objectStore exists\n const storeExists = db.objectStoreNames.contains(this.storeName)\n // If the correct db/version/objectStore exists\n if (storeExists) {\n return db\n } else {\n // Otherwise close the db so the process that is going to update the\n // db can open it\n db.close()\n }\n }\n }\n\n /**\n * Executes a callback with the initialized DB and then closes the db\n * @param callback The method to execute with the initialized DB\n * @returns\n */\n private async tryUseDb<T>(callback: (db: IDBPDatabase<PayloadStore>) => Promise<T> | T): Promise<T | undefined> {\n // Get the initialized DB\n const db = await this.tryGetInitializedDb()\n if (db) {\n try {\n // Perform the callback\n return await callback(db)\n } finally {\n // Close the DB\n db.close()\n }\n }\n return undefined\n }\n}\n"],"mappings":"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","names":["IndexedDbPayloadDivinerSchema","PayloadDivinerSchema","IndexedDbPayloadDivinerConfigSchema","IndexedDbPayloadDivinerSchema","import_diviner_payload_model","payloadValueFilter","key","value","undefined","payload","sourceValue","Array","isArray","containsAll","extractFields","indexName","slice","split","IndexSeparator","map","field","toLowerCase","IndexedDbPayloadDiviner","PayloadDiviner","_db","dbName","config","archivist","IndexedDbArchivist","defaultDbName","dbVersion","defaultDbVersion","storeName","defaultStoreName","divineHandler","payloads","query","find","isPayloadDivinerQueryPayload","result","tryUseDb","db","schemas","limit","offset","order","props","removeFields","tx","transaction","store","objectStore","results","parsedOffset","parsedLimit","assertEx","length","filterSchema","filter","schema","direction","suggestedIndex","selectBestIndex","keyRangeValue","getKeyRangeValue","valueFilters","Object","entries","exists","cursor","index","openCursor","IDBKeyRange","only","advance","every","push","continue","done","Promise","all","PayloadBuilder","build","startHandler","indexFields","indexNames","queryKeys","Set","keys","bestMatch","matchCount","has","tryGetInitializedDb","dbs","indexedDB","databases","dbExists","some","name","version","openDB","storeExists","objectStoreNames","contains","close","callback","configSchemas","IndexedDbPayloadDivinerConfigSchema","defaultConfigSchema"]}
@@ -91,10 +91,7 @@ var _IndexedDbPayloadDiviner = class _IndexedDbPayloadDiviner extends PayloadDiv
91
91
  const suggestedIndex = this.selectBestIndex(filter, store);
92
92
  const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter);
93
93
  const valueFilters = props ? Object.entries(props).map(([key, value]) => payloadValueFilter(key, value)).filter(exists) : [];
94
- let cursor = suggestedIndex ? (
95
- // Conditionally filter on schemas
96
- await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)
97
- ) : await store.openCursor(suggestedIndex, direction);
94
+ let cursor = suggestedIndex ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction) : await store.openCursor(suggestedIndex, direction);
98
95
  while (cursor && parsedOffset > 0) {
99
96
  cursor = await cursor.advance(parsedOffset);
100
97
  parsedOffset = 0;
@@ -198,4 +195,4 @@ export {
198
195
  IndexedDbPayloadDivinerConfigSchema,
199
196
  IndexedDbPayloadDivinerSchema
200
197
  };
201
- //# sourceMappingURL=index.js.map
198
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/Schema.ts","../../src/Config.ts","../../src/Diviner.ts"],"sourcesContent":["import { PayloadDivinerSchema } from '@xyo-network/diviner-payload-model'\n\nexport const IndexedDbPayloadDivinerSchema = `${PayloadDivinerSchema}.indexeddb` as const\nexport type IndexedDbPayloadDivinerSchema = typeof IndexedDbPayloadDivinerSchema\n","import { IndexDescription } from '@xyo-network/archivist-model'\nimport { DivinerConfig } from '@xyo-network/diviner-model'\n\nimport { IndexedDbPayloadDivinerSchema } from './Schema.ts'\n\nexport const IndexedDbPayloadDivinerConfigSchema = `${IndexedDbPayloadDivinerSchema}.config` as const\nexport type IndexedDbPayloadDivinerConfigSchema = typeof IndexedDbPayloadDivinerConfigSchema\n\nexport type IndexedDbPayloadDivinerConfig = DivinerConfig<{\n /**\n * The database name\n */\n dbName?: string\n /**\n * The version of the DB, defaults to 1\n */\n dbVersion?: number\n schema: IndexedDbPayloadDivinerConfigSchema\n /**\n * The storage configuration\n * // TODO: Hoist to main diviner config\n */\n storage?: {\n /**\n * The indexes to create on the object store\n */\n indexes?: IndexDescription[]\n }\n /**\n * The name of the object store\n */\n storeName?: string\n}>\n","import { containsAll } from '@xylabs/array'\nimport { assertEx } from '@xylabs/assert'\nimport { exists } from '@xylabs/exists'\nimport { Hash } from '@xylabs/hex'\nimport { AnyObject, removeFields } from '@xylabs/object'\nimport { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'\nimport { IndexSeparator } from '@xyo-network/archivist-model'\nimport { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'\nimport { isPayloadDivinerQueryPayload, PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport { PayloadBuilder } from '@xyo-network/payload-builder'\nimport { Payload, Schema, WithMeta } from '@xyo-network/payload-model'\nimport { IDBPDatabase, IDBPObjectStore, openDB } from 'idb'\n\nimport { IndexedDbPayloadDivinerConfigSchema } from './Config.ts'\nimport { IndexedDbPayloadDivinerParams } from './Params.ts'\n\ninterface PayloadStore {\n [s: string]: Payload\n}\n\ntype AnyPayload = Payload<Record<string, unknown>>\n\ntype ValueFilter = (payload?: AnyPayload | null) => boolean\n\nconst payloadValueFilter = (key: keyof AnyPayload, value?: unknown | unknown[]): ValueFilter | undefined => {\n if (!value) return undefined\n return (payload) => {\n if (!payload) return false\n const sourceValue = payload?.[key]\n if (sourceValue === undefined) return false\n return Array.isArray(sourceValue) && Array.isArray(value) ? containsAll(sourceValue, value) : sourceValue == value\n }\n}\n\n// Function to extract fields from an index name\nconst extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map(field => field.toLowerCase())\n}\n\nexport class IndexedDbPayloadDiviner<\n TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams,\n TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload,\n TOut extends Payload = Payload,\n TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<\n DivinerInstance<TParams, TIn, TOut>,\n TIn,\n TOut\n >,\n> extends PayloadDiviner<TParams, TIn, TOut, TEventData> {\n static override readonly configSchemas: Schema[] = [...super.configSchemas, IndexedDbPayloadDivinerConfigSchema]\n static override readonly defaultConfigSchema: Schema = IndexedDbPayloadDivinerConfigSchema\n\n private _db: IDBPDatabase<PayloadStore> | undefined\n\n /**\n * The database name. If not supplied via config, it defaults\n * to the archivist's name and if archivist's name is not supplied,\n * it defaults to `archivist`. This behavior\n * biases towards a single, isolated DB per archivist which seems to\n * make the most sense for 99% of use cases.\n */\n get dbName() {\n return this.config?.dbName ?? this.config?.archivist ?? IndexedDbArchivist.defaultDbName\n }\n\n /**\n * The database version. If not supplied via config, it defaults to the archivist default version.\n */\n get dbVersion() {\n return this.config?.dbVersion ?? IndexedDbArchivist.defaultDbVersion\n }\n\n /**\n * The name of the object store. If not supplied via config, it defaults\n * to `payloads`.\n */\n get storeName() {\n return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName\n }\n\n protected override async divineHandler(payloads?: TIn[]): Promise<TOut[]> {\n const query = payloads?.find(isPayloadDivinerQueryPayload) as TIn\n if (!query) return []\n const result = await this.tryUseDb(async (db) => {\n const { schemas, limit, offset, order, ...props } = removeFields(query as unknown as WithMeta<TIn> & { sources?: Hash[] }, [\n 'hash',\n 'schema',\n '$meta',\n '$hash',\n ])\n const tx = db.transaction(this.storeName, 'readonly')\n const store = tx.objectStore(this.storeName)\n const results: TOut[] = []\n let parsedOffset = offset ?? 0\n const parsedLimit = limit ?? 10\n assertEx((schemas?.length ?? 1) === 1, () => 'IndexedDbPayloadDiviner: Only one filter schema supported')\n const filterSchema = schemas?.[0]\n const filter = filterSchema ? { schema: filterSchema, ...props } : { ...props }\n const direction: IDBCursorDirection = order === 'desc' ? 'prev' : 'next'\n const suggestedIndex = this.selectBestIndex(filter, store)\n const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter)\n const valueFilters: ValueFilter[]\n = props\n ? Object.entries(props)\n .map(([key, value]) => payloadValueFilter(key, value))\n .filter(exists)\n : []\n let cursor\n = suggestedIndex\n // Conditionally filter on schemas\n ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)\n // Just iterate all records\n : await store.openCursor(suggestedIndex, direction)\n\n // Skip records until the offset is reached\n while (cursor && parsedOffset > 0) {\n cursor = await cursor.advance(parsedOffset)\n parsedOffset = 0 // Reset offset after skipping\n }\n // Collect results up to the limit\n while (cursor && results.length < parsedLimit) {\n const value = cursor.value\n if (value) {\n // If we're filtering on more than just the schema\n if (valueFilters.length > 0) {\n // Ensure all filters pass\n if (valueFilters.every(filter => filter(value))) {\n // Then save the value\n results.push(value)\n }\n } else {\n // Otherwise just save the value\n results.push(value)\n }\n }\n try {\n cursor = await cursor.continue()\n } catch {\n break\n }\n }\n await tx.done\n // Remove any metadata before returning to the client\n return await Promise.all(results.map(payload => PayloadBuilder.build(payload)))\n })\n return result ?? []\n }\n\n protected override async startHandler() {\n await super.startHandler()\n return true\n }\n\n private getKeyRangeValue(indexName: string | null, query: AnyObject): unknown | unknown[] {\n if (!indexName) return []\n\n // Extracting the relevant fields from the index name\n const indexFields = extractFields(indexName)\n\n // Collecting the values for these fields from the query object\n const keyRangeValue = indexFields.map(field => query[field as keyof AnyObject])\n return keyRangeValue.length === 1 ? keyRangeValue[0] : keyRangeValue\n }\n\n private selectBestIndex(query: AnyObject, store: IDBPObjectStore<PayloadStore>): string | null {\n // List of available indexes\n const { indexNames } = store\n\n // Convert query object keys to a set for easier comparison\n const queryKeys = new Set(Object.keys(query).map(key => key.toLowerCase()))\n\n // Find the best matching index\n let bestMatch: { indexName: string; matchCount: number } = { indexName: '', matchCount: 0 }\n\n for (const indexName of indexNames) {\n const indexFields = extractFields(indexName)\n const matchCount = indexFields.filter(field => queryKeys.has(field)).length\n if (matchCount > bestMatch.matchCount) {\n bestMatch = { indexName, matchCount }\n }\n }\n return bestMatch.matchCount > 0 ? bestMatch.indexName : null\n }\n\n /**\n * Checks that the desired DB/objectStore exists and is initialized to the correct version\n * @returns The initialized DB or undefined if it does not exist in the desired state\n */\n private async tryGetInitializedDb(): Promise<IDBPDatabase<PayloadStore> | undefined> {\n // Enumerate the DBs\n const dbs = await indexedDB.databases()\n // Check that the DB exists at the desired version\n const dbExists = dbs.some((db) => {\n return db.name === this.dbName && db.version === this.dbVersion\n })\n // If the DB exists at the desired version\n if (dbExists) {\n // If the db does exist, open it\n const db = await openDB<PayloadStore>(this.dbName, this.dbVersion)\n // Check that the desired objectStore exists\n const storeExists = db.objectStoreNames.contains(this.storeName)\n // If the correct db/version/objectStore exists\n if (storeExists) {\n return db\n } else {\n // Otherwise close the db so the process that is going to update the\n // db can open it\n db.close()\n }\n }\n }\n\n /**\n * Executes a callback with the initialized DB and then closes the db\n * @param callback The method to execute with the initialized DB\n * @returns\n */\n private async tryUseDb<T>(callback: (db: IDBPDatabase<PayloadStore>) => Promise<T> | T): Promise<T | undefined> {\n // Get the initialized DB\n const db = await this.tryGetInitializedDb()\n if (db) {\n try {\n // Perform the callback\n return await callback(db)\n } finally {\n // Close the DB\n db.close()\n }\n }\n return undefined\n }\n}\n"],"mappings":"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","names":["PayloadDivinerSchema","IndexedDbPayloadDivinerSchema","IndexedDbPayloadDivinerConfigSchema","IndexedDbPayloadDivinerSchema","containsAll","assertEx","exists","removeFields","IndexedDbArchivist","IndexSeparator","PayloadDiviner","isPayloadDivinerQueryPayload","PayloadBuilder","openDB","payloadValueFilter","key","value","undefined","payload","sourceValue","Array","isArray","containsAll","extractFields","indexName","slice","split","IndexSeparator","map","field","toLowerCase","IndexedDbPayloadDiviner","PayloadDiviner","_db","dbName","config","archivist","IndexedDbArchivist","defaultDbName","dbVersion","defaultDbVersion","storeName","defaultStoreName","divineHandler","payloads","query","find","isPayloadDivinerQueryPayload","result","tryUseDb","db","schemas","limit","offset","order","props","removeFields","tx","transaction","store","objectStore","results","parsedOffset","parsedLimit","assertEx","length","filterSchema","filter","schema","direction","suggestedIndex","selectBestIndex","keyRangeValue","getKeyRangeValue","valueFilters","Object","entries","exists","cursor","index","openCursor","IDBKeyRange","only","advance","every","push","continue","done","Promise","all","PayloadBuilder","build","startHandler","indexFields","indexNames","queryKeys","Set","keys","bestMatch","matchCount","has","tryGetInitializedDb","dbs","indexedDB","databases","dbExists","some","name","version","openDB","storeExists","objectStoreNames","contains","close","callback","configSchemas","IndexedDbPayloadDivinerConfigSchema","defaultConfigSchema"]}
package/package.json CHANGED
@@ -10,26 +10,26 @@
10
10
  "url": "https://github.com/XYOracleNetwork/sdk-xyo-client-js/issues"
11
11
  },
12
12
  "dependencies": {
13
- "@xylabs/array": "^3.6.6",
14
- "@xylabs/assert": "^3.6.6",
15
- "@xylabs/exists": "^3.6.6",
16
- "@xylabs/hex": "^3.6.6",
17
- "@xylabs/object": "^3.6.6",
18
- "@xyo-network/archivist-indexeddb": "^2.110.19",
19
- "@xyo-network/archivist-model": "^2.110.19",
20
- "@xyo-network/diviner-model": "^2.110.19",
21
- "@xyo-network/diviner-payload-abstract": "^2.110.19",
22
- "@xyo-network/diviner-payload-model": "^2.110.19",
23
- "@xyo-network/module-model": "^2.110.19",
24
- "@xyo-network/payload-builder": "^2.110.19",
25
- "@xyo-network/payload-model": "^2.110.19",
13
+ "@xylabs/array": "^3.6.8",
14
+ "@xylabs/assert": "^3.6.8",
15
+ "@xylabs/exists": "^3.6.8",
16
+ "@xylabs/hex": "^3.6.8",
17
+ "@xylabs/object": "^3.6.8",
18
+ "@xyo-network/archivist-indexeddb": "^2.111.1",
19
+ "@xyo-network/archivist-model": "^2.111.1",
20
+ "@xyo-network/diviner-model": "^2.111.1",
21
+ "@xyo-network/diviner-payload-abstract": "^2.111.1",
22
+ "@xyo-network/diviner-payload-model": "^2.111.1",
23
+ "@xyo-network/module-model": "^2.111.1",
24
+ "@xyo-network/payload-builder": "^2.111.1",
25
+ "@xyo-network/payload-model": "^2.111.1",
26
26
  "idb": "^8.0.0"
27
27
  },
28
28
  "devDependencies": {
29
- "@xylabs/ts-scripts-yarn3": "^3.14.1",
30
- "@xylabs/tsconfig": "^3.14.1",
31
- "@xyo-network/archivist-indexeddb": "^2.110.19",
32
- "@xyo-network/node-memory": "^2.110.19",
29
+ "@xylabs/ts-scripts-yarn3": "^3.15.8",
30
+ "@xylabs/tsconfig": "^3.15.8",
31
+ "@xyo-network/archivist-indexeddb": "^2.111.1",
32
+ "@xyo-network/node-memory": "^2.111.1",
33
33
  "fake-indexeddb": "^6.0.0",
34
34
  "typescript": "^5.5.4"
35
35
  },
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "import": {
46
46
  "types": "./dist/browser/index.d.mts",
47
- "default": "./dist/browser/index.js"
47
+ "default": "./dist/browser/index.mjs"
48
48
  }
49
49
  },
50
50
  "node": {
@@ -54,14 +54,14 @@
54
54
  },
55
55
  "import": {
56
56
  "types": "./dist/node/index.d.mts",
57
- "default": "./dist/node/index.js"
57
+ "default": "./dist/node/index.mjs"
58
58
  }
59
59
  }
60
60
  },
61
61
  "./package.json": "./package.json"
62
62
  },
63
63
  "main": "dist/node/index.cjs",
64
- "module": "dist/node/index.js",
64
+ "module": "dist/node/index.mjs",
65
65
  "homepage": "https://xyo.network",
66
66
  "license": "LGPL-3.0-only",
67
67
  "publishConfig": {
@@ -72,6 +72,6 @@
72
72
  "url": "https://github.com/XYOracleNetwork/sdk-xyo-client-js.git"
73
73
  },
74
74
  "sideEffects": false,
75
- "version": "2.110.19",
75
+ "version": "2.111.1",
76
76
  "type": "module"
77
77
  }
package/src/Diviner.ts CHANGED
@@ -38,7 +38,7 @@ const extractFields = (indexName: string): string[] => {
38
38
  return indexName
39
39
  .slice(3)
40
40
  .split(IndexSeparator)
41
- .map((field) => field.toLowerCase())
41
+ .map(field => field.toLowerCase())
42
42
  }
43
43
 
44
44
  export class IndexedDbPayloadDiviner<
@@ -103,18 +103,18 @@ export class IndexedDbPayloadDiviner<
103
103
  const direction: IDBCursorDirection = order === 'desc' ? 'prev' : 'next'
104
104
  const suggestedIndex = this.selectBestIndex(filter, store)
105
105
  const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter)
106
- const valueFilters: ValueFilter[] =
107
- props ?
108
- Object.entries(props)
106
+ const valueFilters: ValueFilter[]
107
+ = props
108
+ ? Object.entries(props)
109
109
  .map(([key, value]) => payloadValueFilter(key, value))
110
110
  .filter(exists)
111
- : []
112
- let cursor =
113
- suggestedIndex ?
111
+ : []
112
+ let cursor
113
+ = suggestedIndex
114
114
  // Conditionally filter on schemas
115
- await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)
115
+ ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)
116
116
  // Just iterate all records
117
- : await store.openCursor(suggestedIndex, direction)
117
+ : await store.openCursor(suggestedIndex, direction)
118
118
 
119
119
  // Skip records until the offset is reached
120
120
  while (cursor && parsedOffset > 0) {
@@ -128,7 +128,7 @@ export class IndexedDbPayloadDiviner<
128
128
  // If we're filtering on more than just the schema
129
129
  if (valueFilters.length > 0) {
130
130
  // Ensure all filters pass
131
- if (valueFilters.every((filter) => filter(value))) {
131
+ if (valueFilters.every(filter => filter(value))) {
132
132
  // Then save the value
133
133
  results.push(value)
134
134
  }
@@ -145,7 +145,7 @@ export class IndexedDbPayloadDiviner<
145
145
  }
146
146
  await tx.done
147
147
  // Remove any metadata before returning to the client
148
- return await Promise.all(results.map((payload) => PayloadBuilder.build(payload)))
148
+ return await Promise.all(results.map(payload => PayloadBuilder.build(payload)))
149
149
  })
150
150
  return result ?? []
151
151
  }
@@ -162,7 +162,7 @@ export class IndexedDbPayloadDiviner<
162
162
  const indexFields = extractFields(indexName)
163
163
 
164
164
  // Collecting the values for these fields from the query object
165
- const keyRangeValue = indexFields.map((field) => query[field as keyof AnyObject])
165
+ const keyRangeValue = indexFields.map(field => query[field as keyof AnyObject])
166
166
  return keyRangeValue.length === 1 ? keyRangeValue[0] : keyRangeValue
167
167
  }
168
168
 
@@ -171,14 +171,14 @@ export class IndexedDbPayloadDiviner<
171
171
  const { indexNames } = store
172
172
 
173
173
  // Convert query object keys to a set for easier comparison
174
- const queryKeys = new Set(Object.keys(query).map((key) => key.toLowerCase()))
174
+ const queryKeys = new Set(Object.keys(query).map(key => key.toLowerCase()))
175
175
 
176
176
  // Find the best matching index
177
177
  let bestMatch: { indexName: string; matchCount: number } = { indexName: '', matchCount: 0 }
178
178
 
179
179
  for (const indexName of indexNames) {
180
180
  const indexFields = extractFields(indexName)
181
- const matchCount = indexFields.filter((field) => queryKeys.has(field)).length
181
+ const matchCount = indexFields.filter(field => queryKeys.has(field)).length
182
182
  if (matchCount > bestMatch.matchCount) {
183
183
  bestMatch = { indexName, matchCount }
184
184
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/Schema.ts","../../src/Config.ts","../../src/Diviner.ts"],"sourcesContent":["import { PayloadDivinerSchema } from '@xyo-network/diviner-payload-model'\n\nexport const IndexedDbPayloadDivinerSchema = `${PayloadDivinerSchema}.indexeddb` as const\nexport type IndexedDbPayloadDivinerSchema = typeof IndexedDbPayloadDivinerSchema\n","import { IndexDescription } from '@xyo-network/archivist-model'\nimport { DivinerConfig } from '@xyo-network/diviner-model'\n\nimport { IndexedDbPayloadDivinerSchema } from './Schema.ts'\n\nexport const IndexedDbPayloadDivinerConfigSchema = `${IndexedDbPayloadDivinerSchema}.config` as const\nexport type IndexedDbPayloadDivinerConfigSchema = typeof IndexedDbPayloadDivinerConfigSchema\n\nexport type IndexedDbPayloadDivinerConfig = DivinerConfig<{\n /**\n * The database name\n */\n dbName?: string\n /**\n * The version of the DB, defaults to 1\n */\n dbVersion?: number\n schema: IndexedDbPayloadDivinerConfigSchema\n /**\n * The storage configuration\n * // TODO: Hoist to main diviner config\n */\n storage?: {\n /**\n * The indexes to create on the object store\n */\n indexes?: IndexDescription[]\n }\n /**\n * The name of the object store\n */\n storeName?: string\n}>\n","import { containsAll } from '@xylabs/array'\nimport { assertEx } from '@xylabs/assert'\nimport { exists } from '@xylabs/exists'\nimport { Hash } from '@xylabs/hex'\nimport { AnyObject, removeFields } from '@xylabs/object'\nimport { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'\nimport { IndexSeparator } from '@xyo-network/archivist-model'\nimport { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'\nimport { isPayloadDivinerQueryPayload, PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport { PayloadBuilder } from '@xyo-network/payload-builder'\nimport { Payload, Schema, WithMeta } from '@xyo-network/payload-model'\nimport { IDBPDatabase, IDBPObjectStore, openDB } from 'idb'\n\nimport { IndexedDbPayloadDivinerConfigSchema } from './Config.ts'\nimport { IndexedDbPayloadDivinerParams } from './Params.ts'\n\ninterface PayloadStore {\n [s: string]: Payload\n}\n\ntype AnyPayload = Payload<Record<string, unknown>>\n\ntype ValueFilter = (payload?: AnyPayload | null) => boolean\n\nconst payloadValueFilter = (key: keyof AnyPayload, value?: unknown | unknown[]): ValueFilter | undefined => {\n if (!value) return undefined\n return (payload) => {\n if (!payload) return false\n const sourceValue = payload?.[key]\n if (sourceValue === undefined) return false\n return Array.isArray(sourceValue) && Array.isArray(value) ? containsAll(sourceValue, value) : sourceValue == value\n }\n}\n\n// Function to extract fields from an index name\nconst extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map((field) => field.toLowerCase())\n}\n\nexport class IndexedDbPayloadDiviner<\n TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams,\n TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload,\n TOut extends Payload = Payload,\n TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<\n DivinerInstance<TParams, TIn, TOut>,\n TIn,\n TOut\n >,\n> extends PayloadDiviner<TParams, TIn, TOut, TEventData> {\n static override readonly configSchemas: Schema[] = [...super.configSchemas, IndexedDbPayloadDivinerConfigSchema]\n static override readonly defaultConfigSchema: Schema = IndexedDbPayloadDivinerConfigSchema\n\n private _db: IDBPDatabase<PayloadStore> | undefined\n\n /**\n * The database name. If not supplied via config, it defaults\n * to the archivist's name and if archivist's name is not supplied,\n * it defaults to `archivist`. This behavior\n * biases towards a single, isolated DB per archivist which seems to\n * make the most sense for 99% of use cases.\n */\n get dbName() {\n return this.config?.dbName ?? this.config?.archivist ?? IndexedDbArchivist.defaultDbName\n }\n\n /**\n * The database version. If not supplied via config, it defaults to the archivist default version.\n */\n get dbVersion() {\n return this.config?.dbVersion ?? IndexedDbArchivist.defaultDbVersion\n }\n\n /**\n * The name of the object store. If not supplied via config, it defaults\n * to `payloads`.\n */\n get storeName() {\n return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName\n }\n\n protected override async divineHandler(payloads?: TIn[]): Promise<TOut[]> {\n const query = payloads?.find(isPayloadDivinerQueryPayload) as TIn\n if (!query) return []\n const result = await this.tryUseDb(async (db) => {\n const { schemas, limit, offset, order, ...props } = removeFields(query as unknown as WithMeta<TIn> & { sources?: Hash[] }, [\n 'hash',\n 'schema',\n '$meta',\n '$hash',\n ])\n const tx = db.transaction(this.storeName, 'readonly')\n const store = tx.objectStore(this.storeName)\n const results: TOut[] = []\n let parsedOffset = offset ?? 0\n const parsedLimit = limit ?? 10\n assertEx((schemas?.length ?? 1) === 1, () => 'IndexedDbPayloadDiviner: Only one filter schema supported')\n const filterSchema = schemas?.[0]\n const filter = filterSchema ? { schema: filterSchema, ...props } : { ...props }\n const direction: IDBCursorDirection = order === 'desc' ? 'prev' : 'next'\n const suggestedIndex = this.selectBestIndex(filter, store)\n const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter)\n const valueFilters: ValueFilter[] =\n props ?\n Object.entries(props)\n .map(([key, value]) => payloadValueFilter(key, value))\n .filter(exists)\n : []\n let cursor =\n suggestedIndex ?\n // Conditionally filter on schemas\n await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)\n // Just iterate all records\n : await store.openCursor(suggestedIndex, direction)\n\n // Skip records until the offset is reached\n while (cursor && parsedOffset > 0) {\n cursor = await cursor.advance(parsedOffset)\n parsedOffset = 0 // Reset offset after skipping\n }\n // Collect results up to the limit\n while (cursor && results.length < parsedLimit) {\n const value = cursor.value\n if (value) {\n // If we're filtering on more than just the schema\n if (valueFilters.length > 0) {\n // Ensure all filters pass\n if (valueFilters.every((filter) => filter(value))) {\n // Then save the value\n results.push(value)\n }\n } else {\n // Otherwise just save the value\n results.push(value)\n }\n }\n try {\n cursor = await cursor.continue()\n } catch {\n break\n }\n }\n await tx.done\n // Remove any metadata before returning to the client\n return await Promise.all(results.map((payload) => PayloadBuilder.build(payload)))\n })\n return result ?? []\n }\n\n protected override async startHandler() {\n await super.startHandler()\n return true\n }\n\n private getKeyRangeValue(indexName: string | null, query: AnyObject): unknown | unknown[] {\n if (!indexName) return []\n\n // Extracting the relevant fields from the index name\n const indexFields = extractFields(indexName)\n\n // Collecting the values for these fields from the query object\n const keyRangeValue = indexFields.map((field) => query[field as keyof AnyObject])\n return keyRangeValue.length === 1 ? keyRangeValue[0] : keyRangeValue\n }\n\n private selectBestIndex(query: AnyObject, store: IDBPObjectStore<PayloadStore>): string | null {\n // List of available indexes\n const { indexNames } = store\n\n // Convert query object keys to a set for easier comparison\n const queryKeys = new Set(Object.keys(query).map((key) => key.toLowerCase()))\n\n // Find the best matching index\n let bestMatch: { indexName: string; matchCount: number } = { indexName: '', matchCount: 0 }\n\n for (const indexName of indexNames) {\n const indexFields = extractFields(indexName)\n const matchCount = indexFields.filter((field) => queryKeys.has(field)).length\n if (matchCount > bestMatch.matchCount) {\n bestMatch = { indexName, matchCount }\n }\n }\n return bestMatch.matchCount > 0 ? bestMatch.indexName : null\n }\n\n /**\n * Checks that the desired DB/objectStore exists and is initialized to the correct version\n * @returns The initialized DB or undefined if it does not exist in the desired state\n */\n private async tryGetInitializedDb(): Promise<IDBPDatabase<PayloadStore> | undefined> {\n // Enumerate the DBs\n const dbs = await indexedDB.databases()\n // Check that the DB exists at the desired version\n const dbExists = dbs.some((db) => {\n return db.name === this.dbName && db.version === this.dbVersion\n })\n // If the DB exists at the desired version\n if (dbExists) {\n // If the db does exist, open it\n const db = await openDB<PayloadStore>(this.dbName, this.dbVersion)\n // Check that the desired objectStore exists\n const storeExists = db.objectStoreNames.contains(this.storeName)\n // If the correct db/version/objectStore exists\n if (storeExists) {\n return db\n } else {\n // Otherwise close the db so the process that is going to update the\n // db can open it\n db.close()\n }\n }\n }\n\n /**\n * Executes a callback with the initialized DB and then closes the db\n * @param callback The method to execute with the initialized DB\n * @returns\n */\n private async tryUseDb<T>(callback: (db: IDBPDatabase<PayloadStore>) => Promise<T> | T): Promise<T | undefined> {\n // Get the initialized DB\n const db = await this.tryGetInitializedDb()\n if (db) {\n try {\n // Perform the callback\n return await callback(db)\n } finally {\n // Close the DB\n db.close()\n }\n }\n return undefined\n }\n}\n"],"mappings":"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","names":["PayloadDivinerSchema","IndexedDbPayloadDivinerSchema","IndexedDbPayloadDivinerConfigSchema","IndexedDbPayloadDivinerSchema","containsAll","assertEx","exists","removeFields","IndexedDbArchivist","IndexSeparator","PayloadDiviner","isPayloadDivinerQueryPayload","PayloadBuilder","openDB","payloadValueFilter","key","value","undefined","payload","sourceValue","Array","isArray","containsAll","extractFields","indexName","slice","split","IndexSeparator","map","field","toLowerCase","IndexedDbPayloadDiviner","PayloadDiviner","configSchemas","IndexedDbPayloadDivinerConfigSchema","defaultConfigSchema","_db","dbName","config","archivist","IndexedDbArchivist","defaultDbName","dbVersion","defaultDbVersion","storeName","defaultStoreName","divineHandler","payloads","query","find","isPayloadDivinerQueryPayload","result","tryUseDb","db","schemas","limit","offset","order","props","removeFields","tx","transaction","store","objectStore","results","parsedOffset","parsedLimit","assertEx","length","filterSchema","filter","schema","direction","suggestedIndex","selectBestIndex","keyRangeValue","getKeyRangeValue","valueFilters","Object","entries","exists","cursor","index","openCursor","IDBKeyRange","only","advance","every","push","continue","done","Promise","all","PayloadBuilder","build","startHandler","indexFields","indexNames","queryKeys","Set","keys","bestMatch","matchCount","has","tryGetInitializedDb","dbs","indexedDB","databases","dbExists","some","name","version","openDB","storeExists","objectStoreNames","contains","close","callback"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/Schema.ts","../../src/Config.ts","../../src/Diviner.ts"],"sourcesContent":["import { PayloadDivinerSchema } from '@xyo-network/diviner-payload-model'\n\nexport const IndexedDbPayloadDivinerSchema = `${PayloadDivinerSchema}.indexeddb` as const\nexport type IndexedDbPayloadDivinerSchema = typeof IndexedDbPayloadDivinerSchema\n","import { IndexDescription } from '@xyo-network/archivist-model'\nimport { DivinerConfig } from '@xyo-network/diviner-model'\n\nimport { IndexedDbPayloadDivinerSchema } from './Schema.ts'\n\nexport const IndexedDbPayloadDivinerConfigSchema = `${IndexedDbPayloadDivinerSchema}.config` as const\nexport type IndexedDbPayloadDivinerConfigSchema = typeof IndexedDbPayloadDivinerConfigSchema\n\nexport type IndexedDbPayloadDivinerConfig = DivinerConfig<{\n /**\n * The database name\n */\n dbName?: string\n /**\n * The version of the DB, defaults to 1\n */\n dbVersion?: number\n schema: IndexedDbPayloadDivinerConfigSchema\n /**\n * The storage configuration\n * // TODO: Hoist to main diviner config\n */\n storage?: {\n /**\n * The indexes to create on the object store\n */\n indexes?: IndexDescription[]\n }\n /**\n * The name of the object store\n */\n storeName?: string\n}>\n","import { containsAll } from '@xylabs/array'\nimport { assertEx } from '@xylabs/assert'\nimport { exists } from '@xylabs/exists'\nimport { Hash } from '@xylabs/hex'\nimport { AnyObject, removeFields } from '@xylabs/object'\nimport { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'\nimport { IndexSeparator } from '@xyo-network/archivist-model'\nimport { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'\nimport { isPayloadDivinerQueryPayload, PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport { PayloadBuilder } from '@xyo-network/payload-builder'\nimport { Payload, Schema, WithMeta } from '@xyo-network/payload-model'\nimport { IDBPDatabase, IDBPObjectStore, openDB } from 'idb'\n\nimport { IndexedDbPayloadDivinerConfigSchema } from './Config.ts'\nimport { IndexedDbPayloadDivinerParams } from './Params.ts'\n\ninterface PayloadStore {\n [s: string]: Payload\n}\n\ntype AnyPayload = Payload<Record<string, unknown>>\n\ntype ValueFilter = (payload?: AnyPayload | null) => boolean\n\nconst payloadValueFilter = (key: keyof AnyPayload, value?: unknown | unknown[]): ValueFilter | undefined => {\n if (!value) return undefined\n return (payload) => {\n if (!payload) return false\n const sourceValue = payload?.[key]\n if (sourceValue === undefined) return false\n return Array.isArray(sourceValue) && Array.isArray(value) ? containsAll(sourceValue, value) : sourceValue == value\n }\n}\n\n// Function to extract fields from an index name\nconst extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map((field) => field.toLowerCase())\n}\n\nexport class IndexedDbPayloadDiviner<\n TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams,\n TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload,\n TOut extends Payload = Payload,\n TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<\n DivinerInstance<TParams, TIn, TOut>,\n TIn,\n TOut\n >,\n> extends PayloadDiviner<TParams, TIn, TOut, TEventData> {\n static override readonly configSchemas: Schema[] = [...super.configSchemas, IndexedDbPayloadDivinerConfigSchema]\n static override readonly defaultConfigSchema: Schema = IndexedDbPayloadDivinerConfigSchema\n\n private _db: IDBPDatabase<PayloadStore> | undefined\n\n /**\n * The database name. If not supplied via config, it defaults\n * to the archivist's name and if archivist's name is not supplied,\n * it defaults to `archivist`. This behavior\n * biases towards a single, isolated DB per archivist which seems to\n * make the most sense for 99% of use cases.\n */\n get dbName() {\n return this.config?.dbName ?? this.config?.archivist ?? IndexedDbArchivist.defaultDbName\n }\n\n /**\n * The database version. If not supplied via config, it defaults to the archivist default version.\n */\n get dbVersion() {\n return this.config?.dbVersion ?? IndexedDbArchivist.defaultDbVersion\n }\n\n /**\n * The name of the object store. If not supplied via config, it defaults\n * to `payloads`.\n */\n get storeName() {\n return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName\n }\n\n protected override async divineHandler(payloads?: TIn[]): Promise<TOut[]> {\n const query = payloads?.find(isPayloadDivinerQueryPayload) as TIn\n if (!query) return []\n const result = await this.tryUseDb(async (db) => {\n const { schemas, limit, offset, order, ...props } = removeFields(query as unknown as WithMeta<TIn> & { sources?: Hash[] }, [\n 'hash',\n 'schema',\n '$meta',\n '$hash',\n ])\n const tx = db.transaction(this.storeName, 'readonly')\n const store = tx.objectStore(this.storeName)\n const results: TOut[] = []\n let parsedOffset = offset ?? 0\n const parsedLimit = limit ?? 10\n assertEx((schemas?.length ?? 1) === 1, () => 'IndexedDbPayloadDiviner: Only one filter schema supported')\n const filterSchema = schemas?.[0]\n const filter = filterSchema ? { schema: filterSchema, ...props } : { ...props }\n const direction: IDBCursorDirection = order === 'desc' ? 'prev' : 'next'\n const suggestedIndex = this.selectBestIndex(filter, store)\n const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter)\n const valueFilters: ValueFilter[] =\n props ?\n Object.entries(props)\n .map(([key, value]) => payloadValueFilter(key, value))\n .filter(exists)\n : []\n let cursor =\n suggestedIndex ?\n // Conditionally filter on schemas\n await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)\n // Just iterate all records\n : await store.openCursor(suggestedIndex, direction)\n\n // Skip records until the offset is reached\n while (cursor && parsedOffset > 0) {\n cursor = await cursor.advance(parsedOffset)\n parsedOffset = 0 // Reset offset after skipping\n }\n // Collect results up to the limit\n while (cursor && results.length < parsedLimit) {\n const value = cursor.value\n if (value) {\n // If we're filtering on more than just the schema\n if (valueFilters.length > 0) {\n // Ensure all filters pass\n if (valueFilters.every((filter) => filter(value))) {\n // Then save the value\n results.push(value)\n }\n } else {\n // Otherwise just save the value\n results.push(value)\n }\n }\n try {\n cursor = await cursor.continue()\n } catch {\n break\n }\n }\n await tx.done\n // Remove any metadata before returning to the client\n return await Promise.all(results.map((payload) => PayloadBuilder.build(payload)))\n })\n return result ?? []\n }\n\n protected override async startHandler() {\n await super.startHandler()\n return true\n }\n\n private getKeyRangeValue(indexName: string | null, query: AnyObject): unknown | unknown[] {\n if (!indexName) return []\n\n // Extracting the relevant fields from the index name\n const indexFields = extractFields(indexName)\n\n // Collecting the values for these fields from the query object\n const keyRangeValue = indexFields.map((field) => query[field as keyof AnyObject])\n return keyRangeValue.length === 1 ? keyRangeValue[0] : keyRangeValue\n }\n\n private selectBestIndex(query: AnyObject, store: IDBPObjectStore<PayloadStore>): string | null {\n // List of available indexes\n const { indexNames } = store\n\n // Convert query object keys to a set for easier comparison\n const queryKeys = new Set(Object.keys(query).map((key) => key.toLowerCase()))\n\n // Find the best matching index\n let bestMatch: { indexName: string; matchCount: number } = { indexName: '', matchCount: 0 }\n\n for (const indexName of indexNames) {\n const indexFields = extractFields(indexName)\n const matchCount = indexFields.filter((field) => queryKeys.has(field)).length\n if (matchCount > bestMatch.matchCount) {\n bestMatch = { indexName, matchCount }\n }\n }\n return bestMatch.matchCount > 0 ? bestMatch.indexName : null\n }\n\n /**\n * Checks that the desired DB/objectStore exists and is initialized to the correct version\n * @returns The initialized DB or undefined if it does not exist in the desired state\n */\n private async tryGetInitializedDb(): Promise<IDBPDatabase<PayloadStore> | undefined> {\n // Enumerate the DBs\n const dbs = await indexedDB.databases()\n // Check that the DB exists at the desired version\n const dbExists = dbs.some((db) => {\n return db.name === this.dbName && db.version === this.dbVersion\n })\n // If the DB exists at the desired version\n if (dbExists) {\n // If the db does exist, open it\n const db = await openDB<PayloadStore>(this.dbName, this.dbVersion)\n // Check that the desired objectStore exists\n const storeExists = db.objectStoreNames.contains(this.storeName)\n // If the correct db/version/objectStore exists\n if (storeExists) {\n return db\n } else {\n // Otherwise close the db so the process that is going to update the\n // db can open it\n db.close()\n }\n }\n }\n\n /**\n * Executes a callback with the initialized DB and then closes the db\n * @param callback The method to execute with the initialized DB\n * @returns\n */\n private async tryUseDb<T>(callback: (db: IDBPDatabase<PayloadStore>) => Promise<T> | T): Promise<T | undefined> {\n // Get the initialized DB\n const db = await this.tryGetInitializedDb()\n if (db) {\n try {\n // Perform the callback\n return await callback(db)\n } finally {\n // Close the DB\n db.close()\n }\n }\n return undefined\n }\n}\n"],"mappings":"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","names":["PayloadDivinerSchema","IndexedDbPayloadDivinerSchema","IndexedDbPayloadDivinerConfigSchema","IndexedDbPayloadDivinerSchema","containsAll","assertEx","exists","removeFields","IndexedDbArchivist","IndexSeparator","PayloadDiviner","isPayloadDivinerQueryPayload","PayloadBuilder","openDB","payloadValueFilter","key","value","undefined","payload","sourceValue","Array","isArray","containsAll","extractFields","indexName","slice","split","IndexSeparator","map","field","toLowerCase","IndexedDbPayloadDiviner","PayloadDiviner","configSchemas","IndexedDbPayloadDivinerConfigSchema","defaultConfigSchema","_db","dbName","config","archivist","IndexedDbArchivist","defaultDbName","dbVersion","defaultDbVersion","storeName","defaultStoreName","divineHandler","payloads","query","find","isPayloadDivinerQueryPayload","result","tryUseDb","db","schemas","limit","offset","order","props","removeFields","tx","transaction","store","objectStore","results","parsedOffset","parsedLimit","assertEx","length","filterSchema","filter","schema","direction","suggestedIndex","selectBestIndex","keyRangeValue","getKeyRangeValue","valueFilters","Object","entries","exists","cursor","index","openCursor","IDBKeyRange","only","advance","every","push","continue","done","Promise","all","PayloadBuilder","build","startHandler","indexFields","indexNames","queryKeys","Set","keys","bestMatch","matchCount","has","tryGetInitializedDb","dbs","indexedDB","databases","dbExists","some","name","version","openDB","storeExists","objectStoreNames","contains","close","callback"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/Schema.ts","../../src/Config.ts","../../src/Diviner.ts"],"sourcesContent":["import { PayloadDivinerSchema } from '@xyo-network/diviner-payload-model'\n\nexport const IndexedDbPayloadDivinerSchema = `${PayloadDivinerSchema}.indexeddb` as const\nexport type IndexedDbPayloadDivinerSchema = typeof IndexedDbPayloadDivinerSchema\n","import { IndexDescription } from '@xyo-network/archivist-model'\nimport { DivinerConfig } from '@xyo-network/diviner-model'\n\nimport { IndexedDbPayloadDivinerSchema } from './Schema.ts'\n\nexport const IndexedDbPayloadDivinerConfigSchema = `${IndexedDbPayloadDivinerSchema}.config` as const\nexport type IndexedDbPayloadDivinerConfigSchema = typeof IndexedDbPayloadDivinerConfigSchema\n\nexport type IndexedDbPayloadDivinerConfig = DivinerConfig<{\n /**\n * The database name\n */\n dbName?: string\n /**\n * The version of the DB, defaults to 1\n */\n dbVersion?: number\n schema: IndexedDbPayloadDivinerConfigSchema\n /**\n * The storage configuration\n * // TODO: Hoist to main diviner config\n */\n storage?: {\n /**\n * The indexes to create on the object store\n */\n indexes?: IndexDescription[]\n }\n /**\n * The name of the object store\n */\n storeName?: string\n}>\n","import { containsAll } from '@xylabs/array'\nimport { assertEx } from '@xylabs/assert'\nimport { exists } from '@xylabs/exists'\nimport { Hash } from '@xylabs/hex'\nimport { AnyObject, removeFields } from '@xylabs/object'\nimport { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'\nimport { IndexSeparator } from '@xyo-network/archivist-model'\nimport { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'\nimport { isPayloadDivinerQueryPayload, PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport { PayloadBuilder } from '@xyo-network/payload-builder'\nimport { Payload, Schema, WithMeta } from '@xyo-network/payload-model'\nimport { IDBPDatabase, IDBPObjectStore, openDB } from 'idb'\n\nimport { IndexedDbPayloadDivinerConfigSchema } from './Config.ts'\nimport { IndexedDbPayloadDivinerParams } from './Params.ts'\n\ninterface PayloadStore {\n [s: string]: Payload\n}\n\ntype AnyPayload = Payload<Record<string, unknown>>\n\ntype ValueFilter = (payload?: AnyPayload | null) => boolean\n\nconst payloadValueFilter = (key: keyof AnyPayload, value?: unknown | unknown[]): ValueFilter | undefined => {\n if (!value) return undefined\n return (payload) => {\n if (!payload) return false\n const sourceValue = payload?.[key]\n if (sourceValue === undefined) return false\n return Array.isArray(sourceValue) && Array.isArray(value) ? containsAll(sourceValue, value) : sourceValue == value\n }\n}\n\n// Function to extract fields from an index name\nconst extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map((field) => field.toLowerCase())\n}\n\nexport class IndexedDbPayloadDiviner<\n TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams,\n TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload,\n TOut extends Payload = Payload,\n TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<\n DivinerInstance<TParams, TIn, TOut>,\n TIn,\n TOut\n >,\n> extends PayloadDiviner<TParams, TIn, TOut, TEventData> {\n static override readonly configSchemas: Schema[] = [...super.configSchemas, IndexedDbPayloadDivinerConfigSchema]\n static override readonly defaultConfigSchema: Schema = IndexedDbPayloadDivinerConfigSchema\n\n private _db: IDBPDatabase<PayloadStore> | undefined\n\n /**\n * The database name. If not supplied via config, it defaults\n * to the archivist's name and if archivist's name is not supplied,\n * it defaults to `archivist`. This behavior\n * biases towards a single, isolated DB per archivist which seems to\n * make the most sense for 99% of use cases.\n */\n get dbName() {\n return this.config?.dbName ?? this.config?.archivist ?? IndexedDbArchivist.defaultDbName\n }\n\n /**\n * The database version. If not supplied via config, it defaults to the archivist default version.\n */\n get dbVersion() {\n return this.config?.dbVersion ?? IndexedDbArchivist.defaultDbVersion\n }\n\n /**\n * The name of the object store. If not supplied via config, it defaults\n * to `payloads`.\n */\n get storeName() {\n return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName\n }\n\n protected override async divineHandler(payloads?: TIn[]): Promise<TOut[]> {\n const query = payloads?.find(isPayloadDivinerQueryPayload) as TIn\n if (!query) return []\n const result = await this.tryUseDb(async (db) => {\n const { schemas, limit, offset, order, ...props } = removeFields(query as unknown as WithMeta<TIn> & { sources?: Hash[] }, [\n 'hash',\n 'schema',\n '$meta',\n '$hash',\n ])\n const tx = db.transaction(this.storeName, 'readonly')\n const store = tx.objectStore(this.storeName)\n const results: TOut[] = []\n let parsedOffset = offset ?? 0\n const parsedLimit = limit ?? 10\n assertEx((schemas?.length ?? 1) === 1, () => 'IndexedDbPayloadDiviner: Only one filter schema supported')\n const filterSchema = schemas?.[0]\n const filter = filterSchema ? { schema: filterSchema, ...props } : { ...props }\n const direction: IDBCursorDirection = order === 'desc' ? 'prev' : 'next'\n const suggestedIndex = this.selectBestIndex(filter, store)\n const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter)\n const valueFilters: ValueFilter[] =\n props ?\n Object.entries(props)\n .map(([key, value]) => payloadValueFilter(key, value))\n .filter(exists)\n : []\n let cursor =\n suggestedIndex ?\n // Conditionally filter on schemas\n await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)\n // Just iterate all records\n : await store.openCursor(suggestedIndex, direction)\n\n // Skip records until the offset is reached\n while (cursor && parsedOffset > 0) {\n cursor = await cursor.advance(parsedOffset)\n parsedOffset = 0 // Reset offset after skipping\n }\n // Collect results up to the limit\n while (cursor && results.length < parsedLimit) {\n const value = cursor.value\n if (value) {\n // If we're filtering on more than just the schema\n if (valueFilters.length > 0) {\n // Ensure all filters pass\n if (valueFilters.every((filter) => filter(value))) {\n // Then save the value\n results.push(value)\n }\n } else {\n // Otherwise just save the value\n results.push(value)\n }\n }\n try {\n cursor = await cursor.continue()\n } catch {\n break\n }\n }\n await tx.done\n // Remove any metadata before returning to the client\n return await Promise.all(results.map((payload) => PayloadBuilder.build(payload)))\n })\n return result ?? []\n }\n\n protected override async startHandler() {\n await super.startHandler()\n return true\n }\n\n private getKeyRangeValue(indexName: string | null, query: AnyObject): unknown | unknown[] {\n if (!indexName) return []\n\n // Extracting the relevant fields from the index name\n const indexFields = extractFields(indexName)\n\n // Collecting the values for these fields from the query object\n const keyRangeValue = indexFields.map((field) => query[field as keyof AnyObject])\n return keyRangeValue.length === 1 ? keyRangeValue[0] : keyRangeValue\n }\n\n private selectBestIndex(query: AnyObject, store: IDBPObjectStore<PayloadStore>): string | null {\n // List of available indexes\n const { indexNames } = store\n\n // Convert query object keys to a set for easier comparison\n const queryKeys = new Set(Object.keys(query).map((key) => key.toLowerCase()))\n\n // Find the best matching index\n let bestMatch: { indexName: string; matchCount: number } = { indexName: '', matchCount: 0 }\n\n for (const indexName of indexNames) {\n const indexFields = extractFields(indexName)\n const matchCount = indexFields.filter((field) => queryKeys.has(field)).length\n if (matchCount > bestMatch.matchCount) {\n bestMatch = { indexName, matchCount }\n }\n }\n return bestMatch.matchCount > 0 ? bestMatch.indexName : null\n }\n\n /**\n * Checks that the desired DB/objectStore exists and is initialized to the correct version\n * @returns The initialized DB or undefined if it does not exist in the desired state\n */\n private async tryGetInitializedDb(): Promise<IDBPDatabase<PayloadStore> | undefined> {\n // Enumerate the DBs\n const dbs = await indexedDB.databases()\n // Check that the DB exists at the desired version\n const dbExists = dbs.some((db) => {\n return db.name === this.dbName && db.version === this.dbVersion\n })\n // If the DB exists at the desired version\n if (dbExists) {\n // If the db does exist, open it\n const db = await openDB<PayloadStore>(this.dbName, this.dbVersion)\n // Check that the desired objectStore exists\n const storeExists = db.objectStoreNames.contains(this.storeName)\n // If the correct db/version/objectStore exists\n if (storeExists) {\n return db\n } else {\n // Otherwise close the db so the process that is going to update the\n // db can open it\n db.close()\n }\n }\n }\n\n /**\n * Executes a callback with the initialized DB and then closes the db\n * @param callback The method to execute with the initialized DB\n * @returns\n */\n private async tryUseDb<T>(callback: (db: IDBPDatabase<PayloadStore>) => Promise<T> | T): Promise<T | undefined> {\n // Get the initialized DB\n const db = await this.tryGetInitializedDb()\n if (db) {\n try {\n // Perform the callback\n return await callback(db)\n } finally {\n // Close the DB\n db.close()\n }\n }\n return undefined\n }\n}\n"],"mappings":"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","names":["PayloadDivinerSchema","IndexedDbPayloadDivinerSchema","IndexedDbPayloadDivinerConfigSchema","IndexedDbPayloadDivinerSchema","containsAll","assertEx","exists","removeFields","IndexedDbArchivist","IndexSeparator","PayloadDiviner","isPayloadDivinerQueryPayload","PayloadBuilder","openDB","payloadValueFilter","key","value","undefined","payload","sourceValue","Array","isArray","containsAll","extractFields","indexName","slice","split","IndexSeparator","map","field","toLowerCase","IndexedDbPayloadDiviner","PayloadDiviner","_db","dbName","config","archivist","IndexedDbArchivist","defaultDbName","dbVersion","defaultDbVersion","storeName","defaultStoreName","divineHandler","payloads","query","find","isPayloadDivinerQueryPayload","result","tryUseDb","db","schemas","limit","offset","order","props","removeFields","tx","transaction","store","objectStore","results","parsedOffset","parsedLimit","assertEx","length","filterSchema","filter","schema","direction","suggestedIndex","selectBestIndex","keyRangeValue","getKeyRangeValue","valueFilters","Object","entries","exists","cursor","index","openCursor","IDBKeyRange","only","advance","every","push","continue","done","Promise","all","PayloadBuilder","build","startHandler","indexFields","indexNames","queryKeys","Set","keys","bestMatch","matchCount","has","tryGetInitializedDb","dbs","indexedDB","databases","dbExists","some","name","version","openDB","storeExists","objectStoreNames","contains","close","callback","configSchemas","IndexedDbPayloadDivinerConfigSchema","defaultConfigSchema"]}