@workglow/indexeddb 0.2.28
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/dist/job-queue/IndexedDbQueueStorage.d.ts +167 -0
- package/dist/job-queue/IndexedDbQueueStorage.d.ts.map +1 -0
- package/dist/job-queue/IndexedDbRateLimiterStorage.d.ts +79 -0
- package/dist/job-queue/IndexedDbRateLimiterStorage.d.ts.map +1 -0
- package/dist/job-queue/browser.d.ts +7 -0
- package/dist/job-queue/browser.d.ts.map +1 -0
- package/dist/job-queue/browser.js +1195 -0
- package/dist/job-queue/browser.js.map +12 -0
- package/dist/job-queue/bun.d.ts +7 -0
- package/dist/job-queue/bun.d.ts.map +1 -0
- package/dist/job-queue/common.d.ts +8 -0
- package/dist/job-queue/common.d.ts.map +1 -0
- package/dist/job-queue/node.d.ts +7 -0
- package/dist/job-queue/node.d.ts.map +1 -0
- package/dist/job-queue/node.js +1195 -0
- package/dist/job-queue/node.js.map +12 -0
- package/dist/storage/IndexedDbKvStorage.d.ts +26 -0
- package/dist/storage/IndexedDbKvStorage.d.ts.map +1 -0
- package/dist/storage/IndexedDbTable.d.ts +40 -0
- package/dist/storage/IndexedDbTable.d.ts.map +1 -0
- package/dist/storage/IndexedDbTabularStorage.d.ts +198 -0
- package/dist/storage/IndexedDbTabularStorage.d.ts.map +1 -0
- package/dist/storage/IndexedDbVectorStorage.d.ts +52 -0
- package/dist/storage/IndexedDbVectorStorage.d.ts.map +1 -0
- package/dist/storage/browser.d.ts +7 -0
- package/dist/storage/browser.d.ts.map +1 -0
- package/dist/storage/browser.js +1182 -0
- package/dist/storage/browser.js.map +13 -0
- package/dist/storage/bun.d.ts +7 -0
- package/dist/storage/bun.d.ts.map +1 -0
- package/dist/storage/common.d.ts +10 -0
- package/dist/storage/common.d.ts.map +1 -0
- package/dist/storage/node.d.ts +7 -0
- package/dist/storage/node.d.ts.map +1 -0
- package/dist/storage/node.js +1182 -0
- package/dist/storage/node.js.map +13 -0
- package/package.json +74 -0
|
@@ -0,0 +1,1182 @@
|
|
|
1
|
+
// src/storage/IndexedDbTable.ts
|
|
2
|
+
import { deepEqual } from "@workglow/util";
|
|
3
|
+
var METADATA_STORE_NAME = "__schema_metadata__";
|
|
4
|
+
async function saveSchemaMetadata(db, tableName, snapshot) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
try {
|
|
7
|
+
const transaction = db.transaction(METADATA_STORE_NAME, "readwrite");
|
|
8
|
+
const store = transaction.objectStore(METADATA_STORE_NAME);
|
|
9
|
+
const request = store.put({ ...snapshot, tableName }, tableName);
|
|
10
|
+
request.onsuccess = () => resolve();
|
|
11
|
+
request.onerror = () => reject(request.error);
|
|
12
|
+
transaction.onerror = () => reject(transaction.error);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
resolve();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
async function openIndexedDbTable(tableName, version, upgradeNeededCallback) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const openRequest = indexedDB.open(tableName, version);
|
|
21
|
+
openRequest.onsuccess = (event) => {
|
|
22
|
+
const db = event.target.result;
|
|
23
|
+
db.onversionchange = () => {
|
|
24
|
+
db.close();
|
|
25
|
+
};
|
|
26
|
+
resolve(db);
|
|
27
|
+
};
|
|
28
|
+
openRequest.onupgradeneeded = (event) => {
|
|
29
|
+
if (upgradeNeededCallback) {
|
|
30
|
+
upgradeNeededCallback(event);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
openRequest.onerror = () => {
|
|
34
|
+
const error = openRequest.error;
|
|
35
|
+
if (error && error.name === "VersionError") {
|
|
36
|
+
reject(new Error(`Database ${tableName} exists at a higher version. Cannot open at version ${version || "current"}.`));
|
|
37
|
+
} else {
|
|
38
|
+
reject(error);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
openRequest.onblocked = () => {
|
|
42
|
+
reject(new Error(`Database ${tableName} is blocked. Close all other tabs using this database.`));
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function deleteIndexedDbTable(tableName) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const deleteRequest = indexedDB.deleteDatabase(tableName);
|
|
49
|
+
deleteRequest.onsuccess = () => resolve();
|
|
50
|
+
deleteRequest.onerror = () => reject(deleteRequest.error);
|
|
51
|
+
deleteRequest.onblocked = () => {
|
|
52
|
+
reject(new Error(`Cannot delete database ${tableName}. Close all other tabs using this database.`));
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function compareSchemas(store, expectedPrimaryKey, expectedIndexes) {
|
|
57
|
+
const diff = {
|
|
58
|
+
indexesToAdd: [],
|
|
59
|
+
indexesToRemove: [],
|
|
60
|
+
indexesToModify: [],
|
|
61
|
+
primaryKeyChanged: false,
|
|
62
|
+
needsObjectStoreRecreation: false
|
|
63
|
+
};
|
|
64
|
+
const actualKeyPath = store.keyPath;
|
|
65
|
+
const normalizedExpected = Array.isArray(expectedPrimaryKey) ? expectedPrimaryKey : expectedPrimaryKey;
|
|
66
|
+
const normalizedActual = Array.isArray(actualKeyPath) ? actualKeyPath : actualKeyPath;
|
|
67
|
+
if (!deepEqual(normalizedExpected, normalizedActual)) {
|
|
68
|
+
diff.primaryKeyChanged = true;
|
|
69
|
+
diff.needsObjectStoreRecreation = true;
|
|
70
|
+
return diff;
|
|
71
|
+
}
|
|
72
|
+
const existingIndexes = new Map;
|
|
73
|
+
for (let i = 0;i < store.indexNames.length; i++) {
|
|
74
|
+
const indexName = store.indexNames[i];
|
|
75
|
+
existingIndexes.set(indexName, store.index(indexName));
|
|
76
|
+
}
|
|
77
|
+
for (const expectedIdx of expectedIndexes) {
|
|
78
|
+
const existingIdx = existingIndexes.get(expectedIdx.name);
|
|
79
|
+
if (!existingIdx) {
|
|
80
|
+
diff.indexesToAdd.push(expectedIdx);
|
|
81
|
+
} else {
|
|
82
|
+
const expectedKeyPath = Array.isArray(expectedIdx.keyPath) ? expectedIdx.keyPath : [expectedIdx.keyPath];
|
|
83
|
+
const actualKeyPath2 = Array.isArray(existingIdx.keyPath) ? existingIdx.keyPath : [existingIdx.keyPath];
|
|
84
|
+
const keyPathChanged = !deepEqual(expectedKeyPath, actualKeyPath2);
|
|
85
|
+
const uniqueChanged = existingIdx.unique !== (expectedIdx.options?.unique ?? false);
|
|
86
|
+
const multiEntryChanged = existingIdx.multiEntry !== (expectedIdx.options?.multiEntry ?? false);
|
|
87
|
+
if (keyPathChanged || uniqueChanged || multiEntryChanged) {
|
|
88
|
+
diff.indexesToModify.push(expectedIdx);
|
|
89
|
+
}
|
|
90
|
+
existingIndexes.delete(expectedIdx.name);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
diff.indexesToRemove = Array.from(existingIndexes.keys());
|
|
94
|
+
return diff;
|
|
95
|
+
}
|
|
96
|
+
async function readAllData(store) {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const request = store.getAll();
|
|
99
|
+
request.onsuccess = () => resolve(request.result || []);
|
|
100
|
+
request.onerror = () => reject(request.error);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async function performIncrementalMigration(db, tableName, diff, options = {}) {
|
|
104
|
+
const currentVersion = db.version;
|
|
105
|
+
const newVersion = currentVersion + 1;
|
|
106
|
+
db.close();
|
|
107
|
+
options.onMigrationProgress?.(`Migrating ${tableName} from version ${currentVersion} to ${newVersion}...`, 0);
|
|
108
|
+
return openIndexedDbTable(tableName, newVersion, (event) => {
|
|
109
|
+
const transaction = event.target.transaction;
|
|
110
|
+
const store = transaction.objectStore(tableName);
|
|
111
|
+
for (const indexName of diff.indexesToRemove) {
|
|
112
|
+
options.onMigrationProgress?.(`Removing index: ${indexName}`, 0.2);
|
|
113
|
+
store.deleteIndex(indexName);
|
|
114
|
+
}
|
|
115
|
+
for (const indexDef of diff.indexesToModify) {
|
|
116
|
+
options.onMigrationProgress?.(`Updating index: ${indexDef.name}`, 0.4);
|
|
117
|
+
if (store.indexNames.contains(indexDef.name)) {
|
|
118
|
+
store.deleteIndex(indexDef.name);
|
|
119
|
+
}
|
|
120
|
+
store.createIndex(indexDef.name, indexDef.keyPath, indexDef.options);
|
|
121
|
+
}
|
|
122
|
+
for (const indexDef of diff.indexesToAdd) {
|
|
123
|
+
options.onMigrationProgress?.(`Adding index: ${indexDef.name}`, 0.6);
|
|
124
|
+
store.createIndex(indexDef.name, indexDef.keyPath, indexDef.options);
|
|
125
|
+
}
|
|
126
|
+
options.onMigrationProgress?.(`Migration complete`, 1);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
async function performDestructiveMigration(db, tableName, primaryKey, expectedIndexes, options = {}, autoIncrement = false) {
|
|
130
|
+
if (!options.allowDestructiveMigration) {
|
|
131
|
+
throw new Error(`Destructive migration required for ${tableName} but not allowed. ` + `Primary key has changed. Set allowDestructiveMigration=true to proceed with data loss, ` + `or provide a dataTransformer to migrate data.`);
|
|
132
|
+
}
|
|
133
|
+
const currentVersion = db.version;
|
|
134
|
+
const newVersion = currentVersion + 1;
|
|
135
|
+
options.onMigrationProgress?.(`Performing destructive migration of ${tableName}. Reading existing data...`, 0);
|
|
136
|
+
let existingData = [];
|
|
137
|
+
try {
|
|
138
|
+
const transaction = db.transaction(tableName, "readonly");
|
|
139
|
+
const store = transaction.objectStore(tableName);
|
|
140
|
+
existingData = await readAllData(store);
|
|
141
|
+
options.onMigrationProgress?.(`Read ${existingData.length} records`, 0.3);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
options.onMigrationWarning?.(`Failed to read existing data during migration: ${err}`, err);
|
|
144
|
+
}
|
|
145
|
+
db.close();
|
|
146
|
+
if (options.dataTransformer && existingData.length > 0) {
|
|
147
|
+
options.onMigrationProgress?.(`Transforming ${existingData.length} records...`, 0.4);
|
|
148
|
+
try {
|
|
149
|
+
const transformed = [];
|
|
150
|
+
for (let i = 0;i < existingData.length; i++) {
|
|
151
|
+
const record = existingData[i];
|
|
152
|
+
const transformedRecord = await options.dataTransformer(record);
|
|
153
|
+
if (transformedRecord !== undefined && transformedRecord !== null) {
|
|
154
|
+
transformed.push(transformedRecord);
|
|
155
|
+
}
|
|
156
|
+
if (i % 100 === 0) {
|
|
157
|
+
options.onMigrationProgress?.(`Transformed ${i}/${existingData.length} records`, 0.4 + i / existingData.length * 0.3);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
existingData = transformed;
|
|
161
|
+
options.onMigrationProgress?.(`Transformation complete: ${existingData.length} records`, 0.7);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
options.onMigrationWarning?.(`Data transformation failed: ${err}. Some data may be lost.`, err);
|
|
164
|
+
existingData = [];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
options.onMigrationProgress?.(`Recreating object store...`, 0.75);
|
|
168
|
+
const newDb = await openIndexedDbTable(tableName, newVersion, (event) => {
|
|
169
|
+
const db2 = event.target.result;
|
|
170
|
+
if (db2.objectStoreNames.contains(tableName)) {
|
|
171
|
+
db2.deleteObjectStore(tableName);
|
|
172
|
+
}
|
|
173
|
+
const store = db2.createObjectStore(tableName, { keyPath: primaryKey, autoIncrement });
|
|
174
|
+
for (const idx of expectedIndexes) {
|
|
175
|
+
store.createIndex(idx.name, idx.keyPath, idx.options);
|
|
176
|
+
}
|
|
177
|
+
if (existingData.length > 0) {
|
|
178
|
+
options.onMigrationProgress?.(`Restoring ${existingData.length} records...`, 0.8);
|
|
179
|
+
for (const record of existingData) {
|
|
180
|
+
try {
|
|
181
|
+
store.put(record);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
options.onMigrationWarning?.(`Failed to restore record: ${err}`, err);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
options.onMigrationProgress?.(`Destructive migration complete`, 1);
|
|
189
|
+
return newDb;
|
|
190
|
+
}
|
|
191
|
+
async function createNewDatabase(tableName, primaryKey, expectedIndexes, options = {}, autoIncrement = false) {
|
|
192
|
+
options.onMigrationProgress?.(`Creating new database: ${tableName}`, 0);
|
|
193
|
+
try {
|
|
194
|
+
await deleteIndexedDbTable(tableName);
|
|
195
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
196
|
+
} catch (err) {}
|
|
197
|
+
const version = 1;
|
|
198
|
+
const db = await openIndexedDbTable(tableName, version, (event) => {
|
|
199
|
+
const db2 = event.target.result;
|
|
200
|
+
if (!db2.objectStoreNames.contains(METADATA_STORE_NAME)) {
|
|
201
|
+
db2.createObjectStore(METADATA_STORE_NAME, { keyPath: "tableName" });
|
|
202
|
+
}
|
|
203
|
+
const store = db2.createObjectStore(tableName, { keyPath: primaryKey, autoIncrement });
|
|
204
|
+
for (const idx of expectedIndexes) {
|
|
205
|
+
store.createIndex(idx.name, idx.keyPath, idx.options);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
const snapshot = {
|
|
209
|
+
version: db.version,
|
|
210
|
+
primaryKey,
|
|
211
|
+
indexes: expectedIndexes,
|
|
212
|
+
recordCount: 0,
|
|
213
|
+
timestamp: Date.now()
|
|
214
|
+
};
|
|
215
|
+
await saveSchemaMetadata(db, tableName, snapshot);
|
|
216
|
+
options.onMigrationProgress?.(`Database created successfully`, 1);
|
|
217
|
+
return db;
|
|
218
|
+
}
|
|
219
|
+
async function ensureIndexedDbTable(tableName, primaryKey, expectedIndexes = [], options = {}, autoIncrement = false) {
|
|
220
|
+
try {
|
|
221
|
+
let db;
|
|
222
|
+
let wasJustCreated = false;
|
|
223
|
+
try {
|
|
224
|
+
db = await openIndexedDbTable(tableName);
|
|
225
|
+
if (db.version === 1 && !db.objectStoreNames.contains(tableName)) {
|
|
226
|
+
wasJustCreated = true;
|
|
227
|
+
db.close();
|
|
228
|
+
}
|
|
229
|
+
} catch (err) {
|
|
230
|
+
options.onMigrationProgress?.(`Database ${tableName} does not exist or has version conflict, creating...`, 0);
|
|
231
|
+
return await createNewDatabase(tableName, primaryKey, expectedIndexes, options, autoIncrement);
|
|
232
|
+
}
|
|
233
|
+
if (wasJustCreated) {
|
|
234
|
+
options.onMigrationProgress?.(`Creating new database: ${tableName}`, 0);
|
|
235
|
+
try {
|
|
236
|
+
await deleteIndexedDbTable(tableName);
|
|
237
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
238
|
+
} catch (err) {}
|
|
239
|
+
db = await openIndexedDbTable(tableName, 1, (event) => {
|
|
240
|
+
const db2 = event.target.result;
|
|
241
|
+
if (!db2.objectStoreNames.contains(METADATA_STORE_NAME)) {
|
|
242
|
+
db2.createObjectStore(METADATA_STORE_NAME, { keyPath: "tableName" });
|
|
243
|
+
}
|
|
244
|
+
const store2 = db2.createObjectStore(tableName, { keyPath: primaryKey, autoIncrement });
|
|
245
|
+
for (const idx of expectedIndexes) {
|
|
246
|
+
store2.createIndex(idx.name, idx.keyPath, idx.options);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
const snapshot2 = {
|
|
250
|
+
version: db.version,
|
|
251
|
+
primaryKey,
|
|
252
|
+
indexes: expectedIndexes,
|
|
253
|
+
recordCount: 0,
|
|
254
|
+
timestamp: Date.now()
|
|
255
|
+
};
|
|
256
|
+
await saveSchemaMetadata(db, tableName, snapshot2);
|
|
257
|
+
options.onMigrationProgress?.(`Database created successfully`, 1);
|
|
258
|
+
return db;
|
|
259
|
+
}
|
|
260
|
+
if (!db.objectStoreNames.contains(METADATA_STORE_NAME)) {
|
|
261
|
+
const currentVersion = db.version;
|
|
262
|
+
db.close();
|
|
263
|
+
db = await openIndexedDbTable(tableName, currentVersion + 1, (event) => {
|
|
264
|
+
const db2 = event.target.result;
|
|
265
|
+
if (!db2.objectStoreNames.contains(METADATA_STORE_NAME)) {
|
|
266
|
+
db2.createObjectStore(METADATA_STORE_NAME, { keyPath: "tableName" });
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (!db.objectStoreNames.contains(tableName)) {
|
|
271
|
+
options.onMigrationProgress?.(`Object store ${tableName} does not exist, creating...`, 0);
|
|
272
|
+
db.close();
|
|
273
|
+
return await createNewDatabase(tableName, primaryKey, expectedIndexes, options, autoIncrement);
|
|
274
|
+
}
|
|
275
|
+
const transaction = db.transaction(tableName, "readonly");
|
|
276
|
+
const store = transaction.objectStore(tableName);
|
|
277
|
+
const diff = compareSchemas(store, primaryKey, expectedIndexes);
|
|
278
|
+
await new Promise((resolve) => {
|
|
279
|
+
transaction.oncomplete = () => resolve();
|
|
280
|
+
transaction.onerror = () => resolve();
|
|
281
|
+
});
|
|
282
|
+
const needsMigration = diff.indexesToAdd.length > 0 || diff.indexesToRemove.length > 0 || diff.indexesToModify.length > 0 || diff.needsObjectStoreRecreation;
|
|
283
|
+
if (!needsMigration) {
|
|
284
|
+
options.onMigrationProgress?.(`Schema for ${tableName} is up to date`, 1);
|
|
285
|
+
const snapshot2 = {
|
|
286
|
+
version: db.version,
|
|
287
|
+
primaryKey,
|
|
288
|
+
indexes: expectedIndexes,
|
|
289
|
+
timestamp: Date.now()
|
|
290
|
+
};
|
|
291
|
+
await saveSchemaMetadata(db, tableName, snapshot2);
|
|
292
|
+
return db;
|
|
293
|
+
}
|
|
294
|
+
if (diff.needsObjectStoreRecreation) {
|
|
295
|
+
options.onMigrationProgress?.(`Schema change requires object store recreation for ${tableName}`, 0);
|
|
296
|
+
db = await performDestructiveMigration(db, tableName, primaryKey, expectedIndexes, options, autoIncrement);
|
|
297
|
+
} else {
|
|
298
|
+
options.onMigrationProgress?.(`Performing incremental migration for ${tableName}`, 0);
|
|
299
|
+
db = await performIncrementalMigration(db, tableName, diff, options);
|
|
300
|
+
}
|
|
301
|
+
const snapshot = {
|
|
302
|
+
version: db.version,
|
|
303
|
+
primaryKey,
|
|
304
|
+
indexes: expectedIndexes,
|
|
305
|
+
timestamp: Date.now()
|
|
306
|
+
};
|
|
307
|
+
await saveSchemaMetadata(db, tableName, snapshot);
|
|
308
|
+
return db;
|
|
309
|
+
} catch (err) {
|
|
310
|
+
options.onMigrationWarning?.(`Migration failed for ${tableName}: ${err}`, err);
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async function dropIndexedDbTable(tableName) {
|
|
315
|
+
return deleteIndexedDbTable(tableName);
|
|
316
|
+
}
|
|
317
|
+
// src/storage/IndexedDbKvStorage.ts
|
|
318
|
+
import { createServiceToken as createServiceToken2 } from "@workglow/util";
|
|
319
|
+
|
|
320
|
+
// src/storage/IndexedDbTabularStorage.ts
|
|
321
|
+
import { createServiceToken, deepEqual as deepEqual2, makeFingerprint, uuid4 } from "@workglow/util";
|
|
322
|
+
import {
|
|
323
|
+
HybridSubscriptionManager,
|
|
324
|
+
BaseTabularStorage,
|
|
325
|
+
isSearchCondition,
|
|
326
|
+
pickCoveringIndex
|
|
327
|
+
} from "@workglow/storage";
|
|
328
|
+
var IDB_TABULAR_REPOSITORY = createServiceToken("storage.tabularRepository.indexedDb");
|
|
329
|
+
function compareEntitiesForChange(a, b) {
|
|
330
|
+
const au = a?.updated_at;
|
|
331
|
+
const bu = b?.updated_at;
|
|
332
|
+
if (typeof au === "string" && typeof bu === "string") {
|
|
333
|
+
return au === bu;
|
|
334
|
+
}
|
|
335
|
+
return deepEqual2(a, b);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
class IndexedDbTabularStorage extends BaseTabularStorage {
|
|
339
|
+
table;
|
|
340
|
+
db;
|
|
341
|
+
setupPromise = null;
|
|
342
|
+
migrationOptions;
|
|
343
|
+
hybridManager = null;
|
|
344
|
+
hybridOptions;
|
|
345
|
+
cursorSafeIndexes;
|
|
346
|
+
constructor(table = "tabular_store", schema, primaryKeyNames, indexes = [], migrationOptions = {}, clientProvidedKeys = "if-missing") {
|
|
347
|
+
super(schema, primaryKeyNames, indexes, clientProvidedKeys);
|
|
348
|
+
this.table = table;
|
|
349
|
+
this.migrationOptions = migrationOptions;
|
|
350
|
+
this.hybridOptions = {
|
|
351
|
+
useBroadcastChannel: migrationOptions.useBroadcastChannel ?? true,
|
|
352
|
+
backupPollingIntervalMs: migrationOptions.backupPollingIntervalMs ?? 5000
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
async getDb() {
|
|
356
|
+
if (this.db)
|
|
357
|
+
return this.db;
|
|
358
|
+
await this.setupDatabase();
|
|
359
|
+
return this.db;
|
|
360
|
+
}
|
|
361
|
+
async setupDatabase() {
|
|
362
|
+
if (this.db)
|
|
363
|
+
return;
|
|
364
|
+
if (this.setupPromise) {
|
|
365
|
+
await this.setupPromise;
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
this.setupPromise = this.performSetup();
|
|
369
|
+
try {
|
|
370
|
+
this.db = await this.setupPromise;
|
|
371
|
+
} finally {
|
|
372
|
+
this.setupPromise = null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async performSetup() {
|
|
376
|
+
const pkColumns = super.primaryKeyColumns();
|
|
377
|
+
const expectedIndexes = [];
|
|
378
|
+
for (const spec of this.indexes) {
|
|
379
|
+
const columns = spec;
|
|
380
|
+
if (columns.length <= pkColumns.length) {
|
|
381
|
+
const isPkPrefix = columns.every((col, idx) => col === pkColumns[idx]);
|
|
382
|
+
if (isPkPrefix)
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const columnNames = columns.map((col) => String(col));
|
|
386
|
+
const indexName = columnNames.join("_");
|
|
387
|
+
expectedIndexes.push({
|
|
388
|
+
name: indexName,
|
|
389
|
+
keyPath: columnNames.length === 1 ? columnNames[0] : columnNames,
|
|
390
|
+
options: { unique: false }
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
const primaryKey = pkColumns.length === 1 ? pkColumns[0] : pkColumns;
|
|
394
|
+
const useAutoIncrement = this.hasAutoGeneratedKey() && this.autoGeneratedKeyStrategy === "autoincrement" && pkColumns.length === 1;
|
|
395
|
+
return await ensureIndexedDbTable(this.table, primaryKey, expectedIndexes, this.migrationOptions, useAutoIncrement);
|
|
396
|
+
}
|
|
397
|
+
generateKeyValue(columnName, strategy) {
|
|
398
|
+
if (strategy === "uuid") {
|
|
399
|
+
return uuid4();
|
|
400
|
+
}
|
|
401
|
+
throw new Error(`IndexedDB autoincrement keys are generated by the database, not client-side. Column: ${columnName}`);
|
|
402
|
+
}
|
|
403
|
+
async put(record) {
|
|
404
|
+
const db = await this.getDb();
|
|
405
|
+
let recordToStore = record;
|
|
406
|
+
if (this.hasAutoGeneratedKey() && this.autoGeneratedKeyName) {
|
|
407
|
+
const keyName = String(this.autoGeneratedKeyName);
|
|
408
|
+
const clientProvidedValue = record[keyName];
|
|
409
|
+
const hasClientValue = clientProvidedValue !== undefined && clientProvidedValue !== null;
|
|
410
|
+
if (this.autoGeneratedKeyStrategy === "uuid") {
|
|
411
|
+
let shouldGenerate = false;
|
|
412
|
+
if (this.clientProvidedKeys === "never") {
|
|
413
|
+
shouldGenerate = true;
|
|
414
|
+
} else if (this.clientProvidedKeys === "always") {
|
|
415
|
+
if (!hasClientValue) {
|
|
416
|
+
throw new Error(`Auto-generated key "${keyName}" is required when clientProvidedKeys is "always"`);
|
|
417
|
+
}
|
|
418
|
+
shouldGenerate = false;
|
|
419
|
+
} else {
|
|
420
|
+
shouldGenerate = !hasClientValue;
|
|
421
|
+
}
|
|
422
|
+
if (shouldGenerate) {
|
|
423
|
+
const generatedValue = this.generateKeyValue(keyName, "uuid");
|
|
424
|
+
recordToStore = { ...record, [keyName]: generatedValue };
|
|
425
|
+
}
|
|
426
|
+
} else if (this.autoGeneratedKeyStrategy === "autoincrement") {
|
|
427
|
+
if (this.clientProvidedKeys === "always" && !hasClientValue) {
|
|
428
|
+
throw new Error(`Auto-generated key "${keyName}" is required when clientProvidedKeys is "always"`);
|
|
429
|
+
}
|
|
430
|
+
if (this.clientProvidedKeys === "never") {
|
|
431
|
+
const { [keyName]: _, ...rest } = record;
|
|
432
|
+
recordToStore = rest;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return new Promise((resolve, reject) => {
|
|
437
|
+
const transaction = db.transaction(this.table, "readwrite");
|
|
438
|
+
const store = transaction.objectStore(this.table);
|
|
439
|
+
const request = store.put(recordToStore);
|
|
440
|
+
request.onerror = () => {
|
|
441
|
+
reject(request.error);
|
|
442
|
+
};
|
|
443
|
+
request.onsuccess = () => {
|
|
444
|
+
if (this.hasAutoGeneratedKey() && this.autoGeneratedKeyName && this.autoGeneratedKeyStrategy === "autoincrement") {
|
|
445
|
+
const keyName = String(this.autoGeneratedKeyName);
|
|
446
|
+
if (recordToStore[keyName] === undefined) {
|
|
447
|
+
recordToStore = { ...recordToStore, [keyName]: request.result };
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
this.events.emit("put", recordToStore);
|
|
451
|
+
resolve(recordToStore);
|
|
452
|
+
};
|
|
453
|
+
transaction.oncomplete = () => {
|
|
454
|
+
this.hybridManager?.notifyLocalChange();
|
|
455
|
+
};
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
async putBulk(records) {
|
|
459
|
+
return await Promise.all(records.map((record) => this.put(record)));
|
|
460
|
+
}
|
|
461
|
+
getPrimaryKeyAsOrderedArray(key) {
|
|
462
|
+
return super.getPrimaryKeyAsOrderedArray(key).map((value) => typeof value === "bigint" ? value.toString() : value);
|
|
463
|
+
}
|
|
464
|
+
getIndexedKey(key) {
|
|
465
|
+
const keys = super.getPrimaryKeyAsOrderedArray(key).map((value) => typeof value === "bigint" ? value.toString() : value);
|
|
466
|
+
return keys.length === 1 ? keys[0] : keys;
|
|
467
|
+
}
|
|
468
|
+
async get(key) {
|
|
469
|
+
const db = await this.getDb();
|
|
470
|
+
return new Promise((resolve, reject) => {
|
|
471
|
+
const transaction = db.transaction(this.table, "readonly");
|
|
472
|
+
const store = transaction.objectStore(this.table);
|
|
473
|
+
const request = store.get(this.getIndexedKey(key));
|
|
474
|
+
request.onerror = () => reject(request.error);
|
|
475
|
+
request.onsuccess = () => {
|
|
476
|
+
if (!request.result) {
|
|
477
|
+
this.events.emit("get", key, undefined);
|
|
478
|
+
resolve(undefined);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
this.events.emit("get", key, request.result);
|
|
482
|
+
resolve(request.result);
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
async getAll(options) {
|
|
487
|
+
this.validateGetAllOptions(options);
|
|
488
|
+
const db = await this.getDb();
|
|
489
|
+
const transaction = db.transaction(this.table, "readonly");
|
|
490
|
+
const store = transaction.objectStore(this.table);
|
|
491
|
+
const request = store.getAll();
|
|
492
|
+
return new Promise((resolve, reject) => {
|
|
493
|
+
request.onerror = () => reject(request.error);
|
|
494
|
+
request.onsuccess = () => {
|
|
495
|
+
let values = request.result;
|
|
496
|
+
if (values.length === 0) {
|
|
497
|
+
resolve(undefined);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (options?.orderBy && options.orderBy.length > 0) {
|
|
501
|
+
values.sort((a, b) => {
|
|
502
|
+
for (const { column, direction } of options.orderBy) {
|
|
503
|
+
const aVal = a[column];
|
|
504
|
+
const bVal = b[column];
|
|
505
|
+
if (aVal == null && bVal == null)
|
|
506
|
+
continue;
|
|
507
|
+
if (aVal == null)
|
|
508
|
+
return direction === "ASC" ? -1 : 1;
|
|
509
|
+
if (bVal == null)
|
|
510
|
+
return direction === "ASC" ? 1 : -1;
|
|
511
|
+
if (aVal < bVal)
|
|
512
|
+
return direction === "ASC" ? -1 : 1;
|
|
513
|
+
if (aVal > bVal)
|
|
514
|
+
return direction === "ASC" ? 1 : -1;
|
|
515
|
+
}
|
|
516
|
+
return 0;
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
if (options?.offset !== undefined) {
|
|
520
|
+
values = values.slice(options.offset);
|
|
521
|
+
}
|
|
522
|
+
if (options?.limit !== undefined) {
|
|
523
|
+
values = values.slice(0, options.limit);
|
|
524
|
+
}
|
|
525
|
+
resolve(values.length > 0 ? values : undefined);
|
|
526
|
+
};
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
async delete(key) {
|
|
530
|
+
const db = await this.getDb();
|
|
531
|
+
return new Promise((resolve, reject) => {
|
|
532
|
+
const transaction = db.transaction(this.table, "readwrite");
|
|
533
|
+
const store = transaction.objectStore(this.table);
|
|
534
|
+
const request = store.delete(this.getIndexedKey(key));
|
|
535
|
+
request.onerror = () => reject(request.error);
|
|
536
|
+
request.onsuccess = () => {
|
|
537
|
+
this.events.emit("delete", key);
|
|
538
|
+
resolve();
|
|
539
|
+
};
|
|
540
|
+
transaction.oncomplete = () => {
|
|
541
|
+
this.hybridManager?.notifyLocalChange();
|
|
542
|
+
};
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
async deleteAll() {
|
|
546
|
+
const db = await this.getDb();
|
|
547
|
+
return new Promise((resolve, reject) => {
|
|
548
|
+
const transaction = db.transaction(this.table, "readwrite");
|
|
549
|
+
const store = transaction.objectStore(this.table);
|
|
550
|
+
const request = store.clear();
|
|
551
|
+
request.onerror = () => reject(request.error);
|
|
552
|
+
request.onsuccess = () => {
|
|
553
|
+
this.events.emit("clearall");
|
|
554
|
+
resolve();
|
|
555
|
+
};
|
|
556
|
+
transaction.oncomplete = () => {
|
|
557
|
+
this.hybridManager?.notifyLocalChange();
|
|
558
|
+
};
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
async size() {
|
|
562
|
+
const db = await this.getDb();
|
|
563
|
+
return new Promise((resolve, reject) => {
|
|
564
|
+
const transaction = db.transaction(this.table, "readonly");
|
|
565
|
+
const store = transaction.objectStore(this.table);
|
|
566
|
+
const request = store.count();
|
|
567
|
+
request.onerror = () => reject(request.error);
|
|
568
|
+
request.onsuccess = () => resolve(request.result);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
getCursorSafeIndexes() {
|
|
572
|
+
if (this.cursorSafeIndexes)
|
|
573
|
+
return this.cursorSafeIndexes;
|
|
574
|
+
const required = new Set(this.schema.required ?? []);
|
|
575
|
+
this.cursorSafeIndexes = this.indexes.filter((columns) => columns.every((column) => required.has(String(column))));
|
|
576
|
+
return this.cursorSafeIndexes;
|
|
577
|
+
}
|
|
578
|
+
createIndexedRange(store, criteria) {
|
|
579
|
+
const criteriaColumns = Object.keys(criteria);
|
|
580
|
+
if (criteriaColumns.length === 0)
|
|
581
|
+
return;
|
|
582
|
+
let best;
|
|
583
|
+
for (const indexColumns of this.getCursorSafeIndexes()) {
|
|
584
|
+
const prefixValues = [];
|
|
585
|
+
for (const column of indexColumns) {
|
|
586
|
+
const value = this.getEqualityCriterionValue(criteria, column);
|
|
587
|
+
if (value === undefined)
|
|
588
|
+
break;
|
|
589
|
+
prefixValues.push(value);
|
|
590
|
+
}
|
|
591
|
+
if (prefixValues.length === 0)
|
|
592
|
+
continue;
|
|
593
|
+
const indexedPrefix = indexColumns.slice(0, prefixValues.length);
|
|
594
|
+
const coversCriteria = criteriaColumns.every((column) => indexedPrefix.includes(column));
|
|
595
|
+
const better = !best || coversCriteria && !best.coversCriteria || coversCriteria === best.coversCriteria && prefixValues.length > best.prefixValues.length;
|
|
596
|
+
if (better) {
|
|
597
|
+
best = {
|
|
598
|
+
indexName: indexColumns.map((column) => String(column)).join("_"),
|
|
599
|
+
prefixValues,
|
|
600
|
+
fullMatch: prefixValues.length === indexColumns.length,
|
|
601
|
+
coversCriteria
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (!best)
|
|
606
|
+
return;
|
|
607
|
+
const range = best.fullMatch ? IDBKeyRange.only(best.prefixValues.length === 1 ? best.prefixValues[0] : best.prefixValues) : IDBKeyRange.bound(best.prefixValues, [...best.prefixValues, []]);
|
|
608
|
+
return {
|
|
609
|
+
source: store.index(best.indexName),
|
|
610
|
+
range,
|
|
611
|
+
coversCriteria: best.coversCriteria
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
async count(criteria) {
|
|
615
|
+
if (!criteria || Object.keys(criteria).length === 0) {
|
|
616
|
+
return await this.size();
|
|
617
|
+
}
|
|
618
|
+
this.validateQueryParams(criteria);
|
|
619
|
+
const db = await this.getDb();
|
|
620
|
+
return new Promise((resolve, reject) => {
|
|
621
|
+
const transaction = db.transaction(this.table, "readonly");
|
|
622
|
+
const store = transaction.objectStore(this.table);
|
|
623
|
+
const plan = this.createIndexedRange(store, criteria);
|
|
624
|
+
if (plan?.coversCriteria) {
|
|
625
|
+
const request2 = plan.source.count(plan.range);
|
|
626
|
+
request2.onerror = () => reject(request2.error);
|
|
627
|
+
request2.onsuccess = () => resolve(request2.result);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const source = plan?.source ?? store;
|
|
631
|
+
const range = plan?.range;
|
|
632
|
+
let count = 0;
|
|
633
|
+
const request = source.openCursor(range);
|
|
634
|
+
request.onerror = () => reject(request.error);
|
|
635
|
+
request.onsuccess = () => {
|
|
636
|
+
const cursor = request.result;
|
|
637
|
+
if (!cursor) {
|
|
638
|
+
resolve(count);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (this.matchesCriteria(cursor.value, criteria)) {
|
|
642
|
+
count += 1;
|
|
643
|
+
}
|
|
644
|
+
cursor.continue();
|
|
645
|
+
};
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
async getBulk(offset, limit) {
|
|
649
|
+
if (offset < 0) {
|
|
650
|
+
throw new RangeError(`offset must be non-negative, got ${offset}`);
|
|
651
|
+
}
|
|
652
|
+
if (limit <= 0) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const db = await this.getDb();
|
|
656
|
+
return new Promise((resolve, reject) => {
|
|
657
|
+
const transaction = db.transaction(this.table, "readonly");
|
|
658
|
+
const store = transaction.objectStore(this.table);
|
|
659
|
+
const request = store.openCursor();
|
|
660
|
+
const entities = [];
|
|
661
|
+
let skipped = false;
|
|
662
|
+
request.onerror = () => reject(request.error);
|
|
663
|
+
request.onsuccess = () => {
|
|
664
|
+
const cursor = request.result;
|
|
665
|
+
if (cursor) {
|
|
666
|
+
if (!skipped && offset > 0) {
|
|
667
|
+
skipped = true;
|
|
668
|
+
cursor.advance(offset);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
entities.push(cursor.value);
|
|
672
|
+
if (entities.length === limit) {
|
|
673
|
+
resolve(entities);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
cursor.continue();
|
|
677
|
+
} else {
|
|
678
|
+
resolve(entities.length > 0 ? entities : undefined);
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
matchesCriteria(record, criteria) {
|
|
684
|
+
for (const column of Object.keys(criteria)) {
|
|
685
|
+
const criterion = criteria[column];
|
|
686
|
+
const recordValue = record[column];
|
|
687
|
+
let operator = "=";
|
|
688
|
+
let value;
|
|
689
|
+
if (isSearchCondition(criterion)) {
|
|
690
|
+
operator = criterion.operator;
|
|
691
|
+
value = criterion.value;
|
|
692
|
+
} else {
|
|
693
|
+
value = criterion;
|
|
694
|
+
}
|
|
695
|
+
if (operator !== "=" && (recordValue === null || recordValue === undefined)) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
switch (operator) {
|
|
699
|
+
case "=":
|
|
700
|
+
if (recordValue !== value)
|
|
701
|
+
return false;
|
|
702
|
+
break;
|
|
703
|
+
case "<":
|
|
704
|
+
if (!(recordValue < value))
|
|
705
|
+
return false;
|
|
706
|
+
break;
|
|
707
|
+
case "<=":
|
|
708
|
+
if (!(recordValue <= value))
|
|
709
|
+
return false;
|
|
710
|
+
break;
|
|
711
|
+
case ">":
|
|
712
|
+
if (!(recordValue > value))
|
|
713
|
+
return false;
|
|
714
|
+
break;
|
|
715
|
+
case ">=":
|
|
716
|
+
if (!(recordValue >= value))
|
|
717
|
+
return false;
|
|
718
|
+
break;
|
|
719
|
+
default:
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
async deleteSearch(criteria) {
|
|
726
|
+
const criteriaKeys = Object.keys(criteria);
|
|
727
|
+
if (criteriaKeys.length === 0) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const db = await this.getDb();
|
|
731
|
+
return new Promise(async (resolve, reject) => {
|
|
732
|
+
try {
|
|
733
|
+
const transaction = db.transaction(this.table, "readwrite");
|
|
734
|
+
const store = transaction.objectStore(this.table);
|
|
735
|
+
transaction.oncomplete = () => {
|
|
736
|
+
this.events.emit("delete", criteriaKeys[0]);
|
|
737
|
+
this.hybridManager?.notifyLocalChange();
|
|
738
|
+
resolve();
|
|
739
|
+
};
|
|
740
|
+
transaction.onerror = () => {
|
|
741
|
+
reject(transaction.error);
|
|
742
|
+
};
|
|
743
|
+
const getAllRequest = store.getAll();
|
|
744
|
+
getAllRequest.onsuccess = () => {
|
|
745
|
+
const allRecords = getAllRequest.result;
|
|
746
|
+
const recordsToDelete = allRecords.filter((record) => this.matchesCriteria(record, criteria));
|
|
747
|
+
if (recordsToDelete.length === 0) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
for (const record of recordsToDelete) {
|
|
751
|
+
const primaryKey = this.primaryKeyColumns().reduce((key, col) => {
|
|
752
|
+
key[col] = record[col];
|
|
753
|
+
return key;
|
|
754
|
+
}, {});
|
|
755
|
+
const request = store.delete(this.getIndexedKey(primaryKey));
|
|
756
|
+
request.onerror = () => {
|
|
757
|
+
console.error("Error deleting record:", request.error);
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
getAllRequest.onerror = () => {
|
|
762
|
+
reject(getAllRequest.error);
|
|
763
|
+
};
|
|
764
|
+
} catch (error) {
|
|
765
|
+
reject(error);
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
getEqualityCriterionValue(criteria, column) {
|
|
770
|
+
const criterion = criteria[column];
|
|
771
|
+
if (criterion === undefined)
|
|
772
|
+
return;
|
|
773
|
+
if (isSearchCondition(criterion)) {
|
|
774
|
+
return criterion.operator === "=" ? criterion.value : undefined;
|
|
775
|
+
}
|
|
776
|
+
return criterion;
|
|
777
|
+
}
|
|
778
|
+
compareByOrder(a, b, options) {
|
|
779
|
+
if (!options?.orderBy)
|
|
780
|
+
return 0;
|
|
781
|
+
for (const { column, direction } of options.orderBy) {
|
|
782
|
+
const aVal = a[column];
|
|
783
|
+
const bVal = b[column];
|
|
784
|
+
if (aVal == null && bVal == null)
|
|
785
|
+
continue;
|
|
786
|
+
if (aVal == null)
|
|
787
|
+
return direction === "ASC" ? -1 : 1;
|
|
788
|
+
if (bVal == null)
|
|
789
|
+
return direction === "ASC" ? 1 : -1;
|
|
790
|
+
if (aVal < bVal)
|
|
791
|
+
return direction === "ASC" ? -1 : 1;
|
|
792
|
+
if (aVal > bVal)
|
|
793
|
+
return direction === "ASC" ? 1 : -1;
|
|
794
|
+
}
|
|
795
|
+
return 0;
|
|
796
|
+
}
|
|
797
|
+
createIndexedQuery(store, criteria, options) {
|
|
798
|
+
const orderBy = options?.orderBy ?? [];
|
|
799
|
+
let best;
|
|
800
|
+
for (const indexColumns of this.getCursorSafeIndexes()) {
|
|
801
|
+
const prefixValues = [];
|
|
802
|
+
for (const column of indexColumns) {
|
|
803
|
+
const value = this.getEqualityCriterionValue(criteria, column);
|
|
804
|
+
if (value === undefined)
|
|
805
|
+
break;
|
|
806
|
+
prefixValues.push(value);
|
|
807
|
+
}
|
|
808
|
+
if (prefixValues.length === 0)
|
|
809
|
+
continue;
|
|
810
|
+
const remainingColumns = indexColumns.slice(prefixValues.length);
|
|
811
|
+
let redundantOrderPrefixLength = 0;
|
|
812
|
+
while (redundantOrderPrefixLength < orderBy.length && redundantOrderPrefixLength < prefixValues.length && orderBy[redundantOrderPrefixLength]?.column === indexColumns[redundantOrderPrefixLength]) {
|
|
813
|
+
redundantOrderPrefixLength++;
|
|
814
|
+
}
|
|
815
|
+
const normalizedOrderBy = orderBy.slice(redundantOrderPrefixLength);
|
|
816
|
+
const satisfiesOrder = normalizedOrderBy.length === 0 || normalizedOrderBy.length <= remainingColumns.length && normalizedOrderBy.every((order, index) => order.column === remainingColumns[index]) && orderBy.every((order) => order.direction === orderBy[0]?.direction);
|
|
817
|
+
if (!satisfiesOrder && best)
|
|
818
|
+
continue;
|
|
819
|
+
if (!best || satisfiesOrder && !best.satisfiesOrder || prefixValues.length > best.prefixValues.length) {
|
|
820
|
+
best = {
|
|
821
|
+
indexName: indexColumns.map((column) => String(column)).join("_"),
|
|
822
|
+
prefixValues,
|
|
823
|
+
fullMatch: prefixValues.length === indexColumns.length,
|
|
824
|
+
satisfiesOrder,
|
|
825
|
+
direction: orderBy[0]?.direction === "DESC" ? "prev" : "next"
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const appliedLimit = Boolean(best?.satisfiesOrder && options?.limit !== undefined);
|
|
830
|
+
const appliedOffset = Boolean(best?.satisfiesOrder && options?.offset !== undefined);
|
|
831
|
+
if (!best) {
|
|
832
|
+
return {
|
|
833
|
+
source: store,
|
|
834
|
+
range: undefined,
|
|
835
|
+
direction: orderBy[0]?.direction === "DESC" ? "prev" : "next",
|
|
836
|
+
satisfiesOrder: false,
|
|
837
|
+
appliedLimit: false,
|
|
838
|
+
appliedOffset: false,
|
|
839
|
+
skipRemaining: 0
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
const source = store.index(best.indexName);
|
|
843
|
+
const keyRange = best.fullMatch ? IDBKeyRange.only(best.prefixValues.length === 1 ? best.prefixValues[0] : best.prefixValues) : IDBKeyRange.bound(best.prefixValues, [...best.prefixValues, []]);
|
|
844
|
+
return {
|
|
845
|
+
source,
|
|
846
|
+
range: keyRange,
|
|
847
|
+
direction: best.direction,
|
|
848
|
+
satisfiesOrder: best.satisfiesOrder,
|
|
849
|
+
appliedLimit,
|
|
850
|
+
appliedOffset,
|
|
851
|
+
skipRemaining: appliedOffset ? options?.offset ?? 0 : 0
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
async query(criteria, options) {
|
|
855
|
+
this.validateQueryParams(criteria, options);
|
|
856
|
+
const db = await this.getDb();
|
|
857
|
+
return new Promise((resolve, reject) => {
|
|
858
|
+
const transaction = db.transaction(this.table, "readonly");
|
|
859
|
+
const store = transaction.objectStore(this.table);
|
|
860
|
+
const indexedQuery = this.createIndexedQuery(store, criteria, options);
|
|
861
|
+
const results = [];
|
|
862
|
+
const request = indexedQuery.source.openCursor(indexedQuery.range, indexedQuery.direction);
|
|
863
|
+
request.onsuccess = () => {
|
|
864
|
+
const cursor = request.result;
|
|
865
|
+
if (!cursor) {
|
|
866
|
+
let finalResults = results;
|
|
867
|
+
if (!indexedQuery.satisfiesOrder && options?.orderBy && options.orderBy.length > 0) {
|
|
868
|
+
finalResults = [...finalResults].sort((a, b) => this.compareByOrder(a, b, options));
|
|
869
|
+
}
|
|
870
|
+
if (!indexedQuery.appliedOffset && options?.offset !== undefined) {
|
|
871
|
+
finalResults = finalResults.slice(options.offset);
|
|
872
|
+
}
|
|
873
|
+
if (!indexedQuery.appliedLimit && options?.limit !== undefined) {
|
|
874
|
+
finalResults = finalResults.slice(0, options.limit);
|
|
875
|
+
}
|
|
876
|
+
const result = finalResults.length > 0 ? finalResults : undefined;
|
|
877
|
+
this.events.emit("query", criteria, result);
|
|
878
|
+
resolve(result);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const record = cursor.value;
|
|
882
|
+
if (this.matchesCriteria(record, criteria)) {
|
|
883
|
+
if (indexedQuery.skipRemaining > 0) {
|
|
884
|
+
indexedQuery.skipRemaining -= 1;
|
|
885
|
+
} else {
|
|
886
|
+
results.push(record);
|
|
887
|
+
if (indexedQuery.appliedLimit && results.length === options?.limit) {
|
|
888
|
+
const result = results.length > 0 ? results : undefined;
|
|
889
|
+
this.events.emit("query", criteria, result);
|
|
890
|
+
resolve(result);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
cursor.continue();
|
|
896
|
+
};
|
|
897
|
+
request.onerror = () => reject(request.error);
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
async queryIndex(criteria, options) {
|
|
901
|
+
this.validateSelect(options);
|
|
902
|
+
this.validateQueryParams(criteria, options);
|
|
903
|
+
const registered = this.indexes.map((cols) => {
|
|
904
|
+
const cs = Array.isArray(cols) ? cols : [cols];
|
|
905
|
+
return { name: cs.join("_"), keyPath: cs };
|
|
906
|
+
});
|
|
907
|
+
const picked = pickCoveringIndex({
|
|
908
|
+
table: this.table,
|
|
909
|
+
indexes: registered,
|
|
910
|
+
criteriaColumns: Object.keys(criteria),
|
|
911
|
+
orderByColumns: (options.orderBy ?? []).map((o) => ({
|
|
912
|
+
column: String(o.column),
|
|
913
|
+
direction: o.direction
|
|
914
|
+
})),
|
|
915
|
+
selectColumns: options.select.map(String),
|
|
916
|
+
primaryKeyColumns: this.primaryKeyColumns().map(String)
|
|
917
|
+
});
|
|
918
|
+
const db = await this.getDb();
|
|
919
|
+
return new Promise((resolve, reject) => {
|
|
920
|
+
const tx = db.transaction(this.table, "readonly");
|
|
921
|
+
const store = tx.objectStore(this.table);
|
|
922
|
+
const idx = store.index(picked.name);
|
|
923
|
+
const prefix = [];
|
|
924
|
+
for (const col of picked.keyPath) {
|
|
925
|
+
const c = criteria[col];
|
|
926
|
+
if (c === undefined && !(col in criteria))
|
|
927
|
+
break;
|
|
928
|
+
if (isSearchCondition(c)) {
|
|
929
|
+
if (c.operator !== "=")
|
|
930
|
+
break;
|
|
931
|
+
prefix.push(c.value);
|
|
932
|
+
} else {
|
|
933
|
+
prefix.push(c);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
const range = prefix.length === 0 ? undefined : prefix.length === picked.keyPath.length ? IDBKeyRange.only(prefix.length === 1 ? prefix[0] : prefix) : IDBKeyRange.bound(prefix, [...prefix, []]);
|
|
937
|
+
const direction = picked.reverseDirection ? "prev" : "next";
|
|
938
|
+
const request = idx.openKeyCursor(range, direction);
|
|
939
|
+
const out = [];
|
|
940
|
+
let toSkip = options.offset ?? 0;
|
|
941
|
+
const keyPathPositions = new Map;
|
|
942
|
+
picked.keyPath.forEach((col, i) => keyPathPositions.set(col, i));
|
|
943
|
+
const pkCols = this.primaryKeyColumns().map(String);
|
|
944
|
+
const pkPositions = new Map;
|
|
945
|
+
pkCols.forEach((col, i) => pkPositions.set(col, i));
|
|
946
|
+
request.onsuccess = () => {
|
|
947
|
+
const cursor = request.result;
|
|
948
|
+
if (!cursor) {
|
|
949
|
+
resolve(out);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const key = cursor.key;
|
|
953
|
+
const row = {};
|
|
954
|
+
for (const col of options.select) {
|
|
955
|
+
const colStr = String(col);
|
|
956
|
+
const pos = keyPathPositions.get(colStr);
|
|
957
|
+
if (pos !== undefined) {
|
|
958
|
+
row[colStr] = Array.isArray(key) ? key[pos] : key;
|
|
959
|
+
} else {
|
|
960
|
+
if (pkCols.length === 1 && colStr === pkCols[0]) {
|
|
961
|
+
row[colStr] = cursor.primaryKey;
|
|
962
|
+
} else {
|
|
963
|
+
const pkPos = pkPositions.get(colStr);
|
|
964
|
+
if (pkPos !== undefined) {
|
|
965
|
+
row[colStr] = Array.isArray(cursor.primaryKey) ? cursor.primaryKey[pkPos] : cursor.primaryKey;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
let matches = true;
|
|
971
|
+
for (const [col, crit] of Object.entries(criteria)) {
|
|
972
|
+
const pos = keyPathPositions.get(col);
|
|
973
|
+
if (pos === undefined)
|
|
974
|
+
continue;
|
|
975
|
+
if (pos < prefix.length)
|
|
976
|
+
continue;
|
|
977
|
+
const valFromKey = Array.isArray(key) ? key[pos] : key;
|
|
978
|
+
const op = isSearchCondition(crit) ? crit.operator : "=";
|
|
979
|
+
const val = isSearchCondition(crit) ? crit.value : crit;
|
|
980
|
+
if (!compareWithOperator(valFromKey, op, val)) {
|
|
981
|
+
matches = false;
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
if (matches) {
|
|
986
|
+
if (toSkip > 0) {
|
|
987
|
+
toSkip -= 1;
|
|
988
|
+
} else {
|
|
989
|
+
out.push(row);
|
|
990
|
+
if (options.limit !== undefined && out.length >= options.limit) {
|
|
991
|
+
resolve(out);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
cursor.continue();
|
|
997
|
+
};
|
|
998
|
+
request.onerror = () => reject(request.error);
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
getHybridManager() {
|
|
1002
|
+
if (!this.hybridManager) {
|
|
1003
|
+
const channelName = `indexeddb-tabular-${this.table}`;
|
|
1004
|
+
this.hybridManager = new HybridSubscriptionManager(channelName, async () => {
|
|
1005
|
+
const entities = await this.getAll() || [];
|
|
1006
|
+
const map = new Map;
|
|
1007
|
+
for (const entity of entities) {
|
|
1008
|
+
const { key } = this.separateKeyValueFromCombined(entity);
|
|
1009
|
+
const fingerprint = await makeFingerprint(key);
|
|
1010
|
+
map.set(fingerprint, entity);
|
|
1011
|
+
}
|
|
1012
|
+
return map;
|
|
1013
|
+
}, compareEntitiesForChange, {
|
|
1014
|
+
insert: (item) => ({ type: "INSERT", new: item }),
|
|
1015
|
+
update: (oldItem, newItem) => ({ type: "UPDATE", old: oldItem, new: newItem }),
|
|
1016
|
+
delete: (item) => ({ type: "DELETE", old: item })
|
|
1017
|
+
}, {
|
|
1018
|
+
defaultIntervalMs: 1000,
|
|
1019
|
+
useBroadcastChannel: this.hybridOptions.useBroadcastChannel,
|
|
1020
|
+
backupPollingIntervalMs: this.hybridOptions.backupPollingIntervalMs
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
return this.hybridManager;
|
|
1024
|
+
}
|
|
1025
|
+
subscribeToChanges(callback, options) {
|
|
1026
|
+
const intervalMs = options?.pollingIntervalMs ?? 1000;
|
|
1027
|
+
const manager = this.getHybridManager();
|
|
1028
|
+
return manager.subscribe(callback, { intervalMs });
|
|
1029
|
+
}
|
|
1030
|
+
destroy() {
|
|
1031
|
+
if (this.hybridManager) {
|
|
1032
|
+
this.hybridManager.destroy();
|
|
1033
|
+
this.hybridManager = null;
|
|
1034
|
+
}
|
|
1035
|
+
this.db?.close();
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
function compareWithOperator(a, op, b) {
|
|
1039
|
+
const av = a;
|
|
1040
|
+
const bv = b;
|
|
1041
|
+
switch (op) {
|
|
1042
|
+
case "=":
|
|
1043
|
+
return av === bv;
|
|
1044
|
+
case "<":
|
|
1045
|
+
return av !== null && av !== undefined && av < bv;
|
|
1046
|
+
case "<=":
|
|
1047
|
+
return av !== null && av !== undefined && av <= bv;
|
|
1048
|
+
case ">":
|
|
1049
|
+
return av !== null && av !== undefined && av > bv;
|
|
1050
|
+
case ">=":
|
|
1051
|
+
return av !== null && av !== undefined && av >= bv;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// src/storage/IndexedDbKvStorage.ts
|
|
1056
|
+
import {
|
|
1057
|
+
DefaultKeyValueKey,
|
|
1058
|
+
DefaultKeyValueSchema,
|
|
1059
|
+
KvViaTabularStorage
|
|
1060
|
+
} from "@workglow/storage";
|
|
1061
|
+
var IDB_KV_REPOSITORY = createServiceToken2("storage.kvRepository.indexedDb");
|
|
1062
|
+
|
|
1063
|
+
class IndexedDbKvStorage extends KvViaTabularStorage {
|
|
1064
|
+
dbName;
|
|
1065
|
+
tabularRepository;
|
|
1066
|
+
constructor(dbName, keySchema = { type: "string" }, valueSchema = {}) {
|
|
1067
|
+
super(keySchema, valueSchema);
|
|
1068
|
+
this.dbName = dbName;
|
|
1069
|
+
this.tabularRepository = new IndexedDbTabularStorage(dbName, DefaultKeyValueSchema, DefaultKeyValueKey);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
// src/storage/IndexedDbVectorStorage.ts
|
|
1073
|
+
import { createServiceToken as createServiceToken3 } from "@workglow/util";
|
|
1074
|
+
import { cosineSimilarity } from "@workglow/util/schema";
|
|
1075
|
+
import { getMetadataProperty, getVectorProperty } from "@workglow/storage";
|
|
1076
|
+
var IDB_VECTOR_REPOSITORY = createServiceToken3("storage.vectorRepository.indexedDb");
|
|
1077
|
+
function matchesFilter(metadata, filter) {
|
|
1078
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
1079
|
+
if (metadata[key] !== value) {
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return true;
|
|
1084
|
+
}
|
|
1085
|
+
function textRelevance(text, query) {
|
|
1086
|
+
const textLower = text.toLowerCase();
|
|
1087
|
+
const queryLower = query.toLowerCase();
|
|
1088
|
+
const queryWords = queryLower.split(/\s+/).filter((w) => w.length > 0);
|
|
1089
|
+
if (queryWords.length === 0) {
|
|
1090
|
+
return 0;
|
|
1091
|
+
}
|
|
1092
|
+
let matches = 0;
|
|
1093
|
+
for (const word of queryWords) {
|
|
1094
|
+
if (textLower.includes(word)) {
|
|
1095
|
+
matches++;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return matches / queryWords.length;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
class IndexedDbVectorStorage extends IndexedDbTabularStorage {
|
|
1102
|
+
vectorDimensions;
|
|
1103
|
+
vectorPropertyName;
|
|
1104
|
+
metadataPropertyName;
|
|
1105
|
+
constructor(table = "vectors", schema, primaryKeyNames, indexes = [], dimensions, _vectorCtor = Float32Array, migrationOptions = {}, clientProvidedKeys = "if-missing") {
|
|
1106
|
+
super(table, schema, primaryKeyNames, indexes, migrationOptions, clientProvidedKeys);
|
|
1107
|
+
this.vectorDimensions = dimensions;
|
|
1108
|
+
const vectorProp = getVectorProperty(schema);
|
|
1109
|
+
if (!vectorProp) {
|
|
1110
|
+
throw new Error("Schema must have a property with type array and format TypedArray");
|
|
1111
|
+
}
|
|
1112
|
+
this.vectorPropertyName = vectorProp;
|
|
1113
|
+
this.metadataPropertyName = getMetadataProperty(schema);
|
|
1114
|
+
}
|
|
1115
|
+
getVectorDimensions() {
|
|
1116
|
+
return this.vectorDimensions;
|
|
1117
|
+
}
|
|
1118
|
+
async similaritySearch(query, options = {}) {
|
|
1119
|
+
const { topK = 10, filter, scoreThreshold = 0 } = options;
|
|
1120
|
+
const results = [];
|
|
1121
|
+
const allEntities = await this.getAll() || [];
|
|
1122
|
+
for (const entity of allEntities) {
|
|
1123
|
+
const vector = entity[this.vectorPropertyName];
|
|
1124
|
+
const metadata = this.metadataPropertyName ? entity[this.metadataPropertyName] : {};
|
|
1125
|
+
if (filter && !matchesFilter(metadata, filter)) {
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
const score = cosineSimilarity(query, vector);
|
|
1129
|
+
if (score < scoreThreshold) {
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
results.push({
|
|
1133
|
+
...entity,
|
|
1134
|
+
score
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
results.sort((a, b) => b.score - a.score);
|
|
1138
|
+
const topResults = results.slice(0, topK);
|
|
1139
|
+
return topResults;
|
|
1140
|
+
}
|
|
1141
|
+
async hybridSearch(query, options) {
|
|
1142
|
+
const { topK = 10, filter, scoreThreshold = 0, textQuery, vectorWeight = 0.7 } = options;
|
|
1143
|
+
if (!textQuery || textQuery.trim().length === 0) {
|
|
1144
|
+
return this.similaritySearch(query, { topK, filter, scoreThreshold });
|
|
1145
|
+
}
|
|
1146
|
+
const results = [];
|
|
1147
|
+
const allEntities = await this.getAll() || [];
|
|
1148
|
+
for (const entity of allEntities) {
|
|
1149
|
+
const vector = entity[this.vectorPropertyName];
|
|
1150
|
+
const metadata = this.metadataPropertyName ? entity[this.metadataPropertyName] : {};
|
|
1151
|
+
if (filter && !matchesFilter(metadata, filter)) {
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
const vectorScore = cosineSimilarity(query, vector);
|
|
1155
|
+
const metadataText = Object.values(metadata).join(" ").toLowerCase();
|
|
1156
|
+
const textScore = textRelevance(metadataText, textQuery);
|
|
1157
|
+
const combinedScore = vectorWeight * vectorScore + (1 - vectorWeight) * textScore;
|
|
1158
|
+
if (combinedScore < scoreThreshold) {
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
results.push({
|
|
1162
|
+
...entity,
|
|
1163
|
+
score: combinedScore
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
results.sort((a, b) => b.score - a.score);
|
|
1167
|
+
const topResults = results.slice(0, topK);
|
|
1168
|
+
return topResults;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
export {
|
|
1172
|
+
ensureIndexedDbTable,
|
|
1173
|
+
dropIndexedDbTable,
|
|
1174
|
+
IndexedDbVectorStorage,
|
|
1175
|
+
IndexedDbTabularStorage,
|
|
1176
|
+
IndexedDbKvStorage,
|
|
1177
|
+
IDB_VECTOR_REPOSITORY,
|
|
1178
|
+
IDB_TABULAR_REPOSITORY,
|
|
1179
|
+
IDB_KV_REPOSITORY
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
//# debugId=D7674124BFA18F0C64756E2164756E21
|