@xyo-network/diviner-payload-indexeddb 2.87.0-rc.2

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.
Files changed (77) hide show
  1. package/LICENSE +165 -0
  2. package/README.md +13 -0
  3. package/dist/browser/Config.d.cts +30 -0
  4. package/dist/browser/Config.d.cts.map +1 -0
  5. package/dist/browser/Config.d.mts +30 -0
  6. package/dist/browser/Config.d.mts.map +1 -0
  7. package/dist/browser/Config.d.ts +30 -0
  8. package/dist/browser/Config.d.ts.map +1 -0
  9. package/dist/browser/Diviner.d.cts +38 -0
  10. package/dist/browser/Diviner.d.cts.map +1 -0
  11. package/dist/browser/Diviner.d.mts +38 -0
  12. package/dist/browser/Diviner.d.mts.map +1 -0
  13. package/dist/browser/Diviner.d.ts +38 -0
  14. package/dist/browser/Diviner.d.ts.map +1 -0
  15. package/dist/browser/Params.d.cts +5 -0
  16. package/dist/browser/Params.d.cts.map +1 -0
  17. package/dist/browser/Params.d.mts +5 -0
  18. package/dist/browser/Params.d.mts.map +1 -0
  19. package/dist/browser/Params.d.ts +5 -0
  20. package/dist/browser/Params.d.ts.map +1 -0
  21. package/dist/browser/Schema.d.cts +3 -0
  22. package/dist/browser/Schema.d.cts.map +1 -0
  23. package/dist/browser/Schema.d.mts +3 -0
  24. package/dist/browser/Schema.d.mts.map +1 -0
  25. package/dist/browser/Schema.d.ts +3 -0
  26. package/dist/browser/Schema.d.ts.map +1 -0
  27. package/dist/browser/index.cjs +156 -0
  28. package/dist/browser/index.cjs.map +1 -0
  29. package/dist/browser/index.d.cts +5 -0
  30. package/dist/browser/index.d.cts.map +1 -0
  31. package/dist/browser/index.d.mts +5 -0
  32. package/dist/browser/index.d.mts.map +1 -0
  33. package/dist/browser/index.d.ts +5 -0
  34. package/dist/browser/index.d.ts.map +1 -0
  35. package/dist/browser/index.js +135 -0
  36. package/dist/browser/index.js.map +1 -0
  37. package/dist/node/Config.d.cts +30 -0
  38. package/dist/node/Config.d.cts.map +1 -0
  39. package/dist/node/Config.d.mts +30 -0
  40. package/dist/node/Config.d.mts.map +1 -0
  41. package/dist/node/Config.d.ts +30 -0
  42. package/dist/node/Config.d.ts.map +1 -0
  43. package/dist/node/Diviner.d.cts +38 -0
  44. package/dist/node/Diviner.d.cts.map +1 -0
  45. package/dist/node/Diviner.d.mts +38 -0
  46. package/dist/node/Diviner.d.mts.map +1 -0
  47. package/dist/node/Diviner.d.ts +38 -0
  48. package/dist/node/Diviner.d.ts.map +1 -0
  49. package/dist/node/Params.d.cts +5 -0
  50. package/dist/node/Params.d.cts.map +1 -0
  51. package/dist/node/Params.d.mts +5 -0
  52. package/dist/node/Params.d.mts.map +1 -0
  53. package/dist/node/Params.d.ts +5 -0
  54. package/dist/node/Params.d.ts.map +1 -0
  55. package/dist/node/Schema.d.cts +3 -0
  56. package/dist/node/Schema.d.cts.map +1 -0
  57. package/dist/node/Schema.d.mts +3 -0
  58. package/dist/node/Schema.d.mts.map +1 -0
  59. package/dist/node/Schema.d.ts +3 -0
  60. package/dist/node/Schema.d.ts.map +1 -0
  61. package/dist/node/index.cjs +171 -0
  62. package/dist/node/index.cjs.map +1 -0
  63. package/dist/node/index.d.cts +5 -0
  64. package/dist/node/index.d.cts.map +1 -0
  65. package/dist/node/index.d.mts +5 -0
  66. package/dist/node/index.d.mts.map +1 -0
  67. package/dist/node/index.d.ts +5 -0
  68. package/dist/node/index.d.ts.map +1 -0
  69. package/dist/node/index.js +144 -0
  70. package/dist/node/index.js.map +1 -0
  71. package/package.json +76 -0
  72. package/src/Config.ts +33 -0
  73. package/src/Diviner.ts +156 -0
  74. package/src/Params.ts +6 -0
  75. package/src/Schema.ts +4 -0
  76. package/src/index.ts +4 -0
  77. package/typedoc.json +5 -0
package/src/Diviner.ts ADDED
@@ -0,0 +1,156 @@
1
+ import { assertEx } from '@xylabs/assert'
2
+ import { IndexSeparator } from '@xyo-network/archivist-model'
3
+ import { DivinerModule, DivinerModuleEventData } from '@xyo-network/diviner-model'
4
+ import { PayloadDiviner } from '@xyo-network/diviner-payload-abstract'
5
+ import { isPayloadDivinerQueryPayload, PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'
6
+ import { PayloadHasher } from '@xyo-network/hash'
7
+ import { AnyObject } from '@xyo-network/object'
8
+ import { Payload } from '@xyo-network/payload-model'
9
+ import { IDBPDatabase, IDBPObjectStore, openDB } from 'idb'
10
+
11
+ import { IndexedDbPayloadDivinerConfigSchema } from './Config'
12
+ import { IndexedDbPayloadDivinerParams } from './Params'
13
+
14
+ interface PayloadStore {
15
+ [s: string]: Payload
16
+ }
17
+
18
+ export class IndexedDbPayloadDiviner<
19
+ TParams extends IndexedDbPayloadDivinerParams = IndexedDbPayloadDivinerParams,
20
+ TIn extends PayloadDivinerQueryPayload = PayloadDivinerQueryPayload,
21
+ TOut extends Payload = Payload,
22
+ TEventData extends DivinerModuleEventData<DivinerModule<TParams>, TIn, TOut> = DivinerModuleEventData<DivinerModule<TParams>, TIn, TOut>,
23
+ > extends PayloadDiviner<TParams, TIn, TOut, TEventData> {
24
+ static override configSchemas = [IndexedDbPayloadDivinerConfigSchema]
25
+ static defaultDbName = 'archivist'
26
+ static defaultDbVersion = 1
27
+ static defaultStoreName = 'payloads'
28
+
29
+ private _db: IDBPDatabase<PayloadStore> | undefined
30
+
31
+ /**
32
+ * The database name. If not supplied via config it defaults to
33
+ * `archivist`. This behavior biases towards a single, isolated
34
+ * DB per archivist which seems to make the most sense for 99% of
35
+ * use cases.
36
+ */
37
+ get dbName() {
38
+ return this.config?.dbName ?? IndexedDbPayloadDiviner.defaultDbName
39
+ }
40
+
41
+ /**
42
+ * The database version. If not supplied via config, it defaults to 1.
43
+ */
44
+ get dbVersion() {
45
+ return this.config?.dbVersion ?? IndexedDbPayloadDiviner.defaultDbVersion
46
+ }
47
+
48
+ /**
49
+ * The database indexes.
50
+ */
51
+ get indexes() {
52
+ return this.config?.storage?.indexes ?? []
53
+ }
54
+
55
+ /**
56
+ * The name of the object store. If not supplied via config, it defaults
57
+ * to `payloads`.
58
+ */
59
+ get storeName() {
60
+ return this.config?.storeName ?? IndexedDbPayloadDiviner.defaultStoreName
61
+ }
62
+
63
+ private get db(): IDBPDatabase<PayloadStore> {
64
+ return assertEx(this._db, 'DB not initialized')
65
+ }
66
+
67
+ protected override async divineHandler(payloads?: TIn[]): Promise<TOut[]> {
68
+ const query = assertEx(payloads?.filter(isPayloadDivinerQueryPayload)?.pop(), 'Missing query payload')
69
+ if (!query) return []
70
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
71
+ const { schemas, limit, offset, hash, order, schema: _schema, sources, ...props } = query as unknown as TIn & { sources?: string[] }
72
+ const tx = this.db.transaction(this.storeName, 'readonly')
73
+ const store = tx.objectStore(this.storeName)
74
+ const results: TOut[] = []
75
+ let parsedOffset = offset ?? 0
76
+ const parsedLimit = limit ?? 10
77
+ assertEx((schemas?.length ?? 1) === 1, 'IndexedDbPayloadDiviner: Only one filter schema supported')
78
+ const filterSchema = schemas?.[0]
79
+ const filter = filterSchema ? { schema: filterSchema, ...props } : { ...props }
80
+ const direction: IDBCursorDirection = order === 'desc' ? 'prev' : 'next'
81
+ const suggestedIndex = this.selectBestIndex(filter, store)
82
+ const filterValues = this.getKeyValuesFromQuery(suggestedIndex, filter)
83
+ let cursor = suggestedIndex
84
+ ? // Conditionally filter on schemas
85
+ await store.index(suggestedIndex).openCursor(IDBKeyRange.only(filterValues.length === 1 ? filterValues[0] : filterValues), direction)
86
+ : // Just iterate all records
87
+ await store.openCursor(suggestedIndex, direction)
88
+
89
+ // Skip records until the offset is reached
90
+ while (cursor && parsedOffset > 0) {
91
+ cursor = await cursor.advance(parsedOffset)
92
+ parsedOffset = 0 // Reset offset after skipping
93
+ }
94
+ // Collect results up to the limit
95
+ while (cursor && results.length < parsedLimit) {
96
+ results.push(cursor.value)
97
+ cursor = await cursor.continue()
98
+ }
99
+ await tx.done
100
+ // Remove any metadata before returning to the client
101
+ return results.map((payload) => PayloadHasher.jsonPayload(payload))
102
+ }
103
+
104
+ protected override async startHandler() {
105
+ await super.startHandler()
106
+ // NOTE: We could defer this creation to first access but we
107
+ // want to fail fast here in case something is wrong
108
+ this._db = await openDB<PayloadStore>(this.dbName, this.dbVersion)
109
+ return true
110
+ }
111
+
112
+ private getKeyValuesFromQuery(indexName: string | null, query: AnyObject): unknown[] {
113
+ if (!indexName) return []
114
+ // Function to extract fields from an index name
115
+ const extractFields = (indexName: string): string[] => {
116
+ return indexName
117
+ .slice(3)
118
+ .split(IndexSeparator)
119
+ .map((field) => field.toLowerCase())
120
+ }
121
+
122
+ // Extracting the relevant fields from the index name
123
+ const indexFields = extractFields(indexName)
124
+
125
+ // Collecting the values for these fields from the query object
126
+ return indexFields.map((field) => query[field as keyof AnyObject])
127
+ }
128
+
129
+ private selectBestIndex(query: AnyObject, store: IDBPObjectStore<PayloadStore>): string | null {
130
+ // List of available indexes
131
+ const { indexNames } = store
132
+
133
+ // Function to extract fields from an index name
134
+ const extractFields = (indexName: string): string[] => {
135
+ return indexName
136
+ .slice(3)
137
+ .split(IndexSeparator)
138
+ .map((field) => field.toLowerCase())
139
+ }
140
+
141
+ // Convert query object keys to a set for easier comparison
142
+ const queryKeys = new Set(Object.keys(query).map((key) => key.toLowerCase()))
143
+
144
+ // Find the best matching index
145
+ let bestMatch: { indexName: string; matchCount: number } = { indexName: '', matchCount: 0 }
146
+
147
+ for (const indexName of indexNames) {
148
+ const indexFields = extractFields(indexName)
149
+ const matchCount = indexFields.filter((field) => queryKeys.has(field)).length
150
+ if (matchCount > bestMatch.matchCount) {
151
+ bestMatch = { indexName, matchCount }
152
+ }
153
+ }
154
+ return bestMatch.matchCount > 0 ? bestMatch.indexName : null
155
+ }
156
+ }
package/src/Params.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { DivinerParams } from '@xyo-network/diviner-model'
2
+ import { AnyConfigSchema } from '@xyo-network/module-model'
3
+
4
+ import { IndexedDbPayloadDivinerConfig } from './Config'
5
+
6
+ export type IndexedDbPayloadDivinerParams = DivinerParams<AnyConfigSchema<IndexedDbPayloadDivinerConfig>>
package/src/Schema.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { PayloadDivinerSchema } from '@xyo-network/diviner-payload-model'
2
+
3
+ export const IndexedDbPayloadDivinerSchema = `${PayloadDivinerSchema}.indexeddb`
4
+ export type IndexedDbPayloadDivinerSchema = typeof IndexedDbPayloadDivinerSchema
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './Config'
2
+ export * from './Diviner'
3
+ export * from './Params'
4
+ export * from './Schema'
package/typedoc.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://typedoc.org/schema.json",
3
+ "entryPoints": ["src/index.ts"],
4
+ "tsconfig": "./tsconfig.typedoc.json"
5
+ }