@firtoz/drizzle-indexeddb 0.2.0 → 0.3.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.
@@ -14,6 +14,8 @@ import {
14
14
  createCollectionConfig,
15
15
  } from "@firtoz/drizzle-utils";
16
16
 
17
+ import type { IDBDatabaseLike, KeyRangeSpec } from "../utils";
18
+
17
19
  // biome-ignore lint/suspicious/noExplicitAny: intentional
18
20
  type AnyId = IdOf<any>;
19
21
 
@@ -99,7 +101,7 @@ export interface IndexedDBCollectionConfig<TTable extends Table> {
99
101
  /**
100
102
  * Ref to the IndexedDB database instance
101
103
  */
102
- indexedDBRef: React.RefObject<IDBDatabase | null>;
104
+ indexedDBRef: React.RefObject<IDBDatabaseLike | null>;
103
105
  /**
104
106
  * The database name (for perf markers)
105
107
  */
@@ -260,94 +262,63 @@ export function getExpressionValue(
260
262
  /**
261
263
  * Reads all items from an IndexedDB object store
262
264
  */
263
- function getAllFromStore(
264
- db: IDBDatabase,
265
+ async function getAllFromStore(
266
+ db: IDBDatabaseLike,
265
267
  storeName: string,
266
268
  interceptor?: IDBInterceptor,
267
269
  ): 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
- };
270
+ const items = await db.getAll<IndexedDBSyncItem>(storeName);
271
+
272
+ // Log operation after executing with results
273
+ if (interceptor?.onOperation) {
274
+ interceptor.onOperation({
275
+ type: "getAll",
276
+ storeName,
277
+ itemsReturned: items,
278
+ itemCount: items.length,
279
+ context: "Full table scan",
280
+ timestamp: Date.now(),
281
+ });
282
+ }
297
283
 
298
- request.onerror = () => {
299
- reject(request.error);
300
- };
301
- });
284
+ return items;
302
285
  }
303
286
 
304
287
  /**
305
288
  * Reads items from an IndexedDB index with an optional key range
306
289
  * Note: Index existence is validated at collection creation time
307
290
  */
308
- function getAllFromIndex(
309
- db: IDBDatabase,
291
+ async function getAllFromIndex(
292
+ db: IDBDatabaseLike,
310
293
  storeName: string,
311
294
  indexName: string,
312
- keyRange?: IDBKeyRange,
295
+ keyRange?: KeyRangeSpec,
313
296
  interceptor?: IDBInterceptor,
314
297
  ): 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
- }
298
+ const items = await db.getAllByIndex<IndexedDBSyncItem>(
299
+ storeName,
300
+ indexName,
301
+ keyRange,
302
+ );
343
303
 
344
- resolve(items);
345
- };
304
+ // Log operation after executing with results
305
+ if (interceptor?.onOperation) {
306
+ const rangeDesc = keyRange
307
+ ? `[${keyRange.lower ?? keyRange.value ?? ""}..${keyRange.upper ?? keyRange.value ?? ""}]`
308
+ : "all";
309
+ interceptor.onOperation({
310
+ type: "index-getAll",
311
+ storeName,
312
+ indexName,
313
+ keyRange: keyRange as unknown as IDBKeyRange | undefined,
314
+ itemsReturned: items,
315
+ itemCount: items.length,
316
+ context: `Index query on ${indexName}, range: ${rangeDesc}`,
317
+ timestamp: Date.now(),
318
+ });
319
+ }
346
320
 
347
- request.onerror = () => {
348
- reject(request.error);
349
- };
350
- });
321
+ return items;
351
322
  }
352
323
 
353
324
  /**
@@ -369,7 +340,7 @@ export function tryExtractIndexedQuery(
369
340
  expression: IR.BasicExpression,
370
341
  indexes?: Record<string, string>,
371
342
  debug?: boolean,
372
- ): { fieldName: string; indexName: string; keyRange: IDBKeyRange } | null {
343
+ ): { fieldName: string; indexName: string; keyRange: KeyRangeSpec } | null {
373
344
  if (!indexes) {
374
345
  return null;
375
346
  }
@@ -391,25 +362,40 @@ export function tryExtractIndexedQuery(
391
362
  return null;
392
363
  }
393
364
 
394
- // Convert operator to IndexedDB key range
395
-
396
- let keyRange: IDBKeyRange | null = null;
365
+ // Convert operator to key range spec
366
+ let keyRange: KeyRangeSpec | null = null;
397
367
 
398
368
  switch (comparison.operator) {
399
369
  case "eq":
400
- keyRange = IDBKeyRange.only(comparison.value);
370
+ keyRange = { type: "only", value: comparison.value };
401
371
  break;
402
372
  case "gt":
403
- keyRange = IDBKeyRange.lowerBound(comparison.value, true);
373
+ keyRange = {
374
+ type: "lowerBound",
375
+ lower: comparison.value,
376
+ lowerOpen: true,
377
+ };
404
378
  break;
405
379
  case "gte":
406
- keyRange = IDBKeyRange.lowerBound(comparison.value, false);
380
+ keyRange = {
381
+ type: "lowerBound",
382
+ lower: comparison.value,
383
+ lowerOpen: false,
384
+ };
407
385
  break;
408
386
  case "lt":
409
- keyRange = IDBKeyRange.upperBound(comparison.value, true);
387
+ keyRange = {
388
+ type: "upperBound",
389
+ upper: comparison.value,
390
+ upperOpen: true,
391
+ };
410
392
  break;
411
393
  case "lte":
412
- keyRange = IDBKeyRange.upperBound(comparison.value, false);
394
+ keyRange = {
395
+ type: "upperBound",
396
+ upper: comparison.value,
397
+ upperOpen: false,
398
+ };
413
399
  break;
414
400
  default:
415
401
  if (debug) {
@@ -433,104 +419,7 @@ export function tryExtractIndexedQuery(
433
419
  }
434
420
  }
435
421
 
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
- }
422
+ // Note: Low-level transaction helpers have been replaced by high-level IDBDatabaseLike methods
534
423
 
535
424
  /**
536
425
  * Auto-discovers indexes from the IndexedDB store
@@ -553,30 +442,17 @@ function commitTransaction(transaction: IDBTransaction): Promise<void> {
553
442
  * // This collection will auto-detect and use them for optimized queries
554
443
  */
555
444
  function discoverIndexes(
556
- db: IDBDatabase,
445
+ db: IDBDatabaseLike,
557
446
  storeName: string,
558
447
  ): Record<string, string> {
559
- if (!db.objectStoreNames.contains(storeName)) {
560
- return {};
561
- }
562
-
563
- const transaction = db.transaction(storeName, "readonly");
564
-
565
- const store = transaction.objectStore(storeName);
566
-
448
+ const indexes = db.getStoreIndexes(storeName);
567
449
  const indexMap: Record<string, string> = {};
568
450
 
569
- // Iterate through all indexes in the store
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
-
451
+ for (const index of indexes) {
576
452
  // Only map single-column indexes (string keyPath)
577
453
  // Compound indexes (array keyPath) are more complex and not currently optimized
578
- if (typeof keyPath === "string") {
579
- indexMap[keyPath] = indexName;
454
+ if (typeof index.keyPath === "string") {
455
+ indexMap[index.keyPath] = index.name;
580
456
  }
581
457
  }
582
458
 
@@ -753,31 +629,17 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
753
629
  const results: Array<InferSchemaOutput<SelectSchema<TTable>>> = [];
754
630
 
755
631
  try {
756
- // Use a single transaction for all inserts
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>[] = [];
632
+ const itemsToInsert: IndexedDBSyncItem[] = [];
765
633
 
766
634
  for (const mutation of mutations) {
767
635
  const itemToInsert = mutation.modified;
768
-
769
- // Add result for reactive store
770
636
  results.push(itemToInsert);
771
-
772
- // Add to IndexedDB in parallel (don't await yet)
773
- addPromises.push(
774
- addToStoreInTransaction(store, itemToInsert as IndexedDBSyncItem),
775
- );
637
+ itemsToInsert.push(itemToInsert as IndexedDBSyncItem);
776
638
  }
777
639
 
778
- // Wait for all IndexedDB writes to complete
779
- await Promise.all(addPromises);
780
- await commitTransaction(transaction);
640
+ // Add all items in a single batch operation
641
+ // biome-ignore lint/style/noNonNullAssertion: DB is guaranteed to be ready after readyPromise resolves
642
+ await config.indexedDBRef.current!.add(config.storeName, itemsToInsert);
781
643
  } catch (error) {
782
644
  // Clear results on error so nothing gets written to reactive store
783
645
  results.length = 0;
@@ -789,19 +651,16 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
789
651
 
790
652
  handleUpdate: async (mutations) => {
791
653
  const results: Array<InferSchemaOutput<SelectSchema<TTable>>> = [];
792
- const originalValues: Array<InferSchemaOutput<SelectSchema<TTable>>> = [];
793
- // Use a single transaction for all updates
654
+ const itemsToUpdate: IndexedDBSyncItem[] = [];
655
+
794
656
  // 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);
657
+ const db = config.indexedDBRef.current!;
800
658
 
801
659
  for (const mutation of mutations) {
802
- originalValues.push(mutation.original);
803
-
804
- const existing = await getFromStoreInTransaction(store, mutation.key);
660
+ const existing = await db.get<IndexedDBSyncItem>(
661
+ config.storeName,
662
+ mutation.key,
663
+ );
805
664
 
806
665
  if (existing) {
807
666
  const updateTime = new Date();
@@ -811,7 +670,7 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
811
670
  updatedAt: updateTime,
812
671
  } as IndexedDBSyncItem;
813
672
 
814
- await updateInStoreInTransaction(store, updatedItem);
673
+ itemsToUpdate.push(updatedItem);
815
674
  results.push(
816
675
  updatedItem as unknown as InferSchemaOutput<SelectSchema<TTable>>,
817
676
  );
@@ -821,27 +680,20 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
821
680
  }
822
681
  }
823
682
 
824
- // Wait for transaction to complete
825
- await commitTransaction(transaction);
683
+ // Update all items in a single batch operation
684
+ if (itemsToUpdate.length > 0) {
685
+ await db.put(config.storeName, itemsToUpdate);
686
+ }
826
687
 
827
688
  return results;
828
689
  },
829
690
 
830
691
  handleDelete: async (mutations) => {
831
- // Use a single transaction for all deletes
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);
692
+ const keysToDelete: IDBValidKey[] = mutations.map((m) => m.key);
838
693
 
839
- for (const mutation of mutations) {
840
- await deleteFromStoreInTransaction(store, mutation.key);
841
- }
842
-
843
- // Wait for transaction to complete
844
- await commitTransaction(transaction);
694
+ // Delete all items in a single batch operation
695
+ // biome-ignore lint/style/noNonNullAssertion: DB is guaranteed to be ready after readyPromise resolves
696
+ await config.indexedDBRef.current!.delete(config.storeName, keysToDelete);
845
697
  },
846
698
  };
847
699
 
@@ -4,6 +4,7 @@ import {
4
4
  useIndexedDBCollection,
5
5
  type DrizzleIndexedDBContextValue,
6
6
  } from "./DrizzleIndexedDBProvider";
7
+ import type { IDBDatabaseLike } from "../utils";
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: IDBDatabase | null;
15
+ indexedDB: IDBDatabaseLike | null;
15
16
  };
16
17
 
17
18
  export function useDrizzleIndexedDB<