@firtoz/drizzle-indexeddb 0.4.3 → 0.5.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 +32 -0
- package/package.json +9 -9
- package/src/collections/indexeddb-collection.ts +27 -10
- package/src/context/DrizzleIndexedDBProvider.tsx +17 -16
- package/src/index.ts +8 -0
- package/src/standalone-collection.ts +374 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @firtoz/drizzle-indexeddb
|
|
2
2
|
|
|
3
|
+
## 0.5.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`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.
|
|
8
|
+
|
|
9
|
+
- Updated dependencies [[`8abab0a`](https://github.com/firtoz/fullstack-toolkit/commit/8abab0ae7a99320a4254cb128c0fd823726e58e0)]:
|
|
10
|
+
- @firtoz/drizzle-utils@0.3.1
|
|
11
|
+
|
|
12
|
+
## 0.5.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- [`9e532bb`](https://github.com/firtoz/fullstack-toolkit/commit/9e532bbd83bc671c62fd1333ae25fd9829112464) Thanks [@firtoz](https://github.com/firtoz)! - Add `createStandaloneCollection` utility for using IndexedDB collections outside of React context.
|
|
17
|
+
|
|
18
|
+
Features:
|
|
19
|
+
|
|
20
|
+
- Simple API for standalone usage without React providers
|
|
21
|
+
- Async mutation methods (`insert`, `update`, `delete`, `truncate`) that return Promises
|
|
22
|
+
- Sync accessors (`getAll`, `get`, `isReady`)
|
|
23
|
+
- Full access to collection utils (`truncate`, `pushExternalSync`)
|
|
24
|
+
- Automatic database initialization with migration support
|
|
25
|
+
|
|
26
|
+
Also:
|
|
27
|
+
|
|
28
|
+
- Update `IndexedDbCollection` type to use `CollectionUtils` instead of generic `UtilsRecord` for proper typing of `truncate` and `pushExternalSync`
|
|
29
|
+
- Export `IndexedDbCollection` type from package
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- [`c772c2c`](https://github.com/firtoz/fullstack-toolkit/commit/c772c2cf74af560dc04080933591ccd3014f85a1) Thanks [@firtoz](https://github.com/firtoz)! - Improve types returned and simplify internal logic
|
|
34
|
+
|
|
3
35
|
## 0.4.3
|
|
4
36
|
|
|
5
37
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firtoz/drizzle-indexeddb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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.
|
|
72
|
-
"@tanstack/db": ">=0.5.
|
|
73
|
-
"drizzle-orm": ">=0.
|
|
71
|
+
"@firtoz/drizzle-utils": ">=0.3.1",
|
|
72
|
+
"@tanstack/db": ">=0.5.15",
|
|
73
|
+
"drizzle-orm": ">=0.45.1",
|
|
74
74
|
"drizzle-valibot": ">=0.4.0",
|
|
75
|
-
"react": ">=
|
|
75
|
+
"react": ">=19.2.3",
|
|
76
76
|
"valibot": ">=1.0.0"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"@firtoz/drizzle-utils": "^0.3.
|
|
80
|
-
"@tanstack/db": "^0.5.
|
|
79
|
+
"@firtoz/drizzle-utils": "^0.3.1",
|
|
80
|
+
"@tanstack/db": "^0.5.15",
|
|
81
81
|
"@types/react": "^19.2.7",
|
|
82
|
-
"drizzle-orm": "^0.45.
|
|
82
|
+
"drizzle-orm": "^0.45.1",
|
|
83
83
|
"drizzle-valibot": "^0.4.2",
|
|
84
|
-
"react": "^19.2.
|
|
84
|
+
"react": "^19.2.3",
|
|
85
85
|
"valibot": "^1.2.0"
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
@@ -35,10 +35,6 @@ export interface IndexedDBCollectionConfig<TTable extends Table> {
|
|
|
35
35
|
* Ref to the IndexedDB database instance
|
|
36
36
|
*/
|
|
37
37
|
indexedDBRef: React.RefObject<IDBDatabaseLike | null>;
|
|
38
|
-
/**
|
|
39
|
-
* The database name (for perf markers)
|
|
40
|
-
*/
|
|
41
|
-
dbName: string;
|
|
42
38
|
/**
|
|
43
39
|
* The Drizzle table definition (used for schema and type inference only)
|
|
44
40
|
*/
|
|
@@ -385,14 +381,30 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
385
381
|
|
|
386
382
|
let items: IndexedDBSyncItem[];
|
|
387
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
|
+
|
|
388
400
|
// Try to use an index for efficient querying
|
|
389
|
-
const indexedQuery =
|
|
390
|
-
? tryExtractIndexedQuery(
|
|
401
|
+
const indexedQuery = combinedWhere
|
|
402
|
+
? tryExtractIndexedQuery(combinedWhere, discoveredIndexes, config.debug)
|
|
391
403
|
: null;
|
|
392
404
|
|
|
393
405
|
if (indexedQuery) {
|
|
394
406
|
// Use indexed query for better performance
|
|
395
|
-
|
|
407
|
+
// Index returns exact results for single-field queries, no additional filtering needed
|
|
396
408
|
items = await db.getAllByIndex<IndexedDBSyncItem>(
|
|
397
409
|
config.storeName,
|
|
398
410
|
indexedQuery.indexName,
|
|
@@ -402,9 +414,9 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
402
414
|
// Fall back to getting all items
|
|
403
415
|
items = await db.getAll<IndexedDBSyncItem>(config.storeName);
|
|
404
416
|
|
|
405
|
-
// Apply where filter in memory
|
|
406
|
-
if (
|
|
407
|
-
const whereExpression =
|
|
417
|
+
// Apply combined where filter in memory
|
|
418
|
+
if (combinedWhere) {
|
|
419
|
+
const whereExpression = combinedWhere;
|
|
408
420
|
items = items.filter((item) =>
|
|
409
421
|
evaluateExpression(
|
|
410
422
|
whereExpression,
|
|
@@ -440,6 +452,11 @@ export function indexedDBCollectionOptions<const TTable extends Table>(
|
|
|
440
452
|
});
|
|
441
453
|
}
|
|
442
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
|
+
|
|
443
460
|
// Apply limit
|
|
444
461
|
if (options.limit !== undefined) {
|
|
445
462
|
items = items.slice(0, options.limit);
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
import {
|
|
11
11
|
createCollection,
|
|
12
12
|
type InferSchemaInput,
|
|
13
|
-
type UtilsRecord,
|
|
14
13
|
type Collection,
|
|
15
14
|
type InferSchemaOutput,
|
|
16
15
|
type SyncMode,
|
|
@@ -26,7 +25,7 @@ import type {
|
|
|
26
25
|
SelectSchema,
|
|
27
26
|
GetTableFromSchema,
|
|
28
27
|
InferCollectionFromTable,
|
|
29
|
-
|
|
28
|
+
CollectionUtils,
|
|
30
29
|
} from "@firtoz/drizzle-utils";
|
|
31
30
|
import {
|
|
32
31
|
type Migration,
|
|
@@ -38,21 +37,21 @@ import type { IDBProxySyncMessage } from "../proxy/idb-proxy-types";
|
|
|
38
37
|
|
|
39
38
|
interface CollectionCacheEntry {
|
|
40
39
|
// biome-ignore lint/suspicious/noExplicitAny: Cache needs to store collections of various types
|
|
41
|
-
collection: Collection<any, string>;
|
|
40
|
+
collection: Collection<any, string, CollectionUtils<any>, any, any>;
|
|
42
41
|
refCount: number;
|
|
43
|
-
// biome-ignore lint/suspicious/noExplicitAny: External sync needs to accept any item type
|
|
44
|
-
pushExternalSync: ExternalSyncHandler<any>;
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
// Type for migration functions (generated by Drizzle)
|
|
48
45
|
|
|
49
|
-
type IndexedDbCollection<
|
|
46
|
+
export type IndexedDbCollection<
|
|
50
47
|
TSchema extends Record<string, unknown>,
|
|
51
48
|
TTableName extends keyof TSchema & string,
|
|
52
49
|
> = Collection<
|
|
53
50
|
InferSchemaOutput<SelectSchema<GetTableFromSchema<TSchema, TTableName>>>,
|
|
54
51
|
IdOf<GetTableFromSchema<TSchema, TTableName>>,
|
|
55
|
-
|
|
52
|
+
CollectionUtils<
|
|
53
|
+
InferSchemaOutput<SelectSchema<GetTableFromSchema<TSchema, TTableName>>>
|
|
54
|
+
>,
|
|
56
55
|
SelectSchema<GetTableFromSchema<TSchema, TTableName>>,
|
|
57
56
|
InferSchemaInput<InsertSchema<GetTableFromSchema<TSchema, TTableName>>>
|
|
58
57
|
>;
|
|
@@ -195,7 +194,6 @@ export function DrizzleIndexedDBProvider<
|
|
|
195
194
|
// Create collection options
|
|
196
195
|
const collectionConfig = indexedDBCollectionOptions({
|
|
197
196
|
indexedDBRef,
|
|
198
|
-
dbName,
|
|
199
197
|
table,
|
|
200
198
|
storeName: actualTableName,
|
|
201
199
|
readyPromise: readyPromise.promise,
|
|
@@ -205,12 +203,14 @@ export function DrizzleIndexedDBProvider<
|
|
|
205
203
|
|
|
206
204
|
// Create new collection and cache it with ref count 0
|
|
207
205
|
// The collection will wait for readyPromise before accessing the database
|
|
208
|
-
|
|
206
|
+
// Cast is safe: our collectionConfig provides CollectionUtils, but createCollection types it as UtilsRecord
|
|
207
|
+
const collection = createCollection(
|
|
208
|
+
collectionConfig,
|
|
209
|
+
) as CollectionCacheEntry["collection"];
|
|
209
210
|
|
|
210
211
|
collections.set(cacheKey, {
|
|
211
212
|
collection,
|
|
212
213
|
refCount: 0,
|
|
213
|
-
pushExternalSync: collectionConfig.utils.pushExternalSync,
|
|
214
214
|
});
|
|
215
215
|
}
|
|
216
216
|
|
|
@@ -218,7 +218,7 @@ export function DrizzleIndexedDBProvider<
|
|
|
218
218
|
return collections.get(cacheKey)!
|
|
219
219
|
.collection as unknown as IndexedDbCollection<TSchema, TTableName>;
|
|
220
220
|
},
|
|
221
|
-
[collections, schema, readyPromise.promise, debug,
|
|
221
|
+
[collections, schema, readyPromise.promise, debug, syncMode],
|
|
222
222
|
);
|
|
223
223
|
|
|
224
224
|
const incrementRefCount: DrizzleIndexedDBContextValue<TSchema>["incrementRefCount"] =
|
|
@@ -257,30 +257,31 @@ export function DrizzleIndexedDBProvider<
|
|
|
257
257
|
const actualStoreName = getTableName(table as Table);
|
|
258
258
|
if (actualStoreName === message.storeName) {
|
|
259
259
|
const entry = collections.get(tableName);
|
|
260
|
-
if (entry
|
|
260
|
+
if (entry) {
|
|
261
|
+
const { pushExternalSync } = entry.collection.utils;
|
|
261
262
|
// Route sync message to collection
|
|
262
263
|
switch (message.type) {
|
|
263
264
|
case "sync:add":
|
|
264
|
-
|
|
265
|
+
pushExternalSync({
|
|
265
266
|
type: "insert",
|
|
266
267
|
items: message.items,
|
|
267
268
|
});
|
|
268
269
|
break;
|
|
269
270
|
case "sync:put":
|
|
270
|
-
|
|
271
|
+
pushExternalSync({
|
|
271
272
|
type: "update",
|
|
272
273
|
items: message.items,
|
|
273
274
|
});
|
|
274
275
|
break;
|
|
275
276
|
case "sync:delete":
|
|
276
277
|
// For delete, construct items with id
|
|
277
|
-
|
|
278
|
+
pushExternalSync({
|
|
278
279
|
type: "delete",
|
|
279
280
|
items: message.keys.map((key) => ({ id: key })),
|
|
280
281
|
});
|
|
281
282
|
break;
|
|
282
283
|
case "sync:clear":
|
|
283
|
-
|
|
284
|
+
pushExternalSync({
|
|
284
285
|
type: "truncate",
|
|
285
286
|
});
|
|
286
287
|
break;
|
package/src/index.ts
CHANGED
|
@@ -39,12 +39,20 @@ export {
|
|
|
39
39
|
type IndexedDBSyncItem,
|
|
40
40
|
} from "./collections/indexeddb-collection";
|
|
41
41
|
|
|
42
|
+
// Standalone Collection (for use outside React)
|
|
43
|
+
export {
|
|
44
|
+
createStandaloneCollection,
|
|
45
|
+
type StandaloneCollection,
|
|
46
|
+
type StandaloneCollectionConfig,
|
|
47
|
+
} from "./standalone-collection";
|
|
48
|
+
|
|
42
49
|
// IndexedDB Provider
|
|
43
50
|
export {
|
|
44
51
|
DrizzleIndexedDBProvider,
|
|
45
52
|
DrizzleIndexedDBContext,
|
|
46
53
|
useIndexedDBCollection,
|
|
47
54
|
type DrizzleIndexedDBContextValue,
|
|
55
|
+
type IndexedDbCollection,
|
|
48
56
|
} from "./context/DrizzleIndexedDBProvider";
|
|
49
57
|
|
|
50
58
|
export {
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCollection,
|
|
3
|
+
type Collection,
|
|
4
|
+
type InferSchemaInput,
|
|
5
|
+
type InferSchemaOutput,
|
|
6
|
+
type SyncMode,
|
|
7
|
+
type Transaction,
|
|
8
|
+
type WritableDeep,
|
|
9
|
+
} from "@tanstack/db";
|
|
10
|
+
import type { Table } from "drizzle-orm";
|
|
11
|
+
import type {
|
|
12
|
+
IdOf,
|
|
13
|
+
InsertSchema,
|
|
14
|
+
SelectSchema,
|
|
15
|
+
CollectionUtils,
|
|
16
|
+
} from "@firtoz/drizzle-utils";
|
|
17
|
+
import {
|
|
18
|
+
indexedDBCollectionOptions,
|
|
19
|
+
type IndexedDBCollectionConfig,
|
|
20
|
+
} from "./collections/indexeddb-collection";
|
|
21
|
+
import {
|
|
22
|
+
migrateIndexedDBWithFunctions,
|
|
23
|
+
type Migration,
|
|
24
|
+
} from "./function-migrator";
|
|
25
|
+
import { openIndexedDb } from "./idb-operations";
|
|
26
|
+
import type { IDBCreator, IDBDatabaseLike } from "./idb-types";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Configuration for creating a standalone IndexedDB collection
|
|
30
|
+
*/
|
|
31
|
+
export interface StandaloneCollectionConfig<TTable extends Table> {
|
|
32
|
+
/**
|
|
33
|
+
* Name of the IndexedDB database
|
|
34
|
+
*/
|
|
35
|
+
dbName: string;
|
|
36
|
+
/**
|
|
37
|
+
* The Drizzle table definition
|
|
38
|
+
*/
|
|
39
|
+
table: TTable;
|
|
40
|
+
/**
|
|
41
|
+
* The name of the IndexedDB object store (defaults to table name)
|
|
42
|
+
*/
|
|
43
|
+
storeName?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Migrations to apply (optional)
|
|
46
|
+
*/
|
|
47
|
+
migrations?: Migration[];
|
|
48
|
+
/**
|
|
49
|
+
* Custom database creator (for testing/mocking)
|
|
50
|
+
*/
|
|
51
|
+
dbCreator?: IDBCreator;
|
|
52
|
+
/**
|
|
53
|
+
* Enable debug logging
|
|
54
|
+
*/
|
|
55
|
+
debug?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Sync mode: 'eager' (immediate) or 'lazy' (on-demand)
|
|
58
|
+
*/
|
|
59
|
+
syncMode?: SyncMode;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Type for the underlying collection
|
|
64
|
+
*/
|
|
65
|
+
type InternalCollection<TTable extends Table> = Collection<
|
|
66
|
+
InferSchemaOutput<SelectSchema<TTable>>,
|
|
67
|
+
IdOf<TTable>,
|
|
68
|
+
CollectionUtils<InferSchemaOutput<SelectSchema<TTable>>>,
|
|
69
|
+
SelectSchema<TTable>,
|
|
70
|
+
InferSchemaInput<InsertSchema<TTable>>
|
|
71
|
+
>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Transaction type for mutations
|
|
75
|
+
*/
|
|
76
|
+
type MutationTransaction<TTable extends Table> = Transaction<
|
|
77
|
+
InferSchemaOutput<SelectSchema<TTable>>
|
|
78
|
+
>;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Insert input type (what you pass to insert)
|
|
82
|
+
*/
|
|
83
|
+
type InsertInput<TTable extends Table> = InferSchemaInput<InsertSchema<TTable>>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Item type (what you get back from getAll, etc.)
|
|
87
|
+
*/
|
|
88
|
+
type ItemType<TTable extends Table> = InferSchemaOutput<SelectSchema<TTable>>;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Writable draft type for update callbacks
|
|
92
|
+
*/
|
|
93
|
+
type DraftType<TTable extends Table> = WritableDeep<InsertInput<TTable>>;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Standalone IndexedDB collection API
|
|
97
|
+
*/
|
|
98
|
+
export interface StandaloneCollection<TTable extends Table> {
|
|
99
|
+
/**
|
|
100
|
+
* Promise that resolves when the collection is ready
|
|
101
|
+
*/
|
|
102
|
+
ready: Promise<void>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if the collection is ready (sync)
|
|
106
|
+
*/
|
|
107
|
+
isReady(): boolean;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get all items (sync - returns current state)
|
|
111
|
+
*/
|
|
112
|
+
getAll(): ItemType<TTable>[];
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get an item by key (sync)
|
|
116
|
+
*/
|
|
117
|
+
get(key: IdOf<TTable>): ItemType<TTable> | undefined;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Insert item(s)
|
|
121
|
+
* @returns Promise that resolves when persisted
|
|
122
|
+
*/
|
|
123
|
+
insert(
|
|
124
|
+
data: InsertInput<TTable> | InsertInput<TTable>[],
|
|
125
|
+
callback?: (transaction: MutationTransaction<TTable>) => void,
|
|
126
|
+
): Promise<MutationTransaction<TTable>>;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Update an item by key using a callback that receives a draft
|
|
130
|
+
* @returns Promise that resolves when persisted
|
|
131
|
+
*/
|
|
132
|
+
update(
|
|
133
|
+
key: IdOf<TTable>,
|
|
134
|
+
updater: (draft: DraftType<TTable>) => void,
|
|
135
|
+
callback?: (transaction: MutationTransaction<TTable>) => void,
|
|
136
|
+
): Promise<MutationTransaction<TTable>>;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Delete item(s) by key
|
|
140
|
+
* @returns Promise that resolves when persisted
|
|
141
|
+
*/
|
|
142
|
+
delete(
|
|
143
|
+
key: IdOf<TTable> | IdOf<TTable>[],
|
|
144
|
+
callback?: (transaction: MutationTransaction<TTable>) => void,
|
|
145
|
+
): Promise<MutationTransaction<TTable>>;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Clear all items from the store
|
|
149
|
+
* @returns Promise that resolves when truncate is complete
|
|
150
|
+
*/
|
|
151
|
+
truncate(): Promise<void>;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Access to collection utils (truncate, pushExternalSync)
|
|
155
|
+
*/
|
|
156
|
+
utils: CollectionUtils<ItemType<TTable>>;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The underlying TanStack DB collection (for advanced usage)
|
|
160
|
+
*/
|
|
161
|
+
collection: InternalCollection<TTable>;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The IndexedDB database instance (available after ready)
|
|
165
|
+
*/
|
|
166
|
+
db: IDBDatabaseLike | null;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Close the database connection
|
|
170
|
+
*/
|
|
171
|
+
close(): void;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Create a standalone IndexedDB collection for use outside of React.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* const db = await createStandaloneCollection({
|
|
180
|
+
* dbName: "myapp.db",
|
|
181
|
+
* table: schema.todos,
|
|
182
|
+
* migrations,
|
|
183
|
+
* });
|
|
184
|
+
*
|
|
185
|
+
* // Wait for ready
|
|
186
|
+
* await db.ready;
|
|
187
|
+
*
|
|
188
|
+
* // Get all items
|
|
189
|
+
* const items = db.getAll();
|
|
190
|
+
*
|
|
191
|
+
* // Insert
|
|
192
|
+
* await db.insert({ title: "New todo" });
|
|
193
|
+
*
|
|
194
|
+
* // Update
|
|
195
|
+
* await db.update(itemId, { title: "Updated" });
|
|
196
|
+
*
|
|
197
|
+
* // Delete
|
|
198
|
+
* await db.delete(itemId);
|
|
199
|
+
*
|
|
200
|
+
* // Truncate
|
|
201
|
+
* await db.truncate();
|
|
202
|
+
*
|
|
203
|
+
* // Clean up
|
|
204
|
+
* db.close();
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
export function createStandaloneCollection<TTable extends Table>(
|
|
208
|
+
config: StandaloneCollectionConfig<TTable>,
|
|
209
|
+
): StandaloneCollection<TTable> {
|
|
210
|
+
const {
|
|
211
|
+
dbName,
|
|
212
|
+
table,
|
|
213
|
+
storeName = (table as unknown as { _: { name: string } })._.name,
|
|
214
|
+
migrations = [],
|
|
215
|
+
dbCreator,
|
|
216
|
+
debug = false,
|
|
217
|
+
syncMode = "eager",
|
|
218
|
+
} = config;
|
|
219
|
+
|
|
220
|
+
// Create ready promise
|
|
221
|
+
let resolveReady: () => void;
|
|
222
|
+
const readyPromise = new Promise<void>((resolve) => {
|
|
223
|
+
resolveReady = resolve;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Database ref
|
|
227
|
+
const indexedDBRef: { current: IDBDatabaseLike | null } = { current: null };
|
|
228
|
+
|
|
229
|
+
// Initialize database
|
|
230
|
+
const initDB = async () => {
|
|
231
|
+
try {
|
|
232
|
+
if (migrations.length === 0) {
|
|
233
|
+
if (debug) {
|
|
234
|
+
console.log(
|
|
235
|
+
`[StandaloneCollection] Opening database "${dbName}" directly`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
indexedDBRef.current = await openIndexedDb(dbName, dbCreator);
|
|
239
|
+
} else {
|
|
240
|
+
if (debug) {
|
|
241
|
+
console.log(`[StandaloneCollection] Migrating database "${dbName}"`);
|
|
242
|
+
}
|
|
243
|
+
indexedDBRef.current = await migrateIndexedDBWithFunctions(
|
|
244
|
+
dbName,
|
|
245
|
+
migrations,
|
|
246
|
+
debug,
|
|
247
|
+
dbCreator,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (debug) {
|
|
252
|
+
console.log(`[StandaloneCollection] Database "${dbName}" initialized`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
resolveReady();
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error(
|
|
258
|
+
`[StandaloneCollection] Failed to initialize database "${dbName}":`,
|
|
259
|
+
error,
|
|
260
|
+
);
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// Start initialization
|
|
266
|
+
initDB();
|
|
267
|
+
|
|
268
|
+
// Create collection config
|
|
269
|
+
const collectionConfig = indexedDBCollectionOptions({
|
|
270
|
+
indexedDBRef,
|
|
271
|
+
table,
|
|
272
|
+
storeName,
|
|
273
|
+
readyPromise,
|
|
274
|
+
debug,
|
|
275
|
+
syncMode,
|
|
276
|
+
} as IndexedDBCollectionConfig<TTable>);
|
|
277
|
+
|
|
278
|
+
// Create the collection
|
|
279
|
+
const collection = createCollection(
|
|
280
|
+
collectionConfig,
|
|
281
|
+
) as unknown as InternalCollection<TTable>;
|
|
282
|
+
|
|
283
|
+
// Wait for collection to be ready
|
|
284
|
+
const collectionReady = new Promise<void>((resolve) => {
|
|
285
|
+
if (collection.isReady()) {
|
|
286
|
+
resolve();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
collection.preload();
|
|
290
|
+
collection.onFirstReady(() => resolve());
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Combined ready promise
|
|
294
|
+
const ready = Promise.all([readyPromise, collectionReady]).then(() => {});
|
|
295
|
+
|
|
296
|
+
// Helper to wait for transaction to persist
|
|
297
|
+
const waitForPersist = async (
|
|
298
|
+
transaction: MutationTransaction<TTable>,
|
|
299
|
+
callback?: (transaction: MutationTransaction<TTable>) => void,
|
|
300
|
+
): Promise<MutationTransaction<TTable>> => {
|
|
301
|
+
if (callback) {
|
|
302
|
+
callback(transaction);
|
|
303
|
+
}
|
|
304
|
+
await transaction.isPersisted.promise;
|
|
305
|
+
return transaction;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
ready,
|
|
310
|
+
|
|
311
|
+
isReady(): boolean {
|
|
312
|
+
return collection.isReady();
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
getAll(): ItemType<TTable>[] {
|
|
316
|
+
return collection.toArray;
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
get(key: IdOf<TTable>): ItemType<TTable> | undefined {
|
|
320
|
+
return collection.state.get(key);
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
insert(
|
|
324
|
+
data: InsertInput<TTable> | InsertInput<TTable>[],
|
|
325
|
+
callback?: (transaction: MutationTransaction<TTable>) => void,
|
|
326
|
+
): Promise<MutationTransaction<TTable>> {
|
|
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>;
|
|
333
|
+
return waitForPersist(transaction, callback);
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
update(
|
|
337
|
+
key: IdOf<TTable>,
|
|
338
|
+
updater: (draft: DraftType<TTable>) => void,
|
|
339
|
+
callback?: (transaction: MutationTransaction<TTable>) => void,
|
|
340
|
+
): Promise<MutationTransaction<TTable>> {
|
|
341
|
+
const transaction = collection.update(
|
|
342
|
+
key,
|
|
343
|
+
updater,
|
|
344
|
+
) as MutationTransaction<TTable>;
|
|
345
|
+
return waitForPersist(transaction, callback);
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
delete(
|
|
349
|
+
key: IdOf<TTable> | IdOf<TTable>[],
|
|
350
|
+
callback?: (transaction: MutationTransaction<TTable>) => void,
|
|
351
|
+
): Promise<MutationTransaction<TTable>> {
|
|
352
|
+
const keys = Array.isArray(key) ? key : [key];
|
|
353
|
+
const transaction = collection.delete(keys);
|
|
354
|
+
return waitForPersist(transaction, callback);
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
truncate(): Promise<void> {
|
|
358
|
+
return collection.utils.truncate();
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
utils: collection.utils,
|
|
362
|
+
|
|
363
|
+
collection,
|
|
364
|
+
|
|
365
|
+
get db(): IDBDatabaseLike | null {
|
|
366
|
+
return indexedDBRef.current;
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
close(): void {
|
|
370
|
+
indexedDBRef.current?.close();
|
|
371
|
+
indexedDBRef.current = null;
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|