@xyo-network/diviner-payload-indexeddb 2.87.2 → 2.88.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.
@@ -94,47 +94,47 @@ var _IndexedDbPayloadDiviner = class _IndexedDbPayloadDiviner extends import_div
94
94
  const query = (_a = payloads == null ? void 0 : payloads.filter(import_diviner_payload_model2.isPayloadDivinerQueryPayload)) == null ? void 0 : _a.pop();
95
95
  if (!query)
96
96
  return [];
97
- const db = await this.tryGetInitializedDb();
98
- if (!db)
99
- return [];
100
- const { schemas, limit, offset, hash, order, schema: _schema, sources, ...props } = query;
101
- const tx = db.transaction(this.storeName, "readonly");
102
- const store = tx.objectStore(this.storeName);
103
- const results = [];
104
- let parsedOffset = offset ?? 0;
105
- const parsedLimit = limit ?? 10;
106
- (0, import_assert.assertEx)(((schemas == null ? void 0 : schemas.length) ?? 1) === 1, "IndexedDbPayloadDiviner: Only one filter schema supported");
107
- const filterSchema = schemas == null ? void 0 : schemas[0];
108
- const filter = filterSchema ? {
109
- schema: filterSchema,
110
- ...props
111
- } : {
112
- ...props
113
- };
114
- const direction = order === "desc" ? "prev" : "next";
115
- const suggestedIndex = this.selectBestIndex(filter, store);
116
- const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter);
117
- const valueFilters = props ? Object.entries(props).map(([key, value]) => payloadValueFilter(key, value)).filter(import_exists.exists) : [];
118
- let cursor = suggestedIndex ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction) : await store.openCursor(suggestedIndex, direction);
119
- while (cursor && parsedOffset > 0) {
120
- cursor = await cursor.advance(parsedOffset);
121
- parsedOffset = 0;
122
- }
123
- while (cursor && results.length < parsedLimit) {
124
- const value = cursor.value;
125
- if (value) {
126
- if (valueFilters.length > 0) {
127
- if (valueFilters.every((filter2) => filter2(value))) {
97
+ const result = await this.tryUseDb(async (db) => {
98
+ const { schemas, limit, offset, hash, order, schema: _schema, sources, ...props } = query;
99
+ const tx = db.transaction(this.storeName, "readonly");
100
+ const store = tx.objectStore(this.storeName);
101
+ const results = [];
102
+ let parsedOffset = offset ?? 0;
103
+ const parsedLimit = limit ?? 10;
104
+ (0, import_assert.assertEx)(((schemas == null ? void 0 : schemas.length) ?? 1) === 1, "IndexedDbPayloadDiviner: Only one filter schema supported");
105
+ const filterSchema = schemas == null ? void 0 : schemas[0];
106
+ const filter = filterSchema ? {
107
+ schema: filterSchema,
108
+ ...props
109
+ } : {
110
+ ...props
111
+ };
112
+ const direction = order === "desc" ? "prev" : "next";
113
+ const suggestedIndex = this.selectBestIndex(filter, store);
114
+ const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter);
115
+ const valueFilters = props ? Object.entries(props).map(([key, value]) => payloadValueFilter(key, value)).filter(import_exists.exists) : [];
116
+ let cursor = suggestedIndex ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction) : await store.openCursor(suggestedIndex, direction);
117
+ while (cursor && parsedOffset > 0) {
118
+ cursor = await cursor.advance(parsedOffset);
119
+ parsedOffset = 0;
120
+ }
121
+ while (cursor && results.length < parsedLimit) {
122
+ const value = cursor.value;
123
+ if (value) {
124
+ if (valueFilters.length > 0) {
125
+ if (valueFilters.every((filter2) => filter2(value))) {
126
+ results.push(value);
127
+ }
128
+ } else {
128
129
  results.push(value);
129
130
  }
130
- } else {
131
- results.push(value);
132
131
  }
132
+ cursor = await cursor.continue();
133
133
  }
134
- cursor = await cursor.continue();
135
- }
136
- await tx.done;
137
- return results.map((payload) => import_hash.PayloadHasher.jsonPayload(payload));
134
+ await tx.done;
135
+ return results.map((payload) => import_hash.PayloadHasher.jsonPayload(payload));
136
+ });
137
+ return result ?? [];
138
138
  }
139
139
  async startHandler() {
140
140
  await super.startHandler();
@@ -173,23 +173,39 @@ var _IndexedDbPayloadDiviner = class _IndexedDbPayloadDiviner extends import_div
173
173
  return bestMatch.matchCount > 0 ? bestMatch.indexName : null;
174
174
  }
175
175
  /**
176
- * Checks that the desired DB/Store exists and is initialized
177
- * @returns The initialized DB or undefined if it does not exist
176
+ * Checks that the desired DB/objectStore exists and is initialized to the correct version
177
+ * @returns The initialized DB or undefined if it does not exist in the desired state
178
178
  */
179
179
  async tryGetInitializedDb() {
180
- if (this._db)
181
- return this._db;
182
180
  const dbs = await indexedDB.databases();
183
- const dbExists = dbs.some((db2) => {
184
- return db2.name === this.dbName && db2.version === this.dbVersion;
181
+ const dbExists = dbs.some((db) => {
182
+ return db.name === this.dbName && db.version === this.dbVersion;
185
183
  });
186
- if (!dbExists)
187
- return;
188
- const db = await (0, import_idb.openDB)(this.dbName, this.dbVersion);
189
- const storeExists = db.objectStoreNames.contains(this.storeName);
190
- if (storeExists)
191
- this._db = db;
192
- return this._db;
184
+ if (dbExists) {
185
+ const db = await (0, import_idb.openDB)(this.dbName, this.dbVersion);
186
+ const storeExists = db.objectStoreNames.contains(this.storeName);
187
+ if (storeExists) {
188
+ return db;
189
+ } else {
190
+ db.close();
191
+ }
192
+ }
193
+ }
194
+ /**
195
+ * Executes a callback with the initialized DB and then closes the db
196
+ * @param callback The method to execute with the initialized DB
197
+ * @returns
198
+ */
199
+ async tryUseDb(callback) {
200
+ const db = await this.tryGetInitializedDb();
201
+ if (db) {
202
+ try {
203
+ return await callback(db);
204
+ } finally {
205
+ db.close();
206
+ }
207
+ }
208
+ return void 0;
193
209
  }
194
210
  };
195
211
  __name(_IndexedDbPayloadDiviner, "IndexedDbPayloadDiviner");
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/index.ts","../../src/Schema.ts","../../src/Config.ts","../../src/Diviner.ts"],"sourcesContent":["export * from './Config'\nexport * from './Diviner'\nexport * from './Params'\nexport * from './Schema'\n","import { PayloadDivinerSchema } from '@xyo-network/diviner-payload-model'\n\nexport const IndexedDbPayloadDivinerSchema = `${PayloadDivinerSchema}.indexeddb`\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'\n\nexport const IndexedDbPayloadDivinerConfigSchema = `${IndexedDbPayloadDivinerSchema}.config`\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 { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'\nimport { IndexSeparator } from '@xyo-network/archivist-model'\nimport { DivinerModule, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'\nimport { isPayloadDivinerQueryPayload, PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport { PayloadHasher } from '@xyo-network/hash'\nimport { AnyObject } from '@xyo-network/object'\nimport { Payload } from '@xyo-network/payload-model'\nimport { IDBPDatabase, IDBPObjectStore, openDB } from 'idb'\n\nimport { IndexedDbPayloadDivinerConfigSchema } from './Config'\nimport { IndexedDbPayloadDivinerParams } from './Params'\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\nexport class IndexedDbPayloadDiviner<\n TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams,\n TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload,\n TOut extends Payload = Payload,\n TEventData extends DivinerModuleEventData<DivinerModule<TParams>, TIn, TOut> = DivinerModuleEventData<DivinerModule<TParams>, TIn, TOut>,\n> extends PayloadDiviner<TParams, TIn, TOut, TEventData> {\n static override configSchemas = [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?.filter(isPayloadDivinerQueryPayload)?.pop()\n if (!query) return []\n const db = await this.tryGetInitializedDb()\n if (!db) return []\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { schemas, limit, offset, hash, order, schema: _schema, sources, ...props } = query as unknown as TIn & { sources?: string[] }\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[] = props\n ? Object.entries(props)\n .map(([key, value]) => payloadValueFilter(key, value))\n .filter(exists)\n : []\n let cursor = 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 cursor = await cursor.continue()\n }\n await tx.done\n // Remove any metadata before returning to the client\n return results.map((payload) => PayloadHasher.jsonPayload(payload))\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 // Function to extract fields from an index name\n const extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map((field) => field.toLowerCase())\n }\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 // Function to extract fields from an index name\n const extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map((field) => field.toLowerCase())\n }\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/Store exists and is initialized\n * @returns The initialized DB or undefined if it does not exist\n */\n private async tryGetInitializedDb(): Promise<IDBPDatabase<PayloadStore> | undefined> {\n // If we've already checked and found a successfully initialized\n // db and objectStore, return the cached value\n if (this._db) return this._db\n // Enumerate the DBs\n const dbs = await indexedDB.databases()\n const dbExists = dbs.some((db) => {\n // Check for the desired name/version\n return db.name === this.dbName && db.version === this.dbVersion\n })\n // If the DB does not exist at the desired version, return undefined\n if (!dbExists) return\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/store exists, cache it for future calls\n if (storeExists) this._db = db\n return this._db\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","IndexedDbPayloadDiviner","PayloadDiviner","_db","dbName","config","archivist","IndexedDbArchivist","defaultDbName","dbVersion","defaultDbVersion","storeName","defaultStoreName","divineHandler","payloads","query","filter","isPayloadDivinerQueryPayload","pop","db","tryGetInitializedDb","schemas","limit","offset","hash","order","schema","_schema","sources","props","tx","transaction","store","objectStore","results","parsedOffset","parsedLimit","assertEx","length","filterSchema","direction","suggestedIndex","selectBestIndex","keyRangeValue","getKeyRangeValue","valueFilters","Object","entries","map","exists","cursor","index","openCursor","IDBKeyRange","only","advance","every","push","continue","done","PayloadHasher","jsonPayload","startHandler","indexName","extractFields","slice","split","IndexSeparator","field","toLowerCase","indexFields","indexNames","queryKeys","Set","keys","bestMatch","matchCount","has","dbs","indexedDB","databases","dbExists","some","name","version","openDB","storeExists","objectStoreNames","contains","configSchemas","IndexedDbPayloadDivinerConfigSchema"]}
1
+ {"version":3,"sources":["../../src/index.ts","../../src/Schema.ts","../../src/Config.ts","../../src/Diviner.ts"],"sourcesContent":["export * from './Config'\nexport * from './Diviner'\nexport * from './Params'\nexport * from './Schema'\n","import { PayloadDivinerSchema } from '@xyo-network/diviner-payload-model'\n\nexport const IndexedDbPayloadDivinerSchema = `${PayloadDivinerSchema}.indexeddb`\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'\n\nexport const IndexedDbPayloadDivinerConfigSchema = `${IndexedDbPayloadDivinerSchema}.config`\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 { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'\nimport { IndexSeparator } from '@xyo-network/archivist-model'\nimport { DivinerModule, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'\nimport { isPayloadDivinerQueryPayload, PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport { PayloadHasher } from '@xyo-network/hash'\nimport { AnyObject } from '@xyo-network/object'\nimport { Payload } from '@xyo-network/payload-model'\nimport { IDBPDatabase, IDBPObjectStore, openDB } from 'idb'\n\nimport { IndexedDbPayloadDivinerConfigSchema } from './Config'\nimport { IndexedDbPayloadDivinerParams } from './Params'\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\nexport class IndexedDbPayloadDiviner<\n TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams,\n TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload,\n TOut extends Payload = Payload,\n TEventData extends DivinerModuleEventData<DivinerModule<TParams>, TIn, TOut> = DivinerModuleEventData<DivinerModule<TParams>, TIn, TOut>,\n> extends PayloadDiviner<TParams, TIn, TOut, TEventData> {\n static override configSchemas = [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?.filter(isPayloadDivinerQueryPayload)?.pop()\n if (!query) return []\n const result = await this.tryUseDb(async (db) => {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { schemas, limit, offset, hash, order, schema: _schema, sources, ...props } = query as unknown as TIn & { sources?: string[] }\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[] = props\n ? Object.entries(props)\n .map(([key, value]) => payloadValueFilter(key, value))\n .filter(exists)\n : []\n let cursor = 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 cursor = await cursor.continue()\n }\n await tx.done\n // Remove any metadata before returning to the client\n return results.map((payload) => PayloadHasher.jsonPayload(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 // Function to extract fields from an index name\n const extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map((field) => field.toLowerCase())\n }\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 // Function to extract fields from an index name\n const extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map((field) => field.toLowerCase())\n }\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","IndexedDbPayloadDiviner","PayloadDiviner","_db","dbName","config","archivist","IndexedDbArchivist","defaultDbName","dbVersion","defaultDbVersion","storeName","defaultStoreName","divineHandler","payloads","query","filter","isPayloadDivinerQueryPayload","pop","result","tryUseDb","db","schemas","limit","offset","hash","order","schema","_schema","sources","props","tx","transaction","store","objectStore","results","parsedOffset","parsedLimit","assertEx","length","filterSchema","direction","suggestedIndex","selectBestIndex","keyRangeValue","getKeyRangeValue","valueFilters","Object","entries","map","exists","cursor","index","openCursor","IDBKeyRange","only","advance","every","push","continue","done","PayloadHasher","jsonPayload","startHandler","indexName","extractFields","slice","split","IndexSeparator","field","toLowerCase","indexFields","indexNames","queryKeys","Set","keys","bestMatch","matchCount","has","tryGetInitializedDb","dbs","indexedDB","databases","dbExists","some","name","version","openDB","storeExists","objectStoreNames","contains","close","callback","configSchemas","IndexedDbPayloadDivinerConfigSchema"]}
@@ -68,47 +68,47 @@ var _IndexedDbPayloadDiviner = class _IndexedDbPayloadDiviner extends PayloadDiv
68
68
  const query = (_a = payloads == null ? void 0 : payloads.filter(isPayloadDivinerQueryPayload)) == null ? void 0 : _a.pop();
69
69
  if (!query)
70
70
  return [];
71
- const db = await this.tryGetInitializedDb();
72
- if (!db)
73
- return [];
74
- const { schemas, limit, offset, hash, order, schema: _schema, sources, ...props } = query;
75
- const tx = db.transaction(this.storeName, "readonly");
76
- const store = tx.objectStore(this.storeName);
77
- const results = [];
78
- let parsedOffset = offset ?? 0;
79
- const parsedLimit = limit ?? 10;
80
- assertEx(((schemas == null ? void 0 : schemas.length) ?? 1) === 1, "IndexedDbPayloadDiviner: Only one filter schema supported");
81
- const filterSchema = schemas == null ? void 0 : schemas[0];
82
- const filter = filterSchema ? {
83
- schema: filterSchema,
84
- ...props
85
- } : {
86
- ...props
87
- };
88
- const direction = order === "desc" ? "prev" : "next";
89
- const suggestedIndex = this.selectBestIndex(filter, store);
90
- const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter);
91
- const valueFilters = props ? Object.entries(props).map(([key, value]) => payloadValueFilter(key, value)).filter(exists) : [];
92
- let cursor = suggestedIndex ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction) : await store.openCursor(suggestedIndex, direction);
93
- while (cursor && parsedOffset > 0) {
94
- cursor = await cursor.advance(parsedOffset);
95
- parsedOffset = 0;
96
- }
97
- while (cursor && results.length < parsedLimit) {
98
- const value = cursor.value;
99
- if (value) {
100
- if (valueFilters.length > 0) {
101
- if (valueFilters.every((filter2) => filter2(value))) {
71
+ const result = await this.tryUseDb(async (db) => {
72
+ const { schemas, limit, offset, hash, order, schema: _schema, sources, ...props } = query;
73
+ const tx = db.transaction(this.storeName, "readonly");
74
+ const store = tx.objectStore(this.storeName);
75
+ const results = [];
76
+ let parsedOffset = offset ?? 0;
77
+ const parsedLimit = limit ?? 10;
78
+ assertEx(((schemas == null ? void 0 : schemas.length) ?? 1) === 1, "IndexedDbPayloadDiviner: Only one filter schema supported");
79
+ const filterSchema = schemas == null ? void 0 : schemas[0];
80
+ const filter = filterSchema ? {
81
+ schema: filterSchema,
82
+ ...props
83
+ } : {
84
+ ...props
85
+ };
86
+ const direction = order === "desc" ? "prev" : "next";
87
+ const suggestedIndex = this.selectBestIndex(filter, store);
88
+ const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter);
89
+ const valueFilters = props ? Object.entries(props).map(([key, value]) => payloadValueFilter(key, value)).filter(exists) : [];
90
+ let cursor = suggestedIndex ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction) : await store.openCursor(suggestedIndex, direction);
91
+ while (cursor && parsedOffset > 0) {
92
+ cursor = await cursor.advance(parsedOffset);
93
+ parsedOffset = 0;
94
+ }
95
+ while (cursor && results.length < parsedLimit) {
96
+ const value = cursor.value;
97
+ if (value) {
98
+ if (valueFilters.length > 0) {
99
+ if (valueFilters.every((filter2) => filter2(value))) {
100
+ results.push(value);
101
+ }
102
+ } else {
102
103
  results.push(value);
103
104
  }
104
- } else {
105
- results.push(value);
106
105
  }
106
+ cursor = await cursor.continue();
107
107
  }
108
- cursor = await cursor.continue();
109
- }
110
- await tx.done;
111
- return results.map((payload) => PayloadHasher.jsonPayload(payload));
108
+ await tx.done;
109
+ return results.map((payload) => PayloadHasher.jsonPayload(payload));
110
+ });
111
+ return result ?? [];
112
112
  }
113
113
  async startHandler() {
114
114
  await super.startHandler();
@@ -147,23 +147,39 @@ var _IndexedDbPayloadDiviner = class _IndexedDbPayloadDiviner extends PayloadDiv
147
147
  return bestMatch.matchCount > 0 ? bestMatch.indexName : null;
148
148
  }
149
149
  /**
150
- * Checks that the desired DB/Store exists and is initialized
151
- * @returns The initialized DB or undefined if it does not exist
150
+ * Checks that the desired DB/objectStore exists and is initialized to the correct version
151
+ * @returns The initialized DB or undefined if it does not exist in the desired state
152
152
  */
153
153
  async tryGetInitializedDb() {
154
- if (this._db)
155
- return this._db;
156
154
  const dbs = await indexedDB.databases();
157
- const dbExists = dbs.some((db2) => {
158
- return db2.name === this.dbName && db2.version === this.dbVersion;
155
+ const dbExists = dbs.some((db) => {
156
+ return db.name === this.dbName && db.version === this.dbVersion;
159
157
  });
160
- if (!dbExists)
161
- return;
162
- const db = await openDB(this.dbName, this.dbVersion);
163
- const storeExists = db.objectStoreNames.contains(this.storeName);
164
- if (storeExists)
165
- this._db = db;
166
- return this._db;
158
+ if (dbExists) {
159
+ const db = await openDB(this.dbName, this.dbVersion);
160
+ const storeExists = db.objectStoreNames.contains(this.storeName);
161
+ if (storeExists) {
162
+ return db;
163
+ } else {
164
+ db.close();
165
+ }
166
+ }
167
+ }
168
+ /**
169
+ * Executes a callback with the initialized DB and then closes the db
170
+ * @param callback The method to execute with the initialized DB
171
+ * @returns
172
+ */
173
+ async tryUseDb(callback) {
174
+ const db = await this.tryGetInitializedDb();
175
+ if (db) {
176
+ try {
177
+ return await callback(db);
178
+ } finally {
179
+ db.close();
180
+ }
181
+ }
182
+ return void 0;
167
183
  }
168
184
  };
169
185
  __name(_IndexedDbPayloadDiviner, "IndexedDbPayloadDiviner");
@@ -1 +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`\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'\n\nexport const IndexedDbPayloadDivinerConfigSchema = `${IndexedDbPayloadDivinerSchema}.config`\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 { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'\nimport { IndexSeparator } from '@xyo-network/archivist-model'\nimport { DivinerModule, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'\nimport { isPayloadDivinerQueryPayload, PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport { PayloadHasher } from '@xyo-network/hash'\nimport { AnyObject } from '@xyo-network/object'\nimport { Payload } from '@xyo-network/payload-model'\nimport { IDBPDatabase, IDBPObjectStore, openDB } from 'idb'\n\nimport { IndexedDbPayloadDivinerConfigSchema } from './Config'\nimport { IndexedDbPayloadDivinerParams } from './Params'\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\nexport class IndexedDbPayloadDiviner<\n TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams,\n TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload,\n TOut extends Payload = Payload,\n TEventData extends DivinerModuleEventData<DivinerModule<TParams>, TIn, TOut> = DivinerModuleEventData<DivinerModule<TParams>, TIn, TOut>,\n> extends PayloadDiviner<TParams, TIn, TOut, TEventData> {\n static override configSchemas = [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?.filter(isPayloadDivinerQueryPayload)?.pop()\n if (!query) return []\n const db = await this.tryGetInitializedDb()\n if (!db) return []\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { schemas, limit, offset, hash, order, schema: _schema, sources, ...props } = query as unknown as TIn & { sources?: string[] }\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[] = props\n ? Object.entries(props)\n .map(([key, value]) => payloadValueFilter(key, value))\n .filter(exists)\n : []\n let cursor = 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 cursor = await cursor.continue()\n }\n await tx.done\n // Remove any metadata before returning to the client\n return results.map((payload) => PayloadHasher.jsonPayload(payload))\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 // Function to extract fields from an index name\n const extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map((field) => field.toLowerCase())\n }\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 // Function to extract fields from an index name\n const extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map((field) => field.toLowerCase())\n }\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/Store exists and is initialized\n * @returns The initialized DB or undefined if it does not exist\n */\n private async tryGetInitializedDb(): Promise<IDBPDatabase<PayloadStore> | undefined> {\n // If we've already checked and found a successfully initialized\n // db and objectStore, return the cached value\n if (this._db) return this._db\n // Enumerate the DBs\n const dbs = await indexedDB.databases()\n const dbExists = dbs.some((db) => {\n // Check for the desired name/version\n return db.name === this.dbName && db.version === this.dbVersion\n })\n // If the DB does not exist at the desired version, return undefined\n if (!dbExists) return\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/store exists, cache it for future calls\n if (storeExists) this._db = db\n return this._db\n }\n}\n"],"mappings":"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","names":["PayloadDivinerSchema","IndexedDbPayloadDivinerSchema","IndexedDbPayloadDivinerConfigSchema","IndexedDbPayloadDivinerSchema","containsAll","assertEx","exists","IndexedDbArchivist","IndexSeparator","PayloadDiviner","isPayloadDivinerQueryPayload","PayloadHasher","openDB","payloadValueFilter","key","value","undefined","payload","sourceValue","Array","isArray","containsAll","IndexedDbPayloadDiviner","PayloadDiviner","_db","dbName","config","archivist","IndexedDbArchivist","defaultDbName","dbVersion","defaultDbVersion","storeName","defaultStoreName","divineHandler","payloads","query","filter","isPayloadDivinerQueryPayload","pop","db","tryGetInitializedDb","schemas","limit","offset","hash","order","schema","_schema","sources","props","tx","transaction","store","objectStore","results","parsedOffset","parsedLimit","assertEx","length","filterSchema","direction","suggestedIndex","selectBestIndex","keyRangeValue","getKeyRangeValue","valueFilters","Object","entries","map","exists","cursor","index","openCursor","IDBKeyRange","only","advance","every","push","continue","done","PayloadHasher","jsonPayload","startHandler","indexName","extractFields","slice","split","IndexSeparator","field","toLowerCase","indexFields","indexNames","queryKeys","Set","keys","bestMatch","matchCount","has","dbs","indexedDB","databases","dbExists","some","name","version","openDB","storeExists","objectStoreNames","contains","configSchemas","IndexedDbPayloadDivinerConfigSchema"]}
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`\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'\n\nexport const IndexedDbPayloadDivinerConfigSchema = `${IndexedDbPayloadDivinerSchema}.config`\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 { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'\nimport { IndexSeparator } from '@xyo-network/archivist-model'\nimport { DivinerModule, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'\nimport { isPayloadDivinerQueryPayload, PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport { PayloadHasher } from '@xyo-network/hash'\nimport { AnyObject } from '@xyo-network/object'\nimport { Payload } from '@xyo-network/payload-model'\nimport { IDBPDatabase, IDBPObjectStore, openDB } from 'idb'\n\nimport { IndexedDbPayloadDivinerConfigSchema } from './Config'\nimport { IndexedDbPayloadDivinerParams } from './Params'\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\nexport class IndexedDbPayloadDiviner<\n TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams,\n TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload,\n TOut extends Payload = Payload,\n TEventData extends DivinerModuleEventData<DivinerModule<TParams>, TIn, TOut> = DivinerModuleEventData<DivinerModule<TParams>, TIn, TOut>,\n> extends PayloadDiviner<TParams, TIn, TOut, TEventData> {\n static override configSchemas = [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?.filter(isPayloadDivinerQueryPayload)?.pop()\n if (!query) return []\n const result = await this.tryUseDb(async (db) => {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { schemas, limit, offset, hash, order, schema: _schema, sources, ...props } = query as unknown as TIn & { sources?: string[] }\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[] = props\n ? Object.entries(props)\n .map(([key, value]) => payloadValueFilter(key, value))\n .filter(exists)\n : []\n let cursor = 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 cursor = await cursor.continue()\n }\n await tx.done\n // Remove any metadata before returning to the client\n return results.map((payload) => PayloadHasher.jsonPayload(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 // Function to extract fields from an index name\n const extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map((field) => field.toLowerCase())\n }\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 // Function to extract fields from an index name\n const extractFields = (indexName: string): string[] => {\n return indexName\n .slice(3)\n .split(IndexSeparator)\n .map((field) => field.toLowerCase())\n }\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","IndexedDbArchivist","IndexSeparator","PayloadDiviner","isPayloadDivinerQueryPayload","PayloadHasher","openDB","payloadValueFilter","key","value","undefined","payload","sourceValue","Array","isArray","containsAll","IndexedDbPayloadDiviner","PayloadDiviner","_db","dbName","config","archivist","IndexedDbArchivist","defaultDbName","dbVersion","defaultDbVersion","storeName","defaultStoreName","divineHandler","payloads","query","filter","isPayloadDivinerQueryPayload","pop","result","tryUseDb","db","schemas","limit","offset","hash","order","schema","_schema","sources","props","tx","transaction","store","objectStore","results","parsedOffset","parsedLimit","assertEx","length","filterSchema","direction","suggestedIndex","selectBestIndex","keyRangeValue","getKeyRangeValue","valueFilters","Object","entries","map","exists","cursor","index","openCursor","IDBKeyRange","only","advance","every","push","continue","done","PayloadHasher","jsonPayload","startHandler","indexName","extractFields","slice","split","IndexSeparator","field","toLowerCase","indexFields","indexNames","queryKeys","Set","keys","bestMatch","matchCount","has","tryGetInitializedDb","dbs","indexedDB","databases","dbExists","some","name","version","openDB","storeExists","objectStoreNames","contains","close","callback","configSchemas","IndexedDbPayloadDivinerConfigSchema"]}
package/package.json CHANGED
@@ -10,26 +10,27 @@
10
10
  "url": "https://github.com/XYOracleNetwork/sdk-xyo-client-js/issues"
11
11
  },
12
12
  "dependencies": {
13
- "@xylabs/array": "^2.13.23",
14
- "@xylabs/assert": "^2.13.23",
15
- "@xylabs/exists": "^2.13.23",
16
- "@xyo-network/archivist-model": "~2.87.2",
17
- "@xyo-network/diviner-model": "~2.87.2",
18
- "@xyo-network/diviner-payload-abstract": "~2.87.2",
19
- "@xyo-network/diviner-payload-model": "~2.87.2",
20
- "@xyo-network/hash": "~2.87.2",
21
- "@xyo-network/payload-model": "~2.87.2",
13
+ "@xylabs/array": "^2.13.25",
14
+ "@xylabs/assert": "^2.13.25",
15
+ "@xylabs/exists": "^2.13.25",
16
+ "@xyo-network/archivist-indexeddb": "~2.88.1",
17
+ "@xyo-network/archivist-model": "~2.88.1",
18
+ "@xyo-network/diviner-model": "~2.88.1",
19
+ "@xyo-network/diviner-payload-abstract": "~2.88.1",
20
+ "@xyo-network/diviner-payload-model": "~2.88.1",
21
+ "@xyo-network/hash": "~2.88.1",
22
+ "@xyo-network/module-model": "~2.88.1",
23
+ "@xyo-network/object": "~2.88.1",
24
+ "@xyo-network/payload-model": "~2.88.1",
22
25
  "idb": "^8.0.0"
23
26
  },
24
27
  "devDependencies": {
25
28
  "@xylabs/ts-scripts-yarn3": "^3.2.41",
26
29
  "@xylabs/tsconfig": "^3.2.41",
27
- "@xyo-network/account": "~2.87.2",
28
- "@xyo-network/archivist-indexeddb": "~2.87.2",
29
- "@xyo-network/module-model": "~2.87.2",
30
- "@xyo-network/node-memory": "~2.87.2",
31
- "@xyo-network/object": "~2.87.2",
32
- "@xyo-network/payload-builder": "~2.87.2",
30
+ "@xyo-network/account": "~2.88.1",
31
+ "@xyo-network/archivist-indexeddb": "~2.88.1",
32
+ "@xyo-network/node-memory": "~2.88.1",
33
+ "@xyo-network/payload-builder": "~2.88.1",
33
34
  "fake-indexeddb": "^5.0.2",
34
35
  "typescript": "^5.3.3"
35
36
  },
@@ -72,6 +73,6 @@
72
73
  "url": "https://github.com/XYOracleNetwork/sdk-xyo-client-js.git"
73
74
  },
74
75
  "sideEffects": false,
75
- "version": "2.87.2",
76
+ "version": "2.88.1",
76
77
  "type": "module"
77
78
  }