@firtoz/drizzle-indexeddb 0.5.0 → 0.6.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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # @firtoz/drizzle-indexeddb
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`a08a986`](https://github.com/firtoz/fullstack-toolkit/commit/a08a986cc5161b20c9c875328e49565c15417ffc) Thanks [@firtoz](https://github.com/firtoz)! - Slight refactor
8
+
9
+ - Renamed `createProxyDbCreator` to `createProxyIDbCreator` for consistency across the codebase.
10
+ - Updated server sync message type from `sync:clear` to `sync:truncate` to better reflect its functionality.
11
+ - Adjusted related documentation and test cases to align with these changes.
12
+
13
+ ## 0.5.1
14
+
15
+ ### Patch Changes
16
+
17
+ - [`8abab0a`](https://github.com/firtoz/fullstack-toolkit/commit/8abab0ae7a99320a4254cb128c0fd823726e58e0) Thanks [@firtoz](https://github.com/firtoz)! - Add cursor-based and offset-based pagination support to `loadSubset` operations, enabling efficient navigation through large datasets with consistent behavior across collection backends.
18
+
19
+ - Updated dependencies [[`8abab0a`](https://github.com/firtoz/fullstack-toolkit/commit/8abab0ae7a99320a4254cb128c0fd823726e58e0)]:
20
+ - @firtoz/drizzle-utils@0.3.1
21
+
3
22
  ## 0.5.0
4
23
 
5
24
  ### Minor Changes
@@ -52,12 +71,12 @@
52
71
  - **`IDBProxyServer`** - Server that manages database lifecycle, migrations, and broadcasts mutations to connected clients
53
72
  - **`IDBProxyClient`** - Client implementing `IDBDatabaseLike`, routing operations through a transport layer
54
73
  - **`createMultiClientTransport()`** - In-memory transport for testing N clients connected to one server
55
- - **`createProxyDbCreator()`** - Factory to create `dbCreator` for `DrizzleIndexedDBProvider`
74
+ - **`createProxyIDbCreator()`** - Factory to create `dbCreator` for `DrizzleIndexedDBProvider`
56
75
  - **`createCollectionSyncHandler()`** - Adapter connecting proxy sync messages to collection's external sync
57
76
 
58
77
  **Real-time Multi-Client Sync**:
59
78
 
60
- - Server broadcasts `sync:add`, `sync:put`, `sync:delete`, `sync:clear` messages to all clients (excluding initiator)
79
+ - Server broadcasts `sync:add`, `sync:put`, `sync:delete`, `sync:truncate` messages to all clients (excluding initiator)
61
80
  - All mutations automatically sync across connected clients
62
81
 
63
82
  **Provider Enhancements**:
package/README.md CHANGED
@@ -460,7 +460,7 @@ For scenarios where IndexedDB needs to be accessed over a messaging layer (e.g.,
460
460
  import {
461
461
  createMultiClientTransport,
462
462
  createProxyServer,
463
- createProxyDbCreator,
463
+ createProxyIDbCreator,
464
464
  migrateIndexedDBWithFunctions,
465
465
  DrizzleIndexedDBProvider,
466
466
  } from "@firtoz/drizzle-indexeddb";
@@ -478,7 +478,7 @@ const server = createProxyServer({
478
478
 
479
479
  // Create client
480
480
  const clientTransport = createClientTransport();
481
- const dbCreator = createProxyDbCreator(clientTransport);
481
+ const dbCreator = createProxyIDbCreator(clientTransport);
482
482
 
483
483
  // Use with React provider
484
484
  function App() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firtoz/drizzle-indexeddb",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "IndexedDB migrations powered by Drizzle ORM",
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -68,20 +68,20 @@
68
68
  "access": "public"
69
69
  },
70
70
  "peerDependencies": {
71
- "@firtoz/drizzle-utils": ">=0.3.0",
72
- "@tanstack/db": ">=0.5.0",
73
- "drizzle-orm": ">=0.44.0",
71
+ "@firtoz/drizzle-utils": ">=0.3.1",
72
+ "@tanstack/db": ">=0.5.16",
73
+ "drizzle-orm": ">=0.45.1",
74
74
  "drizzle-valibot": ">=0.4.0",
75
- "react": ">=18.0.0",
75
+ "react": ">=19.2.3",
76
76
  "valibot": ">=1.0.0"
77
77
  },
78
78
  "devDependencies": {
79
- "@firtoz/drizzle-utils": "^0.3.0",
80
- "@tanstack/db": "^0.5.11",
79
+ "@firtoz/drizzle-utils": "^0.3.1",
80
+ "@tanstack/db": "^0.5.16",
81
81
  "@types/react": "^19.2.7",
82
- "drizzle-orm": "^0.45.0",
82
+ "drizzle-orm": "^0.45.1",
83
83
  "drizzle-valibot": "^0.4.2",
84
- "react": "^19.2.1",
84
+ "react": "^19.2.3",
85
85
  "valibot": "^1.2.0"
86
86
  },
87
87
  "dependencies": {
@@ -381,14 +381,30 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
381
381
 
382
382
  let items: IndexedDBSyncItem[];
383
383
 
384
+ // Combine where with cursor expressions if present
385
+ // The cursor.whereFrom gives us rows after the cursor position
386
+ let combinedWhere = options.where;
387
+ if (options.cursor?.whereFrom) {
388
+ if (combinedWhere) {
389
+ // Combine main where with cursor expression using AND
390
+ combinedWhere = {
391
+ type: "func",
392
+ name: "and",
393
+ args: [combinedWhere, options.cursor.whereFrom],
394
+ } as IR.Func;
395
+ } else {
396
+ combinedWhere = options.cursor.whereFrom;
397
+ }
398
+ }
399
+
384
400
  // Try to use an index for efficient querying
385
- const indexedQuery = options.where
386
- ? tryExtractIndexedQuery(options.where, discoveredIndexes, config.debug)
401
+ const indexedQuery = combinedWhere
402
+ ? tryExtractIndexedQuery(combinedWhere, discoveredIndexes, config.debug)
387
403
  : null;
388
404
 
389
405
  if (indexedQuery) {
390
406
  // Use indexed query for better performance
391
-
407
+ // Index returns exact results for single-field queries, no additional filtering needed
392
408
  items = await db.getAllByIndex<IndexedDBSyncItem>(
393
409
  config.storeName,
394
410
  indexedQuery.indexName,
@@ -398,9 +414,9 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
398
414
  // Fall back to getting all items
399
415
  items = await db.getAll<IndexedDBSyncItem>(config.storeName);
400
416
 
401
- // Apply where filter in memory
402
- if (options.where) {
403
- const whereExpression = options.where;
417
+ // Apply combined where filter in memory
418
+ if (combinedWhere) {
419
+ const whereExpression = combinedWhere;
404
420
  items = items.filter((item) =>
405
421
  evaluateExpression(
406
422
  whereExpression,
@@ -436,6 +452,11 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
436
452
  });
437
453
  }
438
454
 
455
+ // Apply offset (skip first N items for pagination)
456
+ if (options.offset !== undefined && options.offset > 0) {
457
+ items = items.slice(options.offset);
458
+ }
459
+
439
460
  // Apply limit
440
461
  if (options.limit !== undefined) {
441
462
  items = items.slice(0, options.limit);
@@ -446,32 +467,16 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
446
467
  }
447
468
  },
448
469
 
449
- handleInsert: async (mutations) => {
470
+ handleInsert: async (itemsToInsert) => {
450
471
  const db = config.indexedDBRef.current;
451
472
  if (!db) {
452
473
  throw new Error("Database not ready");
453
474
  }
454
475
 
455
- const results: Array<InferSchemaOutput<SelectSchema<TTable>>> = [];
456
-
457
- try {
458
- const itemsToInsert: IndexedDBSyncItem[] = [];
459
-
460
- for (const mutation of mutations) {
461
- const itemToInsert = mutation.modified;
462
- results.push(itemToInsert);
463
- itemsToInsert.push(itemToInsert as IndexedDBSyncItem);
464
- }
476
+ // Add all items in a single batch operation
477
+ await db.add(config.storeName, itemsToInsert);
465
478
 
466
- // Add all items in a single batch operation
467
- await db.add(config.storeName, itemsToInsert);
468
- } catch (error) {
469
- // Clear results on error so nothing gets written to reactive store
470
- results.length = 0;
471
- throw error;
472
- }
473
-
474
- return results;
479
+ return itemsToInsert;
475
480
  },
476
481
 
477
482
  handleUpdate: async (mutations) => {
@@ -280,7 +280,7 @@ export function DrizzleIndexedDBProvider<
280
280
  items: message.keys.map((key) => ({ id: key })),
281
281
  });
282
282
  break;
283
- case "sync:clear":
283
+ case "sync:truncate":
284
284
  pushExternalSync({
285
285
  type: "truncate",
286
286
  });
package/src/index.ts CHANGED
@@ -76,7 +76,7 @@ export {
76
76
  createMultiClientTransport,
77
77
  // Client
78
78
  IDBProxyClient,
79
- createProxyDbCreator,
79
+ createProxyIDbCreator,
80
80
  type SyncHandler,
81
81
  // Server
82
82
  IDBProxyServer,
@@ -1,7 +1,6 @@
1
1
  import type {
2
2
  IDBDatabaseLike,
3
3
  IDBCreator,
4
- IDBOpenOptions,
5
4
  IndexInfo,
6
5
  CreateStoreOptions,
7
6
  CreateIndexOptions,
@@ -294,13 +293,13 @@ export class IDBProxyClient implements IDBDatabaseLike {
294
293
  * @param onSync Optional handler called when any sync message is received
295
294
  *
296
295
  * @example
297
- * const dbCreator = createProxyDbCreator(transport, (msg) => {
296
+ * const dbCreator = createProxyIDbCreator(transport, (msg) => {
298
297
  * console.log('Sync:', msg.type, msg.storeName);
299
298
  * });
300
299
  *
301
300
  * <DrizzleIndexedDBProvider dbCreator={dbCreator} ... />
302
301
  */
303
- export function createProxyDbCreator(
302
+ export function createProxyIDbCreator(
304
303
  transport: IDBProxyClientTransport,
305
304
  onSync?: SyncHandler,
306
305
  ): IDBCreator {
@@ -308,10 +307,7 @@ export function createProxyDbCreator(
308
307
  const clientCache = new Map<string, IDBProxyClient>();
309
308
  const connectingCache = new Map<string, Promise<IDBProxyClient>>();
310
309
 
311
- return async (
312
- name: string,
313
- _options?: IDBOpenOptions,
314
- ): Promise<IDBDatabaseLike> => {
310
+ return async (name: string): Promise<IDBDatabaseLike> => {
315
311
  // Return cached client if already connected
316
312
  const cached = clientCache.get(name);
317
313
  if (cached) {
@@ -224,7 +224,7 @@ export class IDBProxyServer {
224
224
  // Broadcast to other clients
225
225
  this.options.transport.broadcast(
226
226
  {
227
- type: "sync:clear",
227
+ type: "sync:truncate",
228
228
  dbName: request.dbName,
229
229
  storeName: request.storeName,
230
230
  },
@@ -59,7 +59,7 @@ export type IDBProxySyncMessage = {
59
59
  | { type: "sync:add"; items: unknown[] }
60
60
  | { type: "sync:put"; items: unknown[] }
61
61
  | { type: "sync:delete"; keys: IDBValidKey[] }
62
- | { type: "sync:clear" }
62
+ | { type: "sync:truncate" }
63
63
  );
64
64
 
65
65
  /**
@@ -65,7 +65,7 @@ export function createCollectionSyncHandler<T = unknown>(
65
65
  });
66
66
  break;
67
67
 
68
- case "sync:clear":
68
+ case "sync:truncate":
69
69
  pushExternalSync({
70
70
  type: "truncate",
71
71
  });
@@ -19,7 +19,7 @@ export {
19
19
  // Proxy client
20
20
  export {
21
21
  IDBProxyClient,
22
- createProxyDbCreator,
22
+ createProxyIDbCreator,
23
23
  type SyncHandler,
24
24
  } from "./idb-proxy-client";
25
25
 
@@ -251,8 +251,8 @@ export function createStandaloneCollection<TTable extends Table>(
251
251
  if (debug) {
252
252
  console.log(`[StandaloneCollection] Database "${dbName}" initialized`);
253
253
  }
254
- // biome-ignore lint/style/noNonNullAssertion: resolveReady is set in promise constructor
255
- resolveReady!();
254
+
255
+ resolveReady();
256
256
  } catch (error) {
257
257
  console.error(
258
258
  `[StandaloneCollection] Failed to initialize database "${dbName}":`,
@@ -294,16 +294,15 @@ export function createStandaloneCollection<TTable extends Table>(
294
294
  const ready = Promise.all([readyPromise, collectionReady]).then(() => {});
295
295
 
296
296
  // Helper to wait for transaction to persist
297
- const waitForPersist = (
298
- // biome-ignore lint/suspicious/noExplicitAny: Transaction types are complex, runtime is correct
299
- transaction: any,
300
- // biome-ignore lint/suspicious/noExplicitAny: Transaction types are complex, runtime is correct
301
- callback?: (transaction: any) => void,
297
+ const waitForPersist = async (
298
+ transaction: MutationTransaction<TTable>,
299
+ callback?: (transaction: MutationTransaction<TTable>) => void,
302
300
  ): Promise<MutationTransaction<TTable>> => {
303
301
  if (callback) {
304
302
  callback(transaction);
305
303
  }
306
- return transaction.isPersisted.promise.then(() => transaction);
304
+ await transaction.isPersisted.promise;
305
+ return transaction;
307
306
  };
308
307
 
309
308
  return {
@@ -325,9 +324,12 @@ export function createStandaloneCollection<TTable extends Table>(
325
324
  data: InsertInput<TTable> | InsertInput<TTable>[],
326
325
  callback?: (transaction: MutationTransaction<TTable>) => void,
327
326
  ): Promise<MutationTransaction<TTable>> {
328
- const items = Array.isArray(data) ? data : [data];
329
- // @ts-expect-error - Type inference is complex here but runtime is correct
330
- const transaction = collection.insert(...items);
327
+ const items = (Array.isArray(data) ? data : [data]) as InferSchemaOutput<
328
+ SelectSchema<TTable>
329
+ >;
330
+ const transaction = collection.insert(
331
+ items,
332
+ ) as MutationTransaction<TTable>;
331
333
  return waitForPersist(transaction, callback);
332
334
  },
333
335
 
@@ -338,8 +340,8 @@ export function createStandaloneCollection<TTable extends Table>(
338
340
  ): Promise<MutationTransaction<TTable>> {
339
341
  const transaction = collection.update(
340
342
  key,
341
- updater as (draft: any) => void,
342
- );
343
+ updater,
344
+ ) as MutationTransaction<TTable>;
343
345
  return waitForPersist(transaction, callback);
344
346
  },
345
347