@firtoz/drizzle-indexeddb 0.2.0 → 0.4.0
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/CHANGELOG.md +70 -26
- package/README.md +274 -51
- package/package.json +14 -6
- package/src/bin/generate-migrations.ts +288 -0
- package/src/collections/indexeddb-collection.ts +97 -397
- package/src/context/useDrizzleIndexedDB.ts +2 -1
- package/src/function-migrator.ts +191 -170
- package/src/idb-interceptor.ts +75 -0
- package/src/idb-operations.ts +41 -0
- package/src/idb-types.ts +135 -0
- package/src/index.ts +57 -9
- package/src/instrumented-idb-database.ts +188 -0
- package/src/native-idb-database.ts +355 -0
- package/src/proxy/idb-proxy-client.ts +345 -0
- package/src/proxy/idb-proxy-server.ts +313 -0
- package/src/proxy/idb-proxy-transport.ts +174 -0
- package/src/proxy/idb-proxy-types.ts +77 -0
- package/src/proxy/idb-sync-adapter.ts +95 -0
- package/src/proxy/index.ts +37 -0
- package/src/snapshot-migrator.ts +0 -420
- package/src/utils.ts +0 -15
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
createCollectionConfig,
|
|
15
15
|
} from "@firtoz/drizzle-utils";
|
|
16
16
|
|
|
17
|
+
import type { IDBDatabaseLike, KeyRangeSpec } from "../idb-types";
|
|
18
|
+
|
|
17
19
|
// biome-ignore lint/suspicious/noExplicitAny: intentional
|
|
18
20
|
type AnyId = IdOf<any>;
|
|
19
21
|
|
|
@@ -28,78 +30,11 @@ export type IndexedDBSyncItem = {
|
|
|
28
30
|
[key: string]: unknown;
|
|
29
31
|
};
|
|
30
32
|
|
|
31
|
-
/**
|
|
32
|
-
* Operation tracking for IndexedDB queries
|
|
33
|
-
* Useful for testing and debugging to verify what operations are actually performed
|
|
34
|
-
*
|
|
35
|
-
* Uses discriminated unions for type safety - TypeScript can narrow the type based on the 'type' field
|
|
36
|
-
*/
|
|
37
|
-
export type IDBOperation =
|
|
38
|
-
| {
|
|
39
|
-
type: "getAll";
|
|
40
|
-
storeName: string;
|
|
41
|
-
itemsReturned: unknown[];
|
|
42
|
-
itemCount: number;
|
|
43
|
-
context: string;
|
|
44
|
-
timestamp: number;
|
|
45
|
-
}
|
|
46
|
-
| {
|
|
47
|
-
type: "index-getAll";
|
|
48
|
-
storeName: string;
|
|
49
|
-
indexName: string;
|
|
50
|
-
keyRange?: IDBKeyRange;
|
|
51
|
-
itemsReturned: unknown[];
|
|
52
|
-
itemCount: number;
|
|
53
|
-
context: string;
|
|
54
|
-
timestamp: number;
|
|
55
|
-
}
|
|
56
|
-
| {
|
|
57
|
-
type: "write";
|
|
58
|
-
storeName: string;
|
|
59
|
-
itemsWritten: unknown[];
|
|
60
|
-
writeCount: number;
|
|
61
|
-
context: string;
|
|
62
|
-
timestamp: number;
|
|
63
|
-
}
|
|
64
|
-
| {
|
|
65
|
-
type: "get";
|
|
66
|
-
storeName: string;
|
|
67
|
-
key: IDBValidKey;
|
|
68
|
-
itemReturned?: unknown;
|
|
69
|
-
timestamp: number;
|
|
70
|
-
}
|
|
71
|
-
| {
|
|
72
|
-
type: "put";
|
|
73
|
-
storeName: string;
|
|
74
|
-
item: unknown;
|
|
75
|
-
timestamp: number;
|
|
76
|
-
}
|
|
77
|
-
| {
|
|
78
|
-
type: "delete";
|
|
79
|
-
storeName: string;
|
|
80
|
-
key: IDBValidKey;
|
|
81
|
-
timestamp: number;
|
|
82
|
-
}
|
|
83
|
-
| {
|
|
84
|
-
type: "clear";
|
|
85
|
-
storeName: string;
|
|
86
|
-
timestamp: number;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Interceptor interface for tracking IndexedDB operations
|
|
91
|
-
* Allows tests and debugging tools to observe what operations are performed
|
|
92
|
-
*/
|
|
93
|
-
export interface IDBInterceptor {
|
|
94
|
-
/** Called when any IndexedDB operation is performed */
|
|
95
|
-
onOperation?: (operation: IDBOperation) => void;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
33
|
export interface IndexedDBCollectionConfig<TTable extends Table> {
|
|
99
34
|
/**
|
|
100
35
|
* Ref to the IndexedDB database instance
|
|
101
36
|
*/
|
|
102
|
-
indexedDBRef: React.RefObject<
|
|
37
|
+
indexedDBRef: React.RefObject<IDBDatabaseLike | null>;
|
|
103
38
|
/**
|
|
104
39
|
* The database name (for perf markers)
|
|
105
40
|
*/
|
|
@@ -124,10 +59,6 @@ export interface IndexedDBCollectionConfig<TTable extends Table> {
|
|
|
124
59
|
* Enable debug logging for index discovery and query optimization
|
|
125
60
|
*/
|
|
126
61
|
debug?: boolean;
|
|
127
|
-
/**
|
|
128
|
-
* Optional interceptor for tracking IndexedDB operations (for testing/debugging)
|
|
129
|
-
*/
|
|
130
|
-
interceptor?: IDBInterceptor;
|
|
131
62
|
}
|
|
132
63
|
|
|
133
64
|
/**
|
|
@@ -257,99 +188,6 @@ export function getExpressionValue(
|
|
|
257
188
|
throw new Error(`Cannot get value from expression type: ${expression.type}`);
|
|
258
189
|
}
|
|
259
190
|
|
|
260
|
-
/**
|
|
261
|
-
* Reads all items from an IndexedDB object store
|
|
262
|
-
*/
|
|
263
|
-
function getAllFromStore(
|
|
264
|
-
db: IDBDatabase,
|
|
265
|
-
storeName: string,
|
|
266
|
-
interceptor?: IDBInterceptor,
|
|
267
|
-
): Promise<IndexedDBSyncItem[]> {
|
|
268
|
-
return new Promise((resolve, reject) => {
|
|
269
|
-
if (!db.objectStoreNames.contains(storeName)) {
|
|
270
|
-
resolve([]);
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const transaction = db.transaction(storeName, "readonly");
|
|
275
|
-
|
|
276
|
-
const store = transaction.objectStore(storeName);
|
|
277
|
-
|
|
278
|
-
const request = store.getAll();
|
|
279
|
-
|
|
280
|
-
request.onsuccess = () => {
|
|
281
|
-
const items = request.result as IndexedDBSyncItem[];
|
|
282
|
-
|
|
283
|
-
// Log operation after executing with results
|
|
284
|
-
if (interceptor?.onOperation) {
|
|
285
|
-
interceptor.onOperation({
|
|
286
|
-
type: "getAll",
|
|
287
|
-
storeName,
|
|
288
|
-
itemsReturned: items,
|
|
289
|
-
itemCount: items.length,
|
|
290
|
-
context: "Full table scan",
|
|
291
|
-
timestamp: Date.now(),
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
resolve(items);
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
request.onerror = () => {
|
|
299
|
-
reject(request.error);
|
|
300
|
-
};
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Reads items from an IndexedDB index with an optional key range
|
|
306
|
-
* Note: Index existence is validated at collection creation time
|
|
307
|
-
*/
|
|
308
|
-
function getAllFromIndex(
|
|
309
|
-
db: IDBDatabase,
|
|
310
|
-
storeName: string,
|
|
311
|
-
indexName: string,
|
|
312
|
-
keyRange?: IDBKeyRange,
|
|
313
|
-
interceptor?: IDBInterceptor,
|
|
314
|
-
): Promise<IndexedDBSyncItem[]> {
|
|
315
|
-
return new Promise((resolve, reject) => {
|
|
316
|
-
const transaction = db.transaction(storeName, "readonly");
|
|
317
|
-
|
|
318
|
-
const store = transaction.objectStore(storeName);
|
|
319
|
-
|
|
320
|
-
const index = store.index(indexName);
|
|
321
|
-
|
|
322
|
-
const request = keyRange ? index.getAll(keyRange) : index.getAll();
|
|
323
|
-
|
|
324
|
-
request.onsuccess = () => {
|
|
325
|
-
const items = request.result as IndexedDBSyncItem[];
|
|
326
|
-
|
|
327
|
-
// Log operation after executing with results
|
|
328
|
-
if (interceptor?.onOperation) {
|
|
329
|
-
const rangeDesc = keyRange
|
|
330
|
-
? `[${keyRange.lower ?? ""}..${keyRange.upper ?? ""}]`
|
|
331
|
-
: "all";
|
|
332
|
-
interceptor.onOperation({
|
|
333
|
-
type: "index-getAll",
|
|
334
|
-
storeName,
|
|
335
|
-
indexName,
|
|
336
|
-
keyRange,
|
|
337
|
-
itemsReturned: items,
|
|
338
|
-
itemCount: items.length,
|
|
339
|
-
context: `Index query on ${indexName}, range: ${rangeDesc}`,
|
|
340
|
-
timestamp: Date.now(),
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
resolve(items);
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
request.onerror = () => {
|
|
348
|
-
reject(request.error);
|
|
349
|
-
};
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
|
|
353
191
|
/**
|
|
354
192
|
* Attempts to extract a simple indexed query from an IR expression
|
|
355
193
|
* Returns the field name and key range if the query can be optimized
|
|
@@ -369,7 +207,7 @@ export function tryExtractIndexedQuery(
|
|
|
369
207
|
expression: IR.BasicExpression,
|
|
370
208
|
indexes?: Record<string, string>,
|
|
371
209
|
debug?: boolean,
|
|
372
|
-
): { fieldName: string; indexName: string; keyRange:
|
|
210
|
+
): { fieldName: string; indexName: string; keyRange: KeyRangeSpec } | null {
|
|
373
211
|
if (!indexes) {
|
|
374
212
|
return null;
|
|
375
213
|
}
|
|
@@ -391,25 +229,40 @@ export function tryExtractIndexedQuery(
|
|
|
391
229
|
return null;
|
|
392
230
|
}
|
|
393
231
|
|
|
394
|
-
// Convert operator to
|
|
395
|
-
|
|
396
|
-
let keyRange: IDBKeyRange | null = null;
|
|
232
|
+
// Convert operator to key range spec
|
|
233
|
+
let keyRange: KeyRangeSpec | null = null;
|
|
397
234
|
|
|
398
235
|
switch (comparison.operator) {
|
|
399
236
|
case "eq":
|
|
400
|
-
keyRange =
|
|
237
|
+
keyRange = { type: "only", value: comparison.value };
|
|
401
238
|
break;
|
|
402
239
|
case "gt":
|
|
403
|
-
keyRange =
|
|
240
|
+
keyRange = {
|
|
241
|
+
type: "lowerBound",
|
|
242
|
+
lower: comparison.value,
|
|
243
|
+
lowerOpen: true,
|
|
244
|
+
};
|
|
404
245
|
break;
|
|
405
246
|
case "gte":
|
|
406
|
-
keyRange =
|
|
247
|
+
keyRange = {
|
|
248
|
+
type: "lowerBound",
|
|
249
|
+
lower: comparison.value,
|
|
250
|
+
lowerOpen: false,
|
|
251
|
+
};
|
|
407
252
|
break;
|
|
408
253
|
case "lt":
|
|
409
|
-
keyRange =
|
|
254
|
+
keyRange = {
|
|
255
|
+
type: "upperBound",
|
|
256
|
+
upper: comparison.value,
|
|
257
|
+
upperOpen: true,
|
|
258
|
+
};
|
|
410
259
|
break;
|
|
411
260
|
case "lte":
|
|
412
|
-
keyRange =
|
|
261
|
+
keyRange = {
|
|
262
|
+
type: "upperBound",
|
|
263
|
+
upper: comparison.value,
|
|
264
|
+
upperOpen: false,
|
|
265
|
+
};
|
|
413
266
|
break;
|
|
414
267
|
default:
|
|
415
268
|
if (debug) {
|
|
@@ -433,104 +286,7 @@ export function tryExtractIndexedQuery(
|
|
|
433
286
|
}
|
|
434
287
|
}
|
|
435
288
|
|
|
436
|
-
|
|
437
|
-
* Adds an item to an IndexedDB object store using an existing transaction
|
|
438
|
-
*/
|
|
439
|
-
function addToStoreInTransaction(
|
|
440
|
-
store: IDBObjectStore,
|
|
441
|
-
item: IndexedDBSyncItem,
|
|
442
|
-
): Promise<void> {
|
|
443
|
-
return new Promise((resolve, reject) => {
|
|
444
|
-
const request = store.add(item);
|
|
445
|
-
|
|
446
|
-
request.onsuccess = () => {
|
|
447
|
-
resolve();
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
request.onerror = () => {
|
|
451
|
-
reject(request.error);
|
|
452
|
-
};
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Updates an item in an IndexedDB object store using an existing transaction
|
|
458
|
-
*/
|
|
459
|
-
function updateInStoreInTransaction(
|
|
460
|
-
store: IDBObjectStore,
|
|
461
|
-
item: IndexedDBSyncItem,
|
|
462
|
-
): Promise<void> {
|
|
463
|
-
return new Promise((resolve, reject) => {
|
|
464
|
-
const request = store.put(item);
|
|
465
|
-
|
|
466
|
-
request.onsuccess = () => {
|
|
467
|
-
resolve();
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
request.onerror = () => {
|
|
471
|
-
reject(request.error);
|
|
472
|
-
};
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Deletes an item from an IndexedDB object store using an existing transaction
|
|
478
|
-
*/
|
|
479
|
-
function deleteFromStoreInTransaction(
|
|
480
|
-
store: IDBObjectStore,
|
|
481
|
-
id: string,
|
|
482
|
-
): Promise<void> {
|
|
483
|
-
return new Promise((resolve, reject) => {
|
|
484
|
-
const request = store.delete(id);
|
|
485
|
-
|
|
486
|
-
request.onsuccess = () => {
|
|
487
|
-
resolve();
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
request.onerror = () => {
|
|
491
|
-
reject(request.error);
|
|
492
|
-
};
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* Gets a single item from an IndexedDB object store by ID using an existing transaction
|
|
498
|
-
*/
|
|
499
|
-
function getFromStoreInTransaction(
|
|
500
|
-
store: IDBObjectStore,
|
|
501
|
-
id: AnyId,
|
|
502
|
-
): Promise<IndexedDBSyncItem | undefined> {
|
|
503
|
-
return new Promise((resolve, reject) => {
|
|
504
|
-
const request = store.get(id);
|
|
505
|
-
|
|
506
|
-
request.onsuccess = () => {
|
|
507
|
-
resolve(request.result as IndexedDBSyncItem | undefined);
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
request.onerror = () => {
|
|
511
|
-
reject(request.error);
|
|
512
|
-
};
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Executes a transaction and returns a promise that resolves when the transaction completes
|
|
518
|
-
*/
|
|
519
|
-
function commitTransaction(transaction: IDBTransaction): Promise<void> {
|
|
520
|
-
return new Promise((resolve, reject) => {
|
|
521
|
-
transaction.oncomplete = () => {
|
|
522
|
-
resolve();
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
transaction.onerror = () => {
|
|
526
|
-
reject(transaction.error);
|
|
527
|
-
};
|
|
528
|
-
|
|
529
|
-
transaction.onabort = () => {
|
|
530
|
-
reject(new Error("Transaction aborted"));
|
|
531
|
-
};
|
|
532
|
-
});
|
|
533
|
-
}
|
|
289
|
+
// Note: Low-level transaction helpers have been replaced by high-level IDBDatabaseLike methods
|
|
534
290
|
|
|
535
291
|
/**
|
|
536
292
|
* Auto-discovers indexes from the IndexedDB store
|
|
@@ -553,30 +309,17 @@ function commitTransaction(transaction: IDBTransaction): Promise<void> {
|
|
|
553
309
|
* // This collection will auto-detect and use them for optimized queries
|
|
554
310
|
*/
|
|
555
311
|
function discoverIndexes(
|
|
556
|
-
db:
|
|
312
|
+
db: IDBDatabaseLike,
|
|
557
313
|
storeName: string,
|
|
558
314
|
): Record<string, string> {
|
|
559
|
-
|
|
560
|
-
return {};
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
const transaction = db.transaction(storeName, "readonly");
|
|
564
|
-
|
|
565
|
-
const store = transaction.objectStore(storeName);
|
|
566
|
-
|
|
315
|
+
const indexes = db.getStoreIndexes(storeName);
|
|
567
316
|
const indexMap: Record<string, string> = {};
|
|
568
317
|
|
|
569
|
-
|
|
570
|
-
const indexNames = Array.from(store.indexNames);
|
|
571
|
-
|
|
572
|
-
for (const indexName of indexNames) {
|
|
573
|
-
const index = store.index(indexName);
|
|
574
|
-
const keyPath = index.keyPath;
|
|
575
|
-
|
|
318
|
+
for (const index of indexes) {
|
|
576
319
|
// Only map single-column indexes (string keyPath)
|
|
577
320
|
// Compound indexes (array keyPath) are more complex and not currently optimized
|
|
578
|
-
if (typeof keyPath === "string") {
|
|
579
|
-
indexMap[keyPath] =
|
|
321
|
+
if (typeof index.keyPath === "string") {
|
|
322
|
+
indexMap[index.keyPath] = index.name;
|
|
580
323
|
}
|
|
581
324
|
}
|
|
582
325
|
|
|
@@ -599,12 +342,13 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
599
342
|
const discoverIndexesOnce = async () => {
|
|
600
343
|
await config.readyPromise;
|
|
601
344
|
|
|
345
|
+
const db = config.indexedDBRef.current;
|
|
346
|
+
if (!db) {
|
|
347
|
+
throw new Error("Database not ready");
|
|
348
|
+
}
|
|
349
|
+
|
|
602
350
|
if (!indexesDiscovered) {
|
|
603
|
-
discoveredIndexes = discoverIndexes(
|
|
604
|
-
// biome-ignore lint/style/noNonNullAssertion: DB is guaranteed to be ready after readyPromise resolves
|
|
605
|
-
config.indexedDBRef.current!,
|
|
606
|
-
config.storeName,
|
|
607
|
-
);
|
|
351
|
+
discoveredIndexes = discoverIndexes(db, config.storeName);
|
|
608
352
|
|
|
609
353
|
indexesDiscovered = true;
|
|
610
354
|
}
|
|
@@ -613,26 +357,14 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
613
357
|
// Create backend-specific implementation
|
|
614
358
|
const backend: SyncBackend<TTable> = {
|
|
615
359
|
initialLoad: async (write) => {
|
|
360
|
+
const db = config.indexedDBRef.current;
|
|
361
|
+
if (!db) {
|
|
362
|
+
throw new Error("Database not ready");
|
|
363
|
+
}
|
|
364
|
+
|
|
616
365
|
await discoverIndexesOnce();
|
|
617
366
|
|
|
618
|
-
const items = await
|
|
619
|
-
// biome-ignore lint/style/noNonNullAssertion: DB is guaranteed to be ready after readyPromise resolves
|
|
620
|
-
config.indexedDBRef.current!,
|
|
621
|
-
config.storeName,
|
|
622
|
-
config.interceptor,
|
|
623
|
-
);
|
|
624
|
-
|
|
625
|
-
// Log what's being written to the collection
|
|
626
|
-
if (config.interceptor?.onOperation) {
|
|
627
|
-
config.interceptor.onOperation({
|
|
628
|
-
type: "write",
|
|
629
|
-
storeName: config.storeName,
|
|
630
|
-
itemsWritten: items,
|
|
631
|
-
writeCount: items.length,
|
|
632
|
-
context: "Initial load (eager mode)",
|
|
633
|
-
timestamp: Date.now(),
|
|
634
|
-
});
|
|
635
|
-
}
|
|
367
|
+
const items = await db.getAll<IndexedDBSyncItem>(config.storeName);
|
|
636
368
|
|
|
637
369
|
for (const item of items) {
|
|
638
370
|
write(item as unknown as InferSchemaOutput<SelectSchema<TTable>>);
|
|
@@ -640,13 +372,14 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
640
372
|
},
|
|
641
373
|
|
|
642
374
|
loadSubset: async (options, write) => {
|
|
375
|
+
const db = config.indexedDBRef.current;
|
|
376
|
+
if (!db) {
|
|
377
|
+
throw new Error("Database not ready");
|
|
378
|
+
}
|
|
379
|
+
|
|
643
380
|
// Ensure indexes are discovered before we try to use them
|
|
644
381
|
if (!indexesDiscovered) {
|
|
645
|
-
discoveredIndexes = discoverIndexes(
|
|
646
|
-
// biome-ignore lint/style/noNonNullAssertion: DB is guaranteed to be ready after readyPromise resolves
|
|
647
|
-
config.indexedDBRef.current!,
|
|
648
|
-
config.storeName,
|
|
649
|
-
);
|
|
382
|
+
discoveredIndexes = discoverIndexes(db, config.storeName);
|
|
650
383
|
indexesDiscovered = true;
|
|
651
384
|
}
|
|
652
385
|
|
|
@@ -659,22 +392,15 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
659
392
|
|
|
660
393
|
if (indexedQuery) {
|
|
661
394
|
// Use indexed query for better performance
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
config.indexedDBRef.current!,
|
|
395
|
+
|
|
396
|
+
items = await db.getAllByIndex<IndexedDBSyncItem>(
|
|
665
397
|
config.storeName,
|
|
666
398
|
indexedQuery.indexName,
|
|
667
399
|
indexedQuery.keyRange,
|
|
668
|
-
config.interceptor,
|
|
669
400
|
);
|
|
670
401
|
} else {
|
|
671
402
|
// Fall back to getting all items
|
|
672
|
-
items = await
|
|
673
|
-
// biome-ignore lint/style/noNonNullAssertion: DB is guaranteed to be ready after readyPromise resolves
|
|
674
|
-
config.indexedDBRef.current!,
|
|
675
|
-
config.storeName,
|
|
676
|
-
config.interceptor,
|
|
677
|
-
);
|
|
403
|
+
items = await db.getAll<IndexedDBSyncItem>(config.storeName);
|
|
678
404
|
|
|
679
405
|
// Apply where filter in memory
|
|
680
406
|
if (options.where) {
|
|
@@ -719,65 +445,30 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
719
445
|
items = items.slice(0, options.limit);
|
|
720
446
|
}
|
|
721
447
|
|
|
722
|
-
// Log what's being written to the collection
|
|
723
|
-
if (config.interceptor?.onOperation) {
|
|
724
|
-
const contextParts: string[] = ["On-demand load"];
|
|
725
|
-
if (indexedQuery) {
|
|
726
|
-
contextParts.push(`via index ${indexedQuery.indexName}`);
|
|
727
|
-
} else if (options.where) {
|
|
728
|
-
contextParts.push("via full scan + in-memory filter");
|
|
729
|
-
}
|
|
730
|
-
if (options.orderBy) {
|
|
731
|
-
contextParts.push("with sorting");
|
|
732
|
-
}
|
|
733
|
-
if (options.limit !== undefined) {
|
|
734
|
-
contextParts.push(`limit ${options.limit}`);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
config.interceptor.onOperation({
|
|
738
|
-
type: "write",
|
|
739
|
-
storeName: config.storeName,
|
|
740
|
-
itemsWritten: items,
|
|
741
|
-
writeCount: items.length,
|
|
742
|
-
context: contextParts.join(", "),
|
|
743
|
-
timestamp: Date.now(),
|
|
744
|
-
});
|
|
745
|
-
}
|
|
746
|
-
|
|
747
448
|
for (const item of items) {
|
|
748
449
|
write(item as unknown as InferSchemaOutput<SelectSchema<TTable>>);
|
|
749
450
|
}
|
|
750
451
|
},
|
|
751
452
|
|
|
752
453
|
handleInsert: async (mutations) => {
|
|
454
|
+
const db = config.indexedDBRef.current;
|
|
455
|
+
if (!db) {
|
|
456
|
+
throw new Error("Database not ready");
|
|
457
|
+
}
|
|
458
|
+
|
|
753
459
|
const results: Array<InferSchemaOutput<SelectSchema<TTable>>> = [];
|
|
754
460
|
|
|
755
461
|
try {
|
|
756
|
-
|
|
757
|
-
// biome-ignore lint/style/noNonNullAssertion: DB is guaranteed to be ready after readyPromise resolves
|
|
758
|
-
const transaction = config.indexedDBRef.current!.transaction(
|
|
759
|
-
config.storeName,
|
|
760
|
-
"readwrite",
|
|
761
|
-
);
|
|
762
|
-
const store = transaction.objectStore(config.storeName);
|
|
763
|
-
|
|
764
|
-
const addPromises: Promise<void>[] = [];
|
|
462
|
+
const itemsToInsert: IndexedDBSyncItem[] = [];
|
|
765
463
|
|
|
766
464
|
for (const mutation of mutations) {
|
|
767
465
|
const itemToInsert = mutation.modified;
|
|
768
|
-
|
|
769
|
-
// Add result for reactive store
|
|
770
466
|
results.push(itemToInsert);
|
|
771
|
-
|
|
772
|
-
// Add to IndexedDB in parallel (don't await yet)
|
|
773
|
-
addPromises.push(
|
|
774
|
-
addToStoreInTransaction(store, itemToInsert as IndexedDBSyncItem),
|
|
775
|
-
);
|
|
467
|
+
itemsToInsert.push(itemToInsert as IndexedDBSyncItem);
|
|
776
468
|
}
|
|
777
469
|
|
|
778
|
-
//
|
|
779
|
-
await
|
|
780
|
-
await commitTransaction(transaction);
|
|
470
|
+
// Add all items in a single batch operation
|
|
471
|
+
await db.add(config.storeName, itemsToInsert);
|
|
781
472
|
} catch (error) {
|
|
782
473
|
// Clear results on error so nothing gets written to reactive store
|
|
783
474
|
results.length = 0;
|
|
@@ -788,20 +479,20 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
788
479
|
},
|
|
789
480
|
|
|
790
481
|
handleUpdate: async (mutations) => {
|
|
482
|
+
const db = config.indexedDBRef.current;
|
|
483
|
+
|
|
484
|
+
if (!db) {
|
|
485
|
+
throw new Error("Database not ready");
|
|
486
|
+
}
|
|
487
|
+
|
|
791
488
|
const results: Array<InferSchemaOutput<SelectSchema<TTable>>> = [];
|
|
792
|
-
const
|
|
793
|
-
// Use a single transaction for all updates
|
|
794
|
-
// biome-ignore lint/style/noNonNullAssertion: DB is guaranteed to be ready after readyPromise resolves
|
|
795
|
-
const transaction = config.indexedDBRef.current!.transaction(
|
|
796
|
-
config.storeName,
|
|
797
|
-
"readwrite",
|
|
798
|
-
);
|
|
799
|
-
const store = transaction.objectStore(config.storeName);
|
|
489
|
+
const itemsToUpdate: IndexedDBSyncItem[] = [];
|
|
800
490
|
|
|
801
491
|
for (const mutation of mutations) {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
492
|
+
const existing = await db.get<IndexedDBSyncItem>(
|
|
493
|
+
config.storeName,
|
|
494
|
+
mutation.key,
|
|
495
|
+
);
|
|
805
496
|
|
|
806
497
|
if (existing) {
|
|
807
498
|
const updateTime = new Date();
|
|
@@ -811,7 +502,7 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
811
502
|
updatedAt: updateTime,
|
|
812
503
|
} as IndexedDBSyncItem;
|
|
813
504
|
|
|
814
|
-
|
|
505
|
+
itemsToUpdate.push(updatedItem);
|
|
815
506
|
results.push(
|
|
816
507
|
updatedItem as unknown as InferSchemaOutput<SelectSchema<TTable>>,
|
|
817
508
|
);
|
|
@@ -821,27 +512,36 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
821
512
|
}
|
|
822
513
|
}
|
|
823
514
|
|
|
824
|
-
//
|
|
825
|
-
|
|
515
|
+
// Update all items in a single batch operation
|
|
516
|
+
if (itemsToUpdate.length > 0) {
|
|
517
|
+
await db.put(config.storeName, itemsToUpdate);
|
|
518
|
+
}
|
|
826
519
|
|
|
827
520
|
return results;
|
|
828
521
|
},
|
|
829
522
|
|
|
830
523
|
handleDelete: async (mutations) => {
|
|
831
|
-
|
|
832
|
-
// biome-ignore lint/style/noNonNullAssertion: DB is guaranteed to be ready after readyPromise resolves
|
|
833
|
-
const transaction = config.indexedDBRef.current!.transaction(
|
|
834
|
-
config.storeName,
|
|
835
|
-
"readwrite",
|
|
836
|
-
);
|
|
837
|
-
const store = transaction.objectStore(config.storeName);
|
|
524
|
+
const db = config.indexedDBRef.current;
|
|
838
525
|
|
|
839
|
-
|
|
840
|
-
|
|
526
|
+
if (!db) {
|
|
527
|
+
throw new Error("Database not ready");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const keysToDelete: IDBValidKey[] = mutations.map((m) => m.key);
|
|
531
|
+
|
|
532
|
+
// Delete all items in a single batch operation
|
|
533
|
+
await db.delete(config.storeName, keysToDelete);
|
|
534
|
+
},
|
|
535
|
+
|
|
536
|
+
handleTruncate: async () => {
|
|
537
|
+
const db = config.indexedDBRef.current;
|
|
538
|
+
|
|
539
|
+
if (!db) {
|
|
540
|
+
throw new Error("Database not ready");
|
|
841
541
|
}
|
|
842
542
|
|
|
843
|
-
//
|
|
844
|
-
await
|
|
543
|
+
// Clear all items from the store
|
|
544
|
+
await db.clear(config.storeName);
|
|
845
545
|
},
|
|
846
546
|
};
|
|
847
547
|
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
useIndexedDBCollection,
|
|
5
5
|
type DrizzleIndexedDBContextValue,
|
|
6
6
|
} from "./DrizzleIndexedDBProvider";
|
|
7
|
+
import type { IDBDatabaseLike } from "../idb-types";
|
|
7
8
|
|
|
8
9
|
export type UseDrizzleIndexedDBContextReturn<
|
|
9
10
|
TSchema extends Record<string, unknown>,
|
|
@@ -11,7 +12,7 @@ export type UseDrizzleIndexedDBContextReturn<
|
|
|
11
12
|
useCollection: <TTableName extends keyof TSchema & string>(
|
|
12
13
|
tableName: TTableName,
|
|
13
14
|
) => ReturnType<typeof useIndexedDBCollection<TSchema, TTableName>>;
|
|
14
|
-
indexedDB:
|
|
15
|
+
indexedDB: IDBDatabaseLike | null;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
export function useDrizzleIndexedDB<
|