@firtoz/drizzle-indexeddb 0.3.0 ā 0.4.1
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 +44 -0
- package/README.md +162 -0
- package/package.json +11 -5
- package/src/collections/indexeddb-collection.ts +52 -204
- package/src/context/DrizzleIndexedDBProvider.tsx +374 -0
- package/src/context/useDrizzleIndexedDB.ts +1 -1
- package/src/function-migrator.ts +2 -1
- 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 +51 -12
- package/src/instrumented-idb-database.ts +188 -0
- package/src/{utils.ts ā native-idb-database.ts} +44 -214
- 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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# @firtoz/drizzle-indexeddb
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`904019f`](https://github.com/firtoz/fullstack-toolkit/commit/904019f4d04bc02521206fbe0feaeecb67e38f87) Thanks [@firtoz](https://github.com/firtoz)! - Fix tsx exporting
|
|
8
|
+
|
|
9
|
+
## 0.4.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [`46059a2`](https://github.com/firtoz/fullstack-toolkit/commit/46059a28bd0135414b9ed022ffe162a2292adae3) Thanks [@firtoz](https://github.com/firtoz)! - Add IDB Proxy system for multi-client IndexedDB sync over messaging layers:
|
|
14
|
+
|
|
15
|
+
**New Proxy Module** (`@firtoz/drizzle-indexeddb/proxy`):
|
|
16
|
+
|
|
17
|
+
- **`IDBProxyServer`** - Server that manages database lifecycle, migrations, and broadcasts mutations to connected clients
|
|
18
|
+
- **`IDBProxyClient`** - Client implementing `IDBDatabaseLike`, routing operations through a transport layer
|
|
19
|
+
- **`createMultiClientTransport()`** - In-memory transport for testing N clients connected to one server
|
|
20
|
+
- **`createProxyDbCreator()`** - Factory to create `dbCreator` for `DrizzleIndexedDBProvider`
|
|
21
|
+
- **`createCollectionSyncHandler()`** - Adapter connecting proxy sync messages to collection's external sync
|
|
22
|
+
|
|
23
|
+
**Real-time Multi-Client Sync**:
|
|
24
|
+
|
|
25
|
+
- Server broadcasts `sync:add`, `sync:put`, `sync:delete`, `sync:clear` messages to all clients (excluding initiator)
|
|
26
|
+
- All mutations automatically sync across connected clients
|
|
27
|
+
|
|
28
|
+
**Provider Enhancements**:
|
|
29
|
+
|
|
30
|
+
- New `onSyncReady` prop for wiring up external sync handlers
|
|
31
|
+
- `handleProxySync` method routes sync messages to the appropriate collection
|
|
32
|
+
|
|
33
|
+
**Collection Truncate**:
|
|
34
|
+
|
|
35
|
+
- `collection.utils.truncate()` clears all data and syncs to other clients
|
|
36
|
+
- `handleTruncate` implemented in IndexedDB backend
|
|
37
|
+
|
|
38
|
+
**Bug Fixes**:
|
|
39
|
+
|
|
40
|
+
- Server handles concurrent database initialization requests (race condition fix)
|
|
41
|
+
|
|
42
|
+
### Patch Changes
|
|
43
|
+
|
|
44
|
+
- Updated dependencies [[`46059a2`](https://github.com/firtoz/fullstack-toolkit/commit/46059a28bd0135414b9ed022ffe162a2292adae3)]:
|
|
45
|
+
- @firtoz/drizzle-utils@0.3.0
|
|
46
|
+
|
|
3
47
|
## 0.3.0
|
|
4
48
|
|
|
5
49
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ npm install @firtoz/drizzle-indexeddb @firtoz/drizzle-utils drizzle-orm @tanstac
|
|
|
20
20
|
- š¦ **Soft deletes** - Built-in support for `deletedAt` column
|
|
21
21
|
- āļø **React hooks** - Provider and hooks for easy React integration
|
|
22
22
|
- š **Function-based migrations** - Generated migration functions from Drizzle schema changes
|
|
23
|
+
- š **Multi-client sync** - IDB Proxy system for real-time sync across multiple clients (Chrome extensions, etc.)
|
|
23
24
|
|
|
24
25
|
## Quick Start
|
|
25
26
|
|
|
@@ -416,6 +417,167 @@ const collection = createCollection(
|
|
|
416
417
|
);
|
|
417
418
|
```
|
|
418
419
|
|
|
420
|
+
### Collection Truncate
|
|
421
|
+
|
|
422
|
+
Clear all data from a collection:
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
// Clear all todos
|
|
426
|
+
await todoCollection.utils.truncate();
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
This clears the IndexedDB store and updates the local reactive store.
|
|
430
|
+
|
|
431
|
+
## IDB Proxy System
|
|
432
|
+
|
|
433
|
+
For scenarios where IndexedDB needs to be accessed over a messaging layer (e.g., Chrome extensions, WebSockets), the proxy system enables multi-client sync:
|
|
434
|
+
|
|
435
|
+
### Overview
|
|
436
|
+
|
|
437
|
+
```
|
|
438
|
+
āāāāāāāāāāā āāāāāāāāāāā āāāāāāāāāāā
|
|
439
|
+
ā Client 1ā ā Client 2ā ā Client Nā
|
|
440
|
+
āāāāāā¬āāāāā āāāāāā¬āāāāā āāāāāā¬āāāāā
|
|
441
|
+
ā ā ā
|
|
442
|
+
āāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāā
|
|
443
|
+
ā
|
|
444
|
+
āāāāāāāā¼āāāāāāā
|
|
445
|
+
ā Server ā
|
|
446
|
+
ā (manages ā
|
|
447
|
+
ā IndexedDB) ā
|
|
448
|
+
āāāāāāāāāāāāāāā
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
- **Server** manages database lifecycle, migrations, and broadcasts mutations
|
|
452
|
+
- **Clients** connect via a transport layer and receive real-time sync updates
|
|
453
|
+
- All insert/update/delete/truncate operations sync to all connected clients
|
|
454
|
+
|
|
455
|
+
### Basic Setup
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
import {
|
|
459
|
+
createMultiClientTransport,
|
|
460
|
+
createProxyServer,
|
|
461
|
+
createProxyDbCreator,
|
|
462
|
+
migrateIndexedDBWithFunctions,
|
|
463
|
+
DrizzleIndexedDBProvider,
|
|
464
|
+
} from "@firtoz/drizzle-indexeddb";
|
|
465
|
+
|
|
466
|
+
// Create transport (in-memory for testing, or custom for production)
|
|
467
|
+
const { createClientTransport, serverTransport } = createMultiClientTransport();
|
|
468
|
+
|
|
469
|
+
// Create server with migrations
|
|
470
|
+
const server = createProxyServer({
|
|
471
|
+
transport: serverTransport,
|
|
472
|
+
dbCreator: async (dbName) => {
|
|
473
|
+
return await migrateIndexedDBWithFunctions(dbName, migrations);
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Create client
|
|
478
|
+
const clientTransport = createClientTransport();
|
|
479
|
+
const dbCreator = createProxyDbCreator(clientTransport);
|
|
480
|
+
|
|
481
|
+
// Use with React provider
|
|
482
|
+
function App() {
|
|
483
|
+
const handleSyncReady = useCallback((handleSync) => {
|
|
484
|
+
clientTransport.onSync(handleSync);
|
|
485
|
+
}, []);
|
|
486
|
+
|
|
487
|
+
return (
|
|
488
|
+
<DrizzleIndexedDBProvider
|
|
489
|
+
dbName="my-app.db"
|
|
490
|
+
schema={schema}
|
|
491
|
+
dbCreator={dbCreator}
|
|
492
|
+
onSyncReady={handleSyncReady}
|
|
493
|
+
>
|
|
494
|
+
<YourApp />
|
|
495
|
+
</DrizzleIndexedDBProvider>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Multiple Clients
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
// Server setup (once)
|
|
504
|
+
const { createClientTransport, serverTransport } = createMultiClientTransport();
|
|
505
|
+
const server = createProxyServer({ transport: serverTransport, ... });
|
|
506
|
+
|
|
507
|
+
// Each client gets its own transport
|
|
508
|
+
const client1Transport = createClientTransport();
|
|
509
|
+
const client2Transport = createClientTransport();
|
|
510
|
+
const client3Transport = createClientTransport();
|
|
511
|
+
|
|
512
|
+
// All clients share the same data and receive real-time sync
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Sync Operations
|
|
516
|
+
|
|
517
|
+
All standard collection operations automatically sync:
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
// Client 1 inserts
|
|
521
|
+
await todoCollection.insert({ title: "Buy milk", completed: false });
|
|
522
|
+
// ā Client 2, 3, N receive the new todo instantly
|
|
523
|
+
|
|
524
|
+
// Client 2 updates
|
|
525
|
+
await todoCollection.update(todoId, (draft) => {
|
|
526
|
+
draft.completed = true;
|
|
527
|
+
});
|
|
528
|
+
// ā Client 1, 3, N see the update instantly
|
|
529
|
+
|
|
530
|
+
// Client 3 deletes
|
|
531
|
+
await todoCollection.delete(todoId);
|
|
532
|
+
// ā Client 1, 2, N see the deletion instantly
|
|
533
|
+
|
|
534
|
+
// Client N truncates
|
|
535
|
+
await todoCollection.utils.truncate();
|
|
536
|
+
// ā All clients are cleared instantly
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Custom Transport
|
|
540
|
+
|
|
541
|
+
For production use (Chrome extension, WebSocket, etc.), implement the transport interface:
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
import type { IDBProxyClientTransport, IDBProxyServerTransport } from "@firtoz/drizzle-indexeddb";
|
|
545
|
+
|
|
546
|
+
// Client transport (e.g., in content script)
|
|
547
|
+
const clientTransport: IDBProxyClientTransport = {
|
|
548
|
+
sendRequest: async (request) => {
|
|
549
|
+
// Send to background script and wait for response
|
|
550
|
+
return await chrome.runtime.sendMessage(request);
|
|
551
|
+
},
|
|
552
|
+
onSync: (handler) => {
|
|
553
|
+
// Listen for sync broadcasts
|
|
554
|
+
chrome.runtime.onMessage.addListener((msg) => {
|
|
555
|
+
if (msg.type?.startsWith("sync:")) handler(msg);
|
|
556
|
+
});
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// Server transport (e.g., in background script)
|
|
561
|
+
const serverTransport: IDBProxyServerTransport = {
|
|
562
|
+
onRequest: (handler) => {
|
|
563
|
+
chrome.runtime.onMessage.addListener(async (msg, sender, sendResponse) => {
|
|
564
|
+
const response = await handler(msg);
|
|
565
|
+
sendResponse(response);
|
|
566
|
+
});
|
|
567
|
+
},
|
|
568
|
+
broadcast: (message, excludeClientId) => {
|
|
569
|
+
// Broadcast to all connected tabs except sender
|
|
570
|
+
chrome.tabs.query({}, (tabs) => {
|
|
571
|
+
for (const tab of tabs) {
|
|
572
|
+
if (tab.id !== excludeClientId) {
|
|
573
|
+
chrome.tabs.sendMessage(tab.id, message);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
```
|
|
580
|
+
|
|
419
581
|
### Handling Migration Errors
|
|
420
582
|
|
|
421
583
|
```typescript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firtoz/drizzle-indexeddb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "IndexedDB migrations powered by Drizzle ORM",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -24,10 +24,16 @@
|
|
|
24
24
|
"types": "./src/*.ts",
|
|
25
25
|
"import": "./src/*.ts",
|
|
26
26
|
"require": "./src/*.ts"
|
|
27
|
+
},
|
|
28
|
+
"./*.tsx": {
|
|
29
|
+
"types": "./src/*.tsx",
|
|
30
|
+
"import": "./src/*.tsx",
|
|
31
|
+
"require": "./src/*.tsx"
|
|
27
32
|
}
|
|
28
33
|
},
|
|
29
34
|
"files": [
|
|
30
35
|
"src/**/*.ts",
|
|
36
|
+
"src/**/*.tsx",
|
|
31
37
|
"!src/**/*.test.ts",
|
|
32
38
|
"README.md",
|
|
33
39
|
"CHANGELOG.md"
|
|
@@ -64,14 +70,14 @@
|
|
|
64
70
|
"access": "public"
|
|
65
71
|
},
|
|
66
72
|
"dependencies": {
|
|
67
|
-
"@firtoz/drizzle-utils": "^0.
|
|
68
|
-
"@tanstack/db": "^0.5.
|
|
69
|
-
"drizzle-orm": "^0.
|
|
73
|
+
"@firtoz/drizzle-utils": "^0.3.0",
|
|
74
|
+
"@tanstack/db": "^0.5.11",
|
|
75
|
+
"drizzle-orm": "^0.45.0",
|
|
70
76
|
"drizzle-valibot": "^0.4.2",
|
|
71
77
|
"valibot": "^1.2.0"
|
|
72
78
|
},
|
|
73
79
|
"peerDependencies": {
|
|
74
|
-
"react": "^19.2.
|
|
80
|
+
"react": "^19.2.1"
|
|
75
81
|
},
|
|
76
82
|
"devDependencies": {
|
|
77
83
|
"@types/react": "^19.2.7"
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
createCollectionConfig,
|
|
15
15
|
} from "@firtoz/drizzle-utils";
|
|
16
16
|
|
|
17
|
-
import type { IDBDatabaseLike, KeyRangeSpec } from "../
|
|
17
|
+
import type { IDBDatabaseLike, KeyRangeSpec } from "../idb-types";
|
|
18
18
|
|
|
19
19
|
// biome-ignore lint/suspicious/noExplicitAny: intentional
|
|
20
20
|
type AnyId = IdOf<any>;
|
|
@@ -30,73 +30,6 @@ export type IndexedDBSyncItem = {
|
|
|
30
30
|
[key: string]: unknown;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
/**
|
|
34
|
-
* Operation tracking for IndexedDB queries
|
|
35
|
-
* Useful for testing and debugging to verify what operations are actually performed
|
|
36
|
-
*
|
|
37
|
-
* Uses discriminated unions for type safety - TypeScript can narrow the type based on the 'type' field
|
|
38
|
-
*/
|
|
39
|
-
export type IDBOperation =
|
|
40
|
-
| {
|
|
41
|
-
type: "getAll";
|
|
42
|
-
storeName: string;
|
|
43
|
-
itemsReturned: unknown[];
|
|
44
|
-
itemCount: number;
|
|
45
|
-
context: string;
|
|
46
|
-
timestamp: number;
|
|
47
|
-
}
|
|
48
|
-
| {
|
|
49
|
-
type: "index-getAll";
|
|
50
|
-
storeName: string;
|
|
51
|
-
indexName: string;
|
|
52
|
-
keyRange?: IDBKeyRange;
|
|
53
|
-
itemsReturned: unknown[];
|
|
54
|
-
itemCount: number;
|
|
55
|
-
context: string;
|
|
56
|
-
timestamp: number;
|
|
57
|
-
}
|
|
58
|
-
| {
|
|
59
|
-
type: "write";
|
|
60
|
-
storeName: string;
|
|
61
|
-
itemsWritten: unknown[];
|
|
62
|
-
writeCount: number;
|
|
63
|
-
context: string;
|
|
64
|
-
timestamp: number;
|
|
65
|
-
}
|
|
66
|
-
| {
|
|
67
|
-
type: "get";
|
|
68
|
-
storeName: string;
|
|
69
|
-
key: IDBValidKey;
|
|
70
|
-
itemReturned?: unknown;
|
|
71
|
-
timestamp: number;
|
|
72
|
-
}
|
|
73
|
-
| {
|
|
74
|
-
type: "put";
|
|
75
|
-
storeName: string;
|
|
76
|
-
item: unknown;
|
|
77
|
-
timestamp: number;
|
|
78
|
-
}
|
|
79
|
-
| {
|
|
80
|
-
type: "delete";
|
|
81
|
-
storeName: string;
|
|
82
|
-
key: IDBValidKey;
|
|
83
|
-
timestamp: number;
|
|
84
|
-
}
|
|
85
|
-
| {
|
|
86
|
-
type: "clear";
|
|
87
|
-
storeName: string;
|
|
88
|
-
timestamp: number;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Interceptor interface for tracking IndexedDB operations
|
|
93
|
-
* Allows tests and debugging tools to observe what operations are performed
|
|
94
|
-
*/
|
|
95
|
-
export interface IDBInterceptor {
|
|
96
|
-
/** Called when any IndexedDB operation is performed */
|
|
97
|
-
onOperation?: (operation: IDBOperation) => void;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
33
|
export interface IndexedDBCollectionConfig<TTable extends Table> {
|
|
101
34
|
/**
|
|
102
35
|
* Ref to the IndexedDB database instance
|
|
@@ -126,10 +59,6 @@ export interface IndexedDBCollectionConfig<TTable extends Table> {
|
|
|
126
59
|
* Enable debug logging for index discovery and query optimization
|
|
127
60
|
*/
|
|
128
61
|
debug?: boolean;
|
|
129
|
-
/**
|
|
130
|
-
* Optional interceptor for tracking IndexedDB operations (for testing/debugging)
|
|
131
|
-
*/
|
|
132
|
-
interceptor?: IDBInterceptor;
|
|
133
62
|
}
|
|
134
63
|
|
|
135
64
|
/**
|
|
@@ -259,68 +188,6 @@ export function getExpressionValue(
|
|
|
259
188
|
throw new Error(`Cannot get value from expression type: ${expression.type}`);
|
|
260
189
|
}
|
|
261
190
|
|
|
262
|
-
/**
|
|
263
|
-
* Reads all items from an IndexedDB object store
|
|
264
|
-
*/
|
|
265
|
-
async function getAllFromStore(
|
|
266
|
-
db: IDBDatabaseLike,
|
|
267
|
-
storeName: string,
|
|
268
|
-
interceptor?: IDBInterceptor,
|
|
269
|
-
): Promise<IndexedDBSyncItem[]> {
|
|
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
|
-
}
|
|
283
|
-
|
|
284
|
-
return items;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Reads items from an IndexedDB index with an optional key range
|
|
289
|
-
* Note: Index existence is validated at collection creation time
|
|
290
|
-
*/
|
|
291
|
-
async function getAllFromIndex(
|
|
292
|
-
db: IDBDatabaseLike,
|
|
293
|
-
storeName: string,
|
|
294
|
-
indexName: string,
|
|
295
|
-
keyRange?: KeyRangeSpec,
|
|
296
|
-
interceptor?: IDBInterceptor,
|
|
297
|
-
): Promise<IndexedDBSyncItem[]> {
|
|
298
|
-
const items = await db.getAllByIndex<IndexedDBSyncItem>(
|
|
299
|
-
storeName,
|
|
300
|
-
indexName,
|
|
301
|
-
keyRange,
|
|
302
|
-
);
|
|
303
|
-
|
|
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
|
-
}
|
|
320
|
-
|
|
321
|
-
return items;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
191
|
/**
|
|
325
192
|
* Attempts to extract a simple indexed query from an IR expression
|
|
326
193
|
* Returns the field name and key range if the query can be optimized
|
|
@@ -475,12 +342,13 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
475
342
|
const discoverIndexesOnce = async () => {
|
|
476
343
|
await config.readyPromise;
|
|
477
344
|
|
|
345
|
+
const db = config.indexedDBRef.current;
|
|
346
|
+
if (!db) {
|
|
347
|
+
throw new Error("Database not ready");
|
|
348
|
+
}
|
|
349
|
+
|
|
478
350
|
if (!indexesDiscovered) {
|
|
479
|
-
discoveredIndexes = discoverIndexes(
|
|
480
|
-
// biome-ignore lint/style/noNonNullAssertion: DB is guaranteed to be ready after readyPromise resolves
|
|
481
|
-
config.indexedDBRef.current!,
|
|
482
|
-
config.storeName,
|
|
483
|
-
);
|
|
351
|
+
discoveredIndexes = discoverIndexes(db, config.storeName);
|
|
484
352
|
|
|
485
353
|
indexesDiscovered = true;
|
|
486
354
|
}
|
|
@@ -489,26 +357,14 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
489
357
|
// Create backend-specific implementation
|
|
490
358
|
const backend: SyncBackend<TTable> = {
|
|
491
359
|
initialLoad: async (write) => {
|
|
360
|
+
const db = config.indexedDBRef.current;
|
|
361
|
+
if (!db) {
|
|
362
|
+
throw new Error("Database not ready");
|
|
363
|
+
}
|
|
364
|
+
|
|
492
365
|
await discoverIndexesOnce();
|
|
493
366
|
|
|
494
|
-
const items = await
|
|
495
|
-
// biome-ignore lint/style/noNonNullAssertion: DB is guaranteed to be ready after readyPromise resolves
|
|
496
|
-
config.indexedDBRef.current!,
|
|
497
|
-
config.storeName,
|
|
498
|
-
config.interceptor,
|
|
499
|
-
);
|
|
500
|
-
|
|
501
|
-
// Log what's being written to the collection
|
|
502
|
-
if (config.interceptor?.onOperation) {
|
|
503
|
-
config.interceptor.onOperation({
|
|
504
|
-
type: "write",
|
|
505
|
-
storeName: config.storeName,
|
|
506
|
-
itemsWritten: items,
|
|
507
|
-
writeCount: items.length,
|
|
508
|
-
context: "Initial load (eager mode)",
|
|
509
|
-
timestamp: Date.now(),
|
|
510
|
-
});
|
|
511
|
-
}
|
|
367
|
+
const items = await db.getAll<IndexedDBSyncItem>(config.storeName);
|
|
512
368
|
|
|
513
369
|
for (const item of items) {
|
|
514
370
|
write(item as unknown as InferSchemaOutput<SelectSchema<TTable>>);
|
|
@@ -516,13 +372,14 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
516
372
|
},
|
|
517
373
|
|
|
518
374
|
loadSubset: async (options, write) => {
|
|
375
|
+
const db = config.indexedDBRef.current;
|
|
376
|
+
if (!db) {
|
|
377
|
+
throw new Error("Database not ready");
|
|
378
|
+
}
|
|
379
|
+
|
|
519
380
|
// Ensure indexes are discovered before we try to use them
|
|
520
381
|
if (!indexesDiscovered) {
|
|
521
|
-
discoveredIndexes = discoverIndexes(
|
|
522
|
-
// biome-ignore lint/style/noNonNullAssertion: DB is guaranteed to be ready after readyPromise resolves
|
|
523
|
-
config.indexedDBRef.current!,
|
|
524
|
-
config.storeName,
|
|
525
|
-
);
|
|
382
|
+
discoveredIndexes = discoverIndexes(db, config.storeName);
|
|
526
383
|
indexesDiscovered = true;
|
|
527
384
|
}
|
|
528
385
|
|
|
@@ -535,22 +392,15 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
535
392
|
|
|
536
393
|
if (indexedQuery) {
|
|
537
394
|
// Use indexed query for better performance
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
config.indexedDBRef.current!,
|
|
395
|
+
|
|
396
|
+
items = await db.getAllByIndex<IndexedDBSyncItem>(
|
|
541
397
|
config.storeName,
|
|
542
398
|
indexedQuery.indexName,
|
|
543
399
|
indexedQuery.keyRange,
|
|
544
|
-
config.interceptor,
|
|
545
400
|
);
|
|
546
401
|
} else {
|
|
547
402
|
// Fall back to getting all items
|
|
548
|
-
items = await
|
|
549
|
-
// biome-ignore lint/style/noNonNullAssertion: DB is guaranteed to be ready after readyPromise resolves
|
|
550
|
-
config.indexedDBRef.current!,
|
|
551
|
-
config.storeName,
|
|
552
|
-
config.interceptor,
|
|
553
|
-
);
|
|
403
|
+
items = await db.getAll<IndexedDBSyncItem>(config.storeName);
|
|
554
404
|
|
|
555
405
|
// Apply where filter in memory
|
|
556
406
|
if (options.where) {
|
|
@@ -595,37 +445,17 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
595
445
|
items = items.slice(0, options.limit);
|
|
596
446
|
}
|
|
597
447
|
|
|
598
|
-
// Log what's being written to the collection
|
|
599
|
-
if (config.interceptor?.onOperation) {
|
|
600
|
-
const contextParts: string[] = ["On-demand load"];
|
|
601
|
-
if (indexedQuery) {
|
|
602
|
-
contextParts.push(`via index ${indexedQuery.indexName}`);
|
|
603
|
-
} else if (options.where) {
|
|
604
|
-
contextParts.push("via full scan + in-memory filter");
|
|
605
|
-
}
|
|
606
|
-
if (options.orderBy) {
|
|
607
|
-
contextParts.push("with sorting");
|
|
608
|
-
}
|
|
609
|
-
if (options.limit !== undefined) {
|
|
610
|
-
contextParts.push(`limit ${options.limit}`);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
config.interceptor.onOperation({
|
|
614
|
-
type: "write",
|
|
615
|
-
storeName: config.storeName,
|
|
616
|
-
itemsWritten: items,
|
|
617
|
-
writeCount: items.length,
|
|
618
|
-
context: contextParts.join(", "),
|
|
619
|
-
timestamp: Date.now(),
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
|
|
623
448
|
for (const item of items) {
|
|
624
449
|
write(item as unknown as InferSchemaOutput<SelectSchema<TTable>>);
|
|
625
450
|
}
|
|
626
451
|
},
|
|
627
452
|
|
|
628
453
|
handleInsert: async (mutations) => {
|
|
454
|
+
const db = config.indexedDBRef.current;
|
|
455
|
+
if (!db) {
|
|
456
|
+
throw new Error("Database not ready");
|
|
457
|
+
}
|
|
458
|
+
|
|
629
459
|
const results: Array<InferSchemaOutput<SelectSchema<TTable>>> = [];
|
|
630
460
|
|
|
631
461
|
try {
|
|
@@ -638,8 +468,7 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
638
468
|
}
|
|
639
469
|
|
|
640
470
|
// Add all items in a single batch operation
|
|
641
|
-
|
|
642
|
-
await config.indexedDBRef.current!.add(config.storeName, itemsToInsert);
|
|
471
|
+
await db.add(config.storeName, itemsToInsert);
|
|
643
472
|
} catch (error) {
|
|
644
473
|
// Clear results on error so nothing gets written to reactive store
|
|
645
474
|
results.length = 0;
|
|
@@ -650,12 +479,15 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
650
479
|
},
|
|
651
480
|
|
|
652
481
|
handleUpdate: async (mutations) => {
|
|
482
|
+
const db = config.indexedDBRef.current;
|
|
483
|
+
|
|
484
|
+
if (!db) {
|
|
485
|
+
throw new Error("Database not ready");
|
|
486
|
+
}
|
|
487
|
+
|
|
653
488
|
const results: Array<InferSchemaOutput<SelectSchema<TTable>>> = [];
|
|
654
489
|
const itemsToUpdate: IndexedDBSyncItem[] = [];
|
|
655
490
|
|
|
656
|
-
// biome-ignore lint/style/noNonNullAssertion: DB is guaranteed to be ready after readyPromise resolves
|
|
657
|
-
const db = config.indexedDBRef.current!;
|
|
658
|
-
|
|
659
491
|
for (const mutation of mutations) {
|
|
660
492
|
const existing = await db.get<IndexedDBSyncItem>(
|
|
661
493
|
config.storeName,
|
|
@@ -689,11 +521,27 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
689
521
|
},
|
|
690
522
|
|
|
691
523
|
handleDelete: async (mutations) => {
|
|
524
|
+
const db = config.indexedDBRef.current;
|
|
525
|
+
|
|
526
|
+
if (!db) {
|
|
527
|
+
throw new Error("Database not ready");
|
|
528
|
+
}
|
|
529
|
+
|
|
692
530
|
const keysToDelete: IDBValidKey[] = mutations.map((m) => m.key);
|
|
693
531
|
|
|
694
532
|
// Delete all items in a single batch operation
|
|
695
|
-
|
|
696
|
-
|
|
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");
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Clear all items from the store
|
|
544
|
+
await db.clear(config.storeName);
|
|
697
545
|
},
|
|
698
546
|
};
|
|
699
547
|
|