@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.
- package/LICENSE +165 -0
- package/README.md +13 -0
- package/dist/browser/Config.d.cts +30 -0
- package/dist/browser/Config.d.cts.map +1 -0
- package/dist/browser/Config.d.mts +30 -0
- package/dist/browser/Config.d.mts.map +1 -0
- package/dist/browser/Config.d.ts +30 -0
- package/dist/browser/Config.d.ts.map +1 -0
- package/dist/browser/Diviner.d.cts +38 -0
- package/dist/browser/Diviner.d.cts.map +1 -0
- package/dist/browser/Diviner.d.mts +38 -0
- package/dist/browser/Diviner.d.mts.map +1 -0
- package/dist/browser/Diviner.d.ts +38 -0
- package/dist/browser/Diviner.d.ts.map +1 -0
- package/dist/browser/Params.d.cts +5 -0
- package/dist/browser/Params.d.cts.map +1 -0
- package/dist/browser/Params.d.mts +5 -0
- package/dist/browser/Params.d.mts.map +1 -0
- package/dist/browser/Params.d.ts +5 -0
- package/dist/browser/Params.d.ts.map +1 -0
- package/dist/browser/Schema.d.cts +3 -0
- package/dist/browser/Schema.d.cts.map +1 -0
- package/dist/browser/Schema.d.mts +3 -0
- package/dist/browser/Schema.d.mts.map +1 -0
- package/dist/browser/Schema.d.ts +3 -0
- package/dist/browser/Schema.d.ts.map +1 -0
- package/dist/browser/index.cjs +156 -0
- package/dist/browser/index.cjs.map +1 -0
- package/dist/browser/index.d.cts +5 -0
- package/dist/browser/index.d.cts.map +1 -0
- package/dist/browser/index.d.mts +5 -0
- package/dist/browser/index.d.mts.map +1 -0
- package/dist/browser/index.d.ts +5 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +135 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/node/Config.d.cts +30 -0
- package/dist/node/Config.d.cts.map +1 -0
- package/dist/node/Config.d.mts +30 -0
- package/dist/node/Config.d.mts.map +1 -0
- package/dist/node/Config.d.ts +30 -0
- package/dist/node/Config.d.ts.map +1 -0
- package/dist/node/Diviner.d.cts +38 -0
- package/dist/node/Diviner.d.cts.map +1 -0
- package/dist/node/Diviner.d.mts +38 -0
- package/dist/node/Diviner.d.mts.map +1 -0
- package/dist/node/Diviner.d.ts +38 -0
- package/dist/node/Diviner.d.ts.map +1 -0
- package/dist/node/Params.d.cts +5 -0
- package/dist/node/Params.d.cts.map +1 -0
- package/dist/node/Params.d.mts +5 -0
- package/dist/node/Params.d.mts.map +1 -0
- package/dist/node/Params.d.ts +5 -0
- package/dist/node/Params.d.ts.map +1 -0
- package/dist/node/Schema.d.cts +3 -0
- package/dist/node/Schema.d.cts.map +1 -0
- package/dist/node/Schema.d.mts +3 -0
- package/dist/node/Schema.d.mts.map +1 -0
- package/dist/node/Schema.d.ts +3 -0
- package/dist/node/Schema.d.ts.map +1 -0
- package/dist/node/index.cjs +171 -0
- package/dist/node/index.cjs.map +1 -0
- package/dist/node/index.d.cts +5 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.mts +5 -0
- package/dist/node/index.d.mts.map +1 -0
- package/dist/node/index.d.ts +5 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +144 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +76 -0
- package/src/Config.ts +33 -0
- package/src/Diviner.ts +156 -0
- package/src/Params.ts +6 -0
- package/src/Schema.ts +4 -0
- package/src/index.ts +4 -0
- 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
package/src/index.ts
ADDED