@xyo-network/diviner-payload-indexeddb 3.6.0-rc.1 → 3.6.0-rc.11

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.
@@ -6,7 +6,6 @@ import type { IndexedDbPayloadDivinerParams } from './Params.ts';
6
6
  export declare class IndexedDbPayloadDiviner<TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams, TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload, TOut extends Payload = Payload, TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut>> extends PayloadDiviner<TParams, TIn, TOut, TEventData> {
7
7
  static readonly configSchemas: Schema[];
8
8
  static readonly defaultConfigSchema: Schema;
9
- private _db;
10
9
  /**
11
10
  * The database name. If not supplied via config, it defaults
12
11
  * to the archivist's name and if archivist's name is not supplied,
@@ -26,8 +25,6 @@ export declare class IndexedDbPayloadDiviner<TParams extends IndexedDbPayloadDiv
26
25
  get storeName(): string;
27
26
  protected divineHandler(payloads?: TIn[]): Promise<TOut[]>;
28
27
  protected startHandler(): Promise<boolean>;
29
- private getKeyRangeValue;
30
- private selectBestIndex;
31
28
  /**
32
29
  * Checks that the desired DB/objectStore exists and is initialized to the correct version
33
30
  * @returns The initialized DB or undefined if it does not exist in the desired state
@@ -1 +1 @@
1
- {"version":3,"file":"Diviner.d.ts","sourceRoot":"","sources":["../../src/Diviner.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAA;AACzF,OAAO,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAA;AACtE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAA;AAEpF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAA;AAKjE,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAA;AA4BhE,qBAAa,uBAAuB,CAClC,OAAO,SAAS,6BAA6B,GAAG,6BAA6B,EAC7E,GAAG,SAAS,0BAA0B,GAAG,0BAA0B,EACnE,IAAI,SAAS,OAAO,GAAG,OAAO,EAC9B,UAAU,SAAS,sBAAsB,CAAC,eAAe,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,sBAAsB,CAChH,eAAe,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,EACnC,GAAG,EACH,IAAI,CACL,CACD,SAAQ,cAAc,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,CAAC;IACtD,gBAAyB,aAAa,EAAE,MAAM,EAAE,CAAgE;IAChH,gBAAyB,mBAAmB,EAAE,MAAM,CAAsC;IAE1F,OAAO,CAAC,GAAG,CAAwC;IAEnD;;;;;;OAMG;IACH,IAAI,MAAM,WAET;IAED;;OAEG;IACH,IAAI,SAAS,WAEZ;IAED;;;OAGG;IACH,IAAI,SAAS,WAEZ;cAEwB,aAAa,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;cAoEhD,YAAY;IAKrC,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,eAAe;IAoBvB;;;OAGG;YACW,mBAAmB;IAwBjC;;;;OAIG;YACW,QAAQ;CAcvB"}
1
+ {"version":3,"file":"Diviner.d.ts","sourceRoot":"","sources":["../../src/Diviner.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAA;AACzF,OAAO,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAA;AACtE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAA;AAEpF,OAAO,KAAK,EACV,OAAO,EAAE,MAAM,EAChB,MAAM,4BAA4B,CAAA;AAKnC,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAA;AAoBhE,qBAAa,uBAAuB,CAClC,OAAO,SAAS,6BAA6B,GAAG,6BAA6B,EAC7E,GAAG,SAAS,0BAA0B,GAAG,0BAA0B,EACnE,IAAI,SAAS,OAAO,GAAG,OAAO,EAC9B,UAAU,SAAS,sBAAsB,CAAC,eAAe,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,sBAAsB,CAChH,eAAe,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,EACnC,GAAG,EACH,IAAI,CACL,CACD,SAAQ,cAAc,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,CAAC;IACtD,gBAAyB,aAAa,EAAE,MAAM,EAAE,CAAgE;IAChH,gBAAyB,mBAAmB,EAAE,MAAM,CAAsC;IAE1F;;;;;;OAMG;IACH,IAAI,MAAM,WAET;IAED;;OAEG;IACH,IAAI,SAAS,WAEZ;IAED;;;OAGG;IACH,IAAI,SAAS,WAEZ;cAEwB,aAAa,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;cAkEhD,YAAY;IAKrC;;;OAGG;YACW,mBAAmB;IAwBjC;;;;OAIG;YACW,QAAQ;CAcvB"}
@@ -14,7 +14,6 @@ import { assertEx } from "@xylabs/assert";
14
14
  import { exists } from "@xylabs/exists";
15
15
  import { removeFields } from "@xylabs/object";
16
16
  import { IndexedDbArchivist } from "@xyo-network/archivist-indexeddb";
17
- import { IndexSeparator } from "@xyo-network/archivist-model";
18
17
  import { PayloadDiviner } from "@xyo-network/diviner-payload-abstract";
19
18
  import { isPayloadDivinerQueryPayload } from "@xyo-network/diviner-payload-model";
20
19
  import { openDB } from "idb";
@@ -27,9 +26,6 @@ var payloadValueFilter = /* @__PURE__ */ __name((key, value) => {
27
26
  return Array.isArray(sourceValue) && Array.isArray(value) ? containsAll(sourceValue, value) : sourceValue == value;
28
27
  };
29
28
  }, "payloadValueFilter");
30
- var extractFields = /* @__PURE__ */ __name((indexName) => {
31
- return indexName.slice(3).split(IndexSeparator).map((field) => field.toLowerCase());
32
- }, "extractFields");
33
29
  var IndexedDbPayloadDiviner = class extends PayloadDiviner {
34
30
  static {
35
31
  __name(this, "IndexedDbPayloadDiviner");
@@ -39,7 +35,6 @@ var IndexedDbPayloadDiviner = class extends PayloadDiviner {
39
35
  IndexedDbPayloadDivinerConfigSchema
40
36
  ];
41
37
  static defaultConfigSchema = IndexedDbPayloadDivinerConfigSchema;
42
- _db;
43
38
  /**
44
39
  * The database name. If not supplied via config, it defaults
45
40
  * to the archivist's name and if archivist's name is not supplied,
@@ -67,14 +62,13 @@ var IndexedDbPayloadDiviner = class extends PayloadDiviner {
67
62
  const query = payloads?.find(isPayloadDivinerQueryPayload);
68
63
  if (!query) return [];
69
64
  const result = await this.tryUseDb(async (db) => {
70
- const { schemas, limit, offset, order, ...props } = removeFields(query, [
71
- "hash",
65
+ const { schemas, limit, cursor, order, ...props } = removeFields(query, [
72
66
  "schema"
73
67
  ]);
74
68
  const tx = db.transaction(this.storeName, "readonly");
75
69
  const store = tx.objectStore(this.storeName);
76
70
  const results = [];
77
- let parsedOffset = offset ?? 0;
71
+ let parsedCursor = cursor;
78
72
  const parsedLimit = limit ?? 10;
79
73
  assertEx((schemas?.length ?? 1) === 1, () => "IndexedDbPayloadDiviner: Only one filter schema supported");
80
74
  const filterSchema = schemas?.[0];
@@ -84,17 +78,19 @@ var IndexedDbPayloadDiviner = class extends PayloadDiviner {
84
78
  } : {
85
79
  ...props
86
80
  };
81
+ const valueFilters = Object.entries(filter).map(([key, value]) => payloadValueFilter(key, value)).filter(exists);
87
82
  const direction = order === "desc" ? "prev" : "next";
88
- const suggestedIndex = this.selectBestIndex(filter, store);
89
- const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter);
90
- const valueFilters = props ? Object.entries(props).map(([key, value]) => payloadValueFilter(key, value)).filter(exists) : [];
91
- let cursor = suggestedIndex ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction) : await store.openCursor(suggestedIndex, direction);
92
- while (cursor && parsedOffset > 0) {
93
- cursor = await cursor.advance(parsedOffset);
94
- parsedOffset = 0;
83
+ const sequenceIndex = assertEx(store.index(IndexedDbArchivist.sequenceIndexName), () => "Failed to get sequence index");
84
+ let dbCursor = assertEx(await sequenceIndex.openCursor(null, direction), () => `Failed to get cursor [${parsedCursor}, ${cursor}]`);
85
+ if (parsedCursor !== void 0) {
86
+ let currentSequence;
87
+ while (dbCursor && currentSequence !== parsedCursor) {
88
+ currentSequence = await dbCursor.value?.sequence;
89
+ dbCursor = await dbCursor.advance(1);
90
+ }
95
91
  }
96
- while (cursor && results.length < parsedLimit) {
97
- const value = cursor.value;
92
+ while (dbCursor && results.length < parsedLimit) {
93
+ const value = dbCursor.value;
98
94
  if (value) {
99
95
  if (valueFilters.length > 0) {
100
96
  if (valueFilters.every((filter2) => filter2(value))) {
@@ -105,7 +101,7 @@ var IndexedDbPayloadDiviner = class extends PayloadDiviner {
105
101
  }
106
102
  }
107
103
  try {
108
- cursor = await cursor.continue();
104
+ dbCursor = await dbCursor.continue();
109
105
  } catch {
110
106
  break;
111
107
  }
@@ -119,31 +115,6 @@ var IndexedDbPayloadDiviner = class extends PayloadDiviner {
119
115
  await super.startHandler();
120
116
  return true;
121
117
  }
122
- getKeyRangeValue(indexName, query) {
123
- if (!indexName) return [];
124
- const indexFields = extractFields(indexName);
125
- const keyRangeValue = indexFields.map((field) => query[field]);
126
- return keyRangeValue.length === 1 ? keyRangeValue[0] : keyRangeValue;
127
- }
128
- selectBestIndex(query, store) {
129
- const { indexNames } = store;
130
- const queryKeys = new Set(Object.keys(query).map((key) => key.toLowerCase()));
131
- let bestMatch = {
132
- indexName: "",
133
- matchCount: 0
134
- };
135
- for (const indexName of indexNames) {
136
- const indexFields = extractFields(indexName);
137
- const matchCount = indexFields.filter((field) => queryKeys.has(field)).length;
138
- if (matchCount > bestMatch.matchCount) {
139
- bestMatch = {
140
- indexName,
141
- matchCount
142
- };
143
- }
144
- }
145
- return bestMatch.matchCount > 0 ? bestMatch.indexName : null;
146
- }
147
118
  /**
148
119
  * Checks that the desired DB/objectStore exists and is initialized to the correct version
149
120
  * @returns The initialized DB or undefined if it does not exist in the desired state
@@ -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` as const\nexport type IndexedDbPayloadDivinerSchema = typeof IndexedDbPayloadDivinerSchema\n","import type { IndexDescription } from '@xyo-network/archivist-model'\nimport type { 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 type { Hash } from '@xylabs/hex'\nimport type { AnyObject } from '@xylabs/object'\nimport { removeFields } from '@xylabs/object'\nimport { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'\nimport { IndexSeparator } from '@xyo-network/archivist-model'\nimport type { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'\nimport type { PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport { isPayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport type { Payload, Schema } from '@xyo-network/payload-model'\nimport type { IDBPDatabase, IDBPObjectStore } from 'idb'\nimport { openDB } from 'idb'\n\nimport { IndexedDbPayloadDivinerConfigSchema } from './Config.ts'\nimport type { 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 {\n schemas, limit, offset, order, ...props\n } = removeFields(query as unknown as TIn & { sources?: Hash[] }, [\n 'hash',\n 'schema',\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 results\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","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","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/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 type { IndexDescription } from '@xyo-network/archivist-model'\nimport type { 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 { removeFields } from '@xylabs/object'\nimport { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'\nimport type { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model'\nimport { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'\nimport type { PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport { isPayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'\nimport type {\n Payload, Schema, Sequence,\n} from '@xyo-network/payload-model'\nimport type { IDBPCursorWithValue, IDBPDatabase } from 'idb'\nimport { openDB } from 'idb'\n\nimport { IndexedDbPayloadDivinerConfigSchema } from './Config.ts'\nimport type { 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\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 /**\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)\n if (!query) return []\n const result = await this.tryUseDb(async (db) => {\n const {\n schemas, limit, cursor, order, ...props\n } = removeFields(query, ['schema'])\n const tx = db.transaction(this.storeName, 'readonly')\n const store = tx.objectStore(this.storeName)\n const results: TOut[] = []\n let parsedCursor = cursor\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 valueFilters: ValueFilter[] = Object.entries(filter)\n .map(([key, value]) => payloadValueFilter(key, value))\n .filter(exists)\n const direction: IDBCursorDirection = order === 'desc' ? 'prev' : 'next'\n\n // Iterate all records using the sequence index\n const sequenceIndex = assertEx(store.index(IndexedDbArchivist.sequenceIndexName), () => 'Failed to get sequence index')\n let dbCursor: IDBPCursorWithValue<PayloadStore, [string], string, string, 'readonly'> | null\n = assertEx(await sequenceIndex.openCursor(null, direction), () => `Failed to get cursor [${parsedCursor}, ${cursor}]`)\n\n // If a cursor was supplied\n if (parsedCursor !== undefined) {\n let currentSequence: Sequence | undefined\n // Skip records until the supplied cursor offset is reached\n while (dbCursor && currentSequence !== parsedCursor) {\n // Find the sequence of the current record\n currentSequence = await dbCursor.value?.sequence\n // Advance one record beyond the cursor\n dbCursor = await dbCursor.advance(1)\n }\n }\n\n // Collect results up to the limit\n while (dbCursor && results.length < parsedLimit) {\n const value = dbCursor.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 dbCursor = await dbCursor.continue()\n } catch {\n break\n }\n }\n await tx.done\n // Remove any metadata before returning to the client\n return results\n })\n return result ?? []\n }\n\n protected override async startHandler() {\n await super.startHandler()\n return true\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","PayloadDiviner","isPayloadDivinerQueryPayload","openDB","payloadValueFilter","key","value","undefined","payload","sourceValue","Array","isArray","containsAll","IndexedDbPayloadDiviner","PayloadDiviner","configSchemas","IndexedDbPayloadDivinerConfigSchema","defaultConfigSchema","dbName","config","archivist","IndexedDbArchivist","defaultDbName","dbVersion","defaultDbVersion","storeName","defaultStoreName","divineHandler","payloads","query","find","isPayloadDivinerQueryPayload","result","tryUseDb","db","schemas","limit","cursor","order","props","removeFields","tx","transaction","store","objectStore","results","parsedCursor","parsedLimit","assertEx","length","filterSchema","filter","schema","valueFilters","Object","entries","map","exists","direction","sequenceIndex","index","sequenceIndexName","dbCursor","openCursor","currentSequence","sequence","advance","every","push","continue","done","startHandler","tryGetInitializedDb","dbs","indexedDB","databases","dbExists","some","name","version","openDB","storeExists","objectStoreNames","contains","close","callback"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xyo-network/diviner-payload-indexeddb",
3
- "version": "3.6.0-rc.1",
3
+ "version": "3.6.0-rc.11",
4
4
  "description": "Primary SDK for using XYO Protocol 2.0",
5
5
  "homepage": "https://xyo.network",
6
6
  "bugs": {
@@ -29,27 +29,27 @@
29
29
  "module": "dist/browser/index.mjs",
30
30
  "types": "dist/browser/index.d.ts",
31
31
  "dependencies": {
32
- "@xylabs/array": "^4.4.12",
33
- "@xylabs/assert": "^4.4.12",
34
- "@xylabs/exists": "^4.4.12",
35
- "@xylabs/hex": "^4.4.12",
36
- "@xylabs/object": "^4.4.12",
37
- "@xyo-network/archivist-indexeddb": "^3.6.0-rc.1",
38
- "@xyo-network/archivist-model": "^3.6.0-rc.1",
39
- "@xyo-network/diviner-model": "^3.6.0-rc.1",
40
- "@xyo-network/diviner-payload-abstract": "^3.6.0-rc.1",
41
- "@xyo-network/diviner-payload-model": "^3.6.0-rc.1",
42
- "@xyo-network/module-model": "^3.6.0-rc.1",
43
- "@xyo-network/payload-builder": "^3.6.0-rc.1",
44
- "@xyo-network/payload-model": "^3.6.0-rc.1",
45
- "idb": "^8.0.0"
32
+ "@xylabs/array": "^4.4.24",
33
+ "@xylabs/assert": "^4.4.24",
34
+ "@xylabs/exists": "^4.4.24",
35
+ "@xylabs/object": "^4.4.24",
36
+ "@xyo-network/archivist-indexeddb": "^3.6.0-rc.11",
37
+ "@xyo-network/archivist-model": "^3.6.0-rc.11",
38
+ "@xyo-network/diviner-model": "^3.6.0-rc.11",
39
+ "@xyo-network/diviner-payload-abstract": "^3.6.0-rc.11",
40
+ "@xyo-network/diviner-payload-model": "^3.6.0-rc.11",
41
+ "@xyo-network/module-model": "^3.6.0-rc.11",
42
+ "@xyo-network/payload-model": "^3.6.0-rc.11",
43
+ "idb": "^8.0.1"
46
44
  },
47
45
  "devDependencies": {
48
- "@xylabs/ts-scripts-yarn3": "^4.2.4",
49
- "@xylabs/tsconfig": "^4.2.4",
50
- "@xylabs/vitest-extended": "^4.4.12",
51
- "@xyo-network/archivist-indexeddb": "^3.6.0-rc.1",
52
- "@xyo-network/node-memory": "^3.6.0-rc.1",
46
+ "@xylabs/delay": "^4.4.24",
47
+ "@xylabs/ts-scripts-yarn3": "^4.2.6",
48
+ "@xylabs/tsconfig": "^4.2.6",
49
+ "@xylabs/vitest-extended": "^4.4.24",
50
+ "@xyo-network/archivist-indexeddb": "^3.6.0-rc.11",
51
+ "@xyo-network/node-memory": "^3.6.0-rc.11",
52
+ "@xyo-network/payload-builder": "^3.6.0-rc.11",
53
53
  "fake-indexeddb": "^6.0.0",
54
54
  "typescript": "^5.7.2",
55
55
  "vitest": "^2.1.8"
package/src/Diviner.ts CHANGED
@@ -1,17 +1,16 @@
1
1
  import { containsAll } from '@xylabs/array'
2
2
  import { assertEx } from '@xylabs/assert'
3
3
  import { exists } from '@xylabs/exists'
4
- import type { Hash } from '@xylabs/hex'
5
- import type { AnyObject } from '@xylabs/object'
6
4
  import { removeFields } from '@xylabs/object'
7
5
  import { IndexedDbArchivist } from '@xyo-network/archivist-indexeddb'
8
- import { IndexSeparator } from '@xyo-network/archivist-model'
9
6
  import type { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model'
10
7
  import { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'
11
8
  import type { PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'
12
9
  import { isPayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'
13
- import type { Payload, Schema } from '@xyo-network/payload-model'
14
- import type { IDBPDatabase, IDBPObjectStore } from 'idb'
10
+ import type {
11
+ Payload, Schema, Sequence,
12
+ } from '@xyo-network/payload-model'
13
+ import type { IDBPCursorWithValue, IDBPDatabase } from 'idb'
15
14
  import { openDB } from 'idb'
16
15
 
17
16
  import { IndexedDbPayloadDivinerConfigSchema } from './Config.ts'
@@ -35,14 +34,6 @@ const payloadValueFilter = (key: keyof AnyPayload, value?: unknown | unknown[]):
35
34
  }
36
35
  }
37
36
 
38
- // Function to extract fields from an index name
39
- const extractFields = (indexName: string): string[] => {
40
- return indexName
41
- .slice(3)
42
- .split(IndexSeparator)
43
- .map(field => field.toLowerCase())
44
- }
45
-
46
37
  export class IndexedDbPayloadDiviner<
47
38
  TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams,
48
39
  TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload,
@@ -56,8 +47,6 @@ export class IndexedDbPayloadDiviner<
56
47
  static override readonly configSchemas: Schema[] = [...super.configSchemas, IndexedDbPayloadDivinerConfigSchema]
57
48
  static override readonly defaultConfigSchema: Schema = IndexedDbPayloadDivinerConfigSchema
58
49
 
59
- private _db: IDBPDatabase<PayloadStore> | undefined
60
-
61
50
  /**
62
51
  * The database name. If not supplied via config, it defaults
63
52
  * to the archivist's name and if archivist's name is not supplied,
@@ -85,47 +74,45 @@ export class IndexedDbPayloadDiviner<
85
74
  }
86
75
 
87
76
  protected override async divineHandler(payloads?: TIn[]): Promise<TOut[]> {
88
- const query = payloads?.find(isPayloadDivinerQueryPayload) as TIn
77
+ const query = payloads?.find(isPayloadDivinerQueryPayload)
89
78
  if (!query) return []
90
79
  const result = await this.tryUseDb(async (db) => {
91
80
  const {
92
- schemas, limit, offset, order, ...props
93
- } = removeFields(query as unknown as TIn & { sources?: Hash[] }, [
94
- 'hash',
95
- 'schema',
96
- ])
81
+ schemas, limit, cursor, order, ...props
82
+ } = removeFields(query, ['schema'])
97
83
  const tx = db.transaction(this.storeName, 'readonly')
98
84
  const store = tx.objectStore(this.storeName)
99
85
  const results: TOut[] = []
100
- let parsedOffset = offset ?? 0
86
+ let parsedCursor = cursor
101
87
  const parsedLimit = limit ?? 10
102
88
  assertEx((schemas?.length ?? 1) === 1, () => 'IndexedDbPayloadDiviner: Only one filter schema supported')
103
89
  const filterSchema = schemas?.[0]
104
90
  const filter = filterSchema ? { schema: filterSchema, ...props } : { ...props }
91
+ const valueFilters: ValueFilter[] = Object.entries(filter)
92
+ .map(([key, value]) => payloadValueFilter(key, value))
93
+ .filter(exists)
105
94
  const direction: IDBCursorDirection = order === 'desc' ? 'prev' : 'next'
106
- const suggestedIndex = this.selectBestIndex(filter, store)
107
- const keyRangeValue = this.getKeyRangeValue(suggestedIndex, filter)
108
- const valueFilters: ValueFilter[]
109
- = props
110
- ? Object.entries(props)
111
- .map(([key, value]) => payloadValueFilter(key, value))
112
- .filter(exists)
113
- : []
114
- let cursor
115
- = suggestedIndex
116
- // Conditionally filter on schemas
117
- ? await store.index(suggestedIndex).openCursor(IDBKeyRange.only(keyRangeValue), direction)
118
- // Just iterate all records
119
- : await store.openCursor(suggestedIndex, direction)
120
-
121
- // Skip records until the offset is reached
122
- while (cursor && parsedOffset > 0) {
123
- cursor = await cursor.advance(parsedOffset)
124
- parsedOffset = 0 // Reset offset after skipping
95
+
96
+ // Iterate all records using the sequence index
97
+ const sequenceIndex = assertEx(store.index(IndexedDbArchivist.sequenceIndexName), () => 'Failed to get sequence index')
98
+ let dbCursor: IDBPCursorWithValue<PayloadStore, [string], string, string, 'readonly'> | null
99
+ = assertEx(await sequenceIndex.openCursor(null, direction), () => `Failed to get cursor [${parsedCursor}, ${cursor}]`)
100
+
101
+ // If a cursor was supplied
102
+ if (parsedCursor !== undefined) {
103
+ let currentSequence: Sequence | undefined
104
+ // Skip records until the supplied cursor offset is reached
105
+ while (dbCursor && currentSequence !== parsedCursor) {
106
+ // Find the sequence of the current record
107
+ currentSequence = await dbCursor.value?.sequence
108
+ // Advance one record beyond the cursor
109
+ dbCursor = await dbCursor.advance(1)
110
+ }
125
111
  }
112
+
126
113
  // Collect results up to the limit
127
- while (cursor && results.length < parsedLimit) {
128
- const value = cursor.value
114
+ while (dbCursor && results.length < parsedLimit) {
115
+ const value = dbCursor.value
129
116
  if (value) {
130
117
  // If we're filtering on more than just the schema
131
118
  if (valueFilters.length > 0) {
@@ -140,7 +127,7 @@ export class IndexedDbPayloadDiviner<
140
127
  }
141
128
  }
142
129
  try {
143
- cursor = await cursor.continue()
130
+ dbCursor = await dbCursor.continue()
144
131
  } catch {
145
132
  break
146
133
  }
@@ -157,37 +144,6 @@ export class IndexedDbPayloadDiviner<
157
144
  return true
158
145
  }
159
146
 
160
- private getKeyRangeValue(indexName: string | null, query: AnyObject): unknown | unknown[] {
161
- if (!indexName) return []
162
-
163
- // Extracting the relevant fields from the index name
164
- const indexFields = extractFields(indexName)
165
-
166
- // Collecting the values for these fields from the query object
167
- const keyRangeValue = indexFields.map(field => query[field as keyof AnyObject])
168
- return keyRangeValue.length === 1 ? keyRangeValue[0] : keyRangeValue
169
- }
170
-
171
- private selectBestIndex(query: AnyObject, store: IDBPObjectStore<PayloadStore>): string | null {
172
- // List of available indexes
173
- const { indexNames } = store
174
-
175
- // Convert query object keys to a set for easier comparison
176
- const queryKeys = new Set(Object.keys(query).map(key => key.toLowerCase()))
177
-
178
- // Find the best matching index
179
- let bestMatch: { indexName: string; matchCount: number } = { indexName: '', matchCount: 0 }
180
-
181
- for (const indexName of indexNames) {
182
- const indexFields = extractFields(indexName)
183
- const matchCount = indexFields.filter(field => queryKeys.has(field)).length
184
- if (matchCount > bestMatch.matchCount) {
185
- bestMatch = { indexName, matchCount }
186
- }
187
- }
188
- return bestMatch.matchCount > 0 ? bestMatch.indexName : null
189
- }
190
-
191
147
  /**
192
148
  * Checks that the desired DB/objectStore exists and is initialized to the correct version
193
149
  * @returns The initialized DB or undefined if it does not exist in the desired state