@firtoz/drizzle-indexeddb 0.4.0 → 0.4.2
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 +12 -0
- package/package.json +21 -13
- package/src/context/DrizzleIndexedDBProvider.tsx +365 -0
- package/src/context/useDrizzleIndexedDB.ts +9 -4
- package/src/idb-operations.ts +8 -3
- package/src/native-idb-database.ts +119 -78
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @firtoz/drizzle-indexeddb
|
|
2
2
|
|
|
3
|
+
## 0.4.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`58afa0a`](https://github.com/firtoz/fullstack-toolkit/commit/58afa0a5365f55f536e50194a73f847293102e7f) Thanks [@firtoz](https://github.com/firtoz)! - Hopefully this should work
|
|
8
|
+
|
|
9
|
+
## 0.4.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [`904019f`](https://github.com/firtoz/fullstack-toolkit/commit/904019f4d04bc02521206fbe0feaeecb67e38f87) Thanks [@firtoz](https://github.com/firtoz)! - Fix tsx exporting
|
|
14
|
+
|
|
3
15
|
## 0.4.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firtoz/drizzle-indexeddb",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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"
|
|
@@ -36,9 +42,7 @@
|
|
|
36
42
|
"typecheck": "tsc --noEmit -p ./tsconfig.json",
|
|
37
43
|
"lint": "biome check --write src",
|
|
38
44
|
"lint:ci": "biome ci src",
|
|
39
|
-
"format": "biome format src --write"
|
|
40
|
-
"test": "bun test --pass-with-no-tests",
|
|
41
|
-
"test:watch": "bun test --watch"
|
|
45
|
+
"format": "biome format src --write"
|
|
42
46
|
},
|
|
43
47
|
"keywords": [
|
|
44
48
|
"typescript",
|
|
@@ -63,17 +67,21 @@
|
|
|
63
67
|
"publishConfig": {
|
|
64
68
|
"access": "public"
|
|
65
69
|
},
|
|
66
|
-
"dependencies": {
|
|
67
|
-
"@firtoz/drizzle-utils": "^0.3.0",
|
|
68
|
-
"@tanstack/db": "^0.5.10",
|
|
69
|
-
"drizzle-orm": "^0.44.7",
|
|
70
|
-
"drizzle-valibot": "^0.4.2",
|
|
71
|
-
"valibot": "^1.2.0"
|
|
72
|
-
},
|
|
73
70
|
"peerDependencies": {
|
|
74
|
-
"
|
|
71
|
+
"@firtoz/drizzle-utils": ">=0.3.0",
|
|
72
|
+
"@tanstack/db": ">=0.5.0",
|
|
73
|
+
"drizzle-orm": ">=0.44.0",
|
|
74
|
+
"drizzle-valibot": ">=0.4.0",
|
|
75
|
+
"react": ">=18.0.0",
|
|
76
|
+
"valibot": ">=1.0.0"
|
|
75
77
|
},
|
|
76
78
|
"devDependencies": {
|
|
77
|
-
"@
|
|
79
|
+
"@firtoz/drizzle-utils": "^0.3.0",
|
|
80
|
+
"@tanstack/db": "^0.5.11",
|
|
81
|
+
"@types/react": "^19.2.7",
|
|
82
|
+
"drizzle-orm": "^0.45.0",
|
|
83
|
+
"drizzle-valibot": "^0.4.2",
|
|
84
|
+
"react": "^19.2.1",
|
|
85
|
+
"valibot": "^1.2.0"
|
|
78
86
|
}
|
|
79
87
|
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import type { PropsWithChildren } from "react";
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useMemo,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useState,
|
|
8
|
+
useRef,
|
|
9
|
+
} from "react";
|
|
10
|
+
import {
|
|
11
|
+
createCollection,
|
|
12
|
+
type InferSchemaInput,
|
|
13
|
+
type UtilsRecord,
|
|
14
|
+
type Collection,
|
|
15
|
+
type InferSchemaOutput,
|
|
16
|
+
type SyncMode,
|
|
17
|
+
} from "@tanstack/db";
|
|
18
|
+
import { getTableName, type Table } from "drizzle-orm";
|
|
19
|
+
import {
|
|
20
|
+
indexedDBCollectionOptions,
|
|
21
|
+
type IndexedDBCollectionConfig,
|
|
22
|
+
} from "@firtoz/drizzle-indexeddb";
|
|
23
|
+
import type {
|
|
24
|
+
IdOf,
|
|
25
|
+
InsertSchema,
|
|
26
|
+
SelectSchema,
|
|
27
|
+
GetTableFromSchema,
|
|
28
|
+
InferCollectionFromTable,
|
|
29
|
+
ExternalSyncHandler,
|
|
30
|
+
} from "@firtoz/drizzle-utils";
|
|
31
|
+
import {
|
|
32
|
+
type Migration,
|
|
33
|
+
migrateIndexedDBWithFunctions,
|
|
34
|
+
} from "../function-migrator";
|
|
35
|
+
import type { IDBCreator, IDBDatabaseLike } from "../idb-types";
|
|
36
|
+
import { openIndexedDb } from "../idb-operations";
|
|
37
|
+
import type { IDBProxySyncMessage } from "../proxy/idb-proxy-types";
|
|
38
|
+
|
|
39
|
+
interface CollectionCacheEntry {
|
|
40
|
+
// biome-ignore lint/suspicious/noExplicitAny: Cache needs to store collections of various types
|
|
41
|
+
collection: Collection<any, string>;
|
|
42
|
+
refCount: number;
|
|
43
|
+
// biome-ignore lint/suspicious/noExplicitAny: External sync needs to accept any item type
|
|
44
|
+
pushExternalSync: ExternalSyncHandler<any>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Type for migration functions (generated by Drizzle)
|
|
48
|
+
|
|
49
|
+
type IndexedDbCollection<
|
|
50
|
+
TSchema extends Record<string, unknown>,
|
|
51
|
+
TTableName extends keyof TSchema & string,
|
|
52
|
+
> = Collection<
|
|
53
|
+
InferSchemaOutput<SelectSchema<GetTableFromSchema<TSchema, TTableName>>>,
|
|
54
|
+
IdOf<GetTableFromSchema<TSchema, TTableName>>,
|
|
55
|
+
UtilsRecord,
|
|
56
|
+
SelectSchema<GetTableFromSchema<TSchema, TTableName>>,
|
|
57
|
+
InferSchemaInput<InsertSchema<GetTableFromSchema<TSchema, TTableName>>>
|
|
58
|
+
>;
|
|
59
|
+
|
|
60
|
+
export type DrizzleIndexedDBContextValue<
|
|
61
|
+
TSchema extends Record<string, unknown>,
|
|
62
|
+
> = {
|
|
63
|
+
indexedDB: IDBDatabaseLike | null;
|
|
64
|
+
getCollection: <TTableName extends keyof TSchema & string>(
|
|
65
|
+
tableName: TTableName,
|
|
66
|
+
) => IndexedDbCollection<TSchema, TTableName>;
|
|
67
|
+
incrementRefCount: (tableName: string) => void;
|
|
68
|
+
decrementRefCount: (tableName: string) => void;
|
|
69
|
+
/**
|
|
70
|
+
* Handle a sync message from a proxy server.
|
|
71
|
+
* Routes the message to the appropriate collection's external sync handler.
|
|
72
|
+
*/
|
|
73
|
+
handleProxySync: (message: IDBProxySyncMessage) => void;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const DrizzleIndexedDBContext =
|
|
77
|
+
// biome-ignore lint/suspicious/noExplicitAny: Context needs to accept any schema type
|
|
78
|
+
createContext<DrizzleIndexedDBContextValue<any> | null>(null);
|
|
79
|
+
|
|
80
|
+
type DrizzleIndexedDBProviderProps<TSchema extends Record<string, unknown>> =
|
|
81
|
+
PropsWithChildren<{
|
|
82
|
+
dbName: string;
|
|
83
|
+
schema: TSchema;
|
|
84
|
+
migrations?: Migration[];
|
|
85
|
+
migrateFunction?: (
|
|
86
|
+
dbName: string,
|
|
87
|
+
migrations: Migration[],
|
|
88
|
+
debug?: boolean,
|
|
89
|
+
dbCreator?: IDBCreator,
|
|
90
|
+
) => Promise<IDBDatabaseLike>;
|
|
91
|
+
debug?: boolean;
|
|
92
|
+
syncMode?: SyncMode;
|
|
93
|
+
/**
|
|
94
|
+
* Optional custom database creator for testing/mocking.
|
|
95
|
+
* Use createInstrumentedDbCreator() to track IndexedDB operations.
|
|
96
|
+
*/
|
|
97
|
+
dbCreator?: IDBCreator;
|
|
98
|
+
/**
|
|
99
|
+
* Called when the sync handler is ready.
|
|
100
|
+
* Use this to connect proxy sync messages to the provider.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* const proxyClient = ...;
|
|
104
|
+
* <DrizzleIndexedDBProvider
|
|
105
|
+
* onSyncReady={(handleSync) => proxyClient.onSync(handleSync)}
|
|
106
|
+
* ...
|
|
107
|
+
* />
|
|
108
|
+
*/
|
|
109
|
+
onSyncReady?: (handleSync: (message: IDBProxySyncMessage) => void) => void;
|
|
110
|
+
}>;
|
|
111
|
+
|
|
112
|
+
export function DrizzleIndexedDBProvider<
|
|
113
|
+
TSchema extends Record<string, unknown>,
|
|
114
|
+
>({
|
|
115
|
+
children,
|
|
116
|
+
dbName,
|
|
117
|
+
schema,
|
|
118
|
+
migrations = [],
|
|
119
|
+
migrateFunction = migrateIndexedDBWithFunctions,
|
|
120
|
+
debug = false,
|
|
121
|
+
syncMode = "eager",
|
|
122
|
+
dbCreator,
|
|
123
|
+
onSyncReady,
|
|
124
|
+
}: DrizzleIndexedDBProviderProps<TSchema>) {
|
|
125
|
+
const [indexedDB, setIndexedDB] = useState<IDBDatabaseLike | null>(null);
|
|
126
|
+
const indexedDBRef = useRef<IDBDatabaseLike | null>(null);
|
|
127
|
+
const [readyPromise] = useState(() => {
|
|
128
|
+
let resolveReady: () => void;
|
|
129
|
+
const promise = new Promise<void>((resolve) => {
|
|
130
|
+
resolveReady = resolve;
|
|
131
|
+
});
|
|
132
|
+
// biome-ignore lint/style/noNonNullAssertion: resolveReady is guaranteed to be set
|
|
133
|
+
return { promise, resolve: resolveReady! };
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
let db: IDBDatabaseLike | null = null;
|
|
138
|
+
const initDB = async () => {
|
|
139
|
+
try {
|
|
140
|
+
if (migrations.length === 0) {
|
|
141
|
+
// Open database directly without migration logic
|
|
142
|
+
db = await openIndexedDb(dbName, dbCreator);
|
|
143
|
+
} else {
|
|
144
|
+
db = await migrateFunction(dbName, migrations, debug, dbCreator);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
indexedDBRef.current = db;
|
|
148
|
+
setIndexedDB(db);
|
|
149
|
+
readyPromise.resolve();
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error(
|
|
152
|
+
`[DrizzleIndexedDBProvider] Failed to initialize database ${dbName}:`,
|
|
153
|
+
error,
|
|
154
|
+
);
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
initDB();
|
|
160
|
+
|
|
161
|
+
// Cleanup on unmount
|
|
162
|
+
return () => {
|
|
163
|
+
if (db) {
|
|
164
|
+
db.close();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}, [dbName, migrations, migrateFunction, debug, readyPromise, dbCreator]);
|
|
168
|
+
|
|
169
|
+
// Collection cache with ref counting
|
|
170
|
+
const collections = useMemo<Map<string, CollectionCacheEntry>>(
|
|
171
|
+
() => new Map(),
|
|
172
|
+
[],
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const getCollection = useCallback<
|
|
176
|
+
DrizzleIndexedDBContextValue<TSchema>["getCollection"]
|
|
177
|
+
>(
|
|
178
|
+
<TTableName extends keyof TSchema & string>(tableName: TTableName) => {
|
|
179
|
+
const cacheKey = tableName;
|
|
180
|
+
|
|
181
|
+
// Check if collection already exists in cache
|
|
182
|
+
if (!collections.has(cacheKey)) {
|
|
183
|
+
// Get the table definition from schema
|
|
184
|
+
const table = schema[tableName] as Table;
|
|
185
|
+
|
|
186
|
+
if (!table) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Table "${tableName}" not found in schema. Available tables: ${Object.keys(schema).join(", ")}`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Extract the actual store/table name from the table definition
|
|
193
|
+
const actualTableName = getTableName(table);
|
|
194
|
+
|
|
195
|
+
// Create collection options
|
|
196
|
+
const collectionConfig = indexedDBCollectionOptions({
|
|
197
|
+
indexedDBRef,
|
|
198
|
+
dbName,
|
|
199
|
+
table,
|
|
200
|
+
storeName: actualTableName,
|
|
201
|
+
readyPromise: readyPromise.promise,
|
|
202
|
+
debug,
|
|
203
|
+
syncMode,
|
|
204
|
+
} as IndexedDBCollectionConfig<Table>);
|
|
205
|
+
|
|
206
|
+
// Create new collection and cache it with ref count 0
|
|
207
|
+
// The collection will wait for readyPromise before accessing the database
|
|
208
|
+
const collection = createCollection(collectionConfig);
|
|
209
|
+
|
|
210
|
+
collections.set(cacheKey, {
|
|
211
|
+
collection,
|
|
212
|
+
refCount: 0,
|
|
213
|
+
pushExternalSync: collectionConfig.utils.pushExternalSync,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// biome-ignore lint/style/noNonNullAssertion: We just ensured the collection exists
|
|
218
|
+
return collections.get(cacheKey)!
|
|
219
|
+
.collection as unknown as IndexedDbCollection<TSchema, TTableName>;
|
|
220
|
+
},
|
|
221
|
+
[collections, schema, readyPromise.promise, debug, dbName, syncMode],
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const incrementRefCount: DrizzleIndexedDBContextValue<TSchema>["incrementRefCount"] =
|
|
225
|
+
useCallback(
|
|
226
|
+
(tableName: string) => {
|
|
227
|
+
const entry = collections.get(tableName);
|
|
228
|
+
if (entry) {
|
|
229
|
+
entry.refCount++;
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
[collections],
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const decrementRefCount: DrizzleIndexedDBContextValue<TSchema>["decrementRefCount"] =
|
|
236
|
+
useCallback(
|
|
237
|
+
(tableName: string) => {
|
|
238
|
+
const entry = collections.get(tableName);
|
|
239
|
+
if (entry) {
|
|
240
|
+
entry.refCount--;
|
|
241
|
+
|
|
242
|
+
// If ref count reaches 0, remove from cache
|
|
243
|
+
if (entry.refCount <= 0) {
|
|
244
|
+
collections.delete(tableName);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
[collections],
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Handle proxy sync messages by routing to the appropriate collection
|
|
252
|
+
const handleProxySync: DrizzleIndexedDBContextValue<TSchema>["handleProxySync"] =
|
|
253
|
+
useCallback(
|
|
254
|
+
(message: IDBProxySyncMessage) => {
|
|
255
|
+
// Find the collection for this store by checking the schema
|
|
256
|
+
for (const [tableName, table] of Object.entries(schema)) {
|
|
257
|
+
const actualStoreName = getTableName(table as Table);
|
|
258
|
+
if (actualStoreName === message.storeName) {
|
|
259
|
+
const entry = collections.get(tableName);
|
|
260
|
+
if (entry?.pushExternalSync) {
|
|
261
|
+
// Route sync message to collection
|
|
262
|
+
switch (message.type) {
|
|
263
|
+
case "sync:add":
|
|
264
|
+
entry.pushExternalSync({
|
|
265
|
+
type: "insert",
|
|
266
|
+
items: message.items,
|
|
267
|
+
});
|
|
268
|
+
break;
|
|
269
|
+
case "sync:put":
|
|
270
|
+
entry.pushExternalSync({
|
|
271
|
+
type: "update",
|
|
272
|
+
items: message.items,
|
|
273
|
+
});
|
|
274
|
+
break;
|
|
275
|
+
case "sync:delete":
|
|
276
|
+
// For delete, construct items with id
|
|
277
|
+
entry.pushExternalSync({
|
|
278
|
+
type: "delete",
|
|
279
|
+
items: message.keys.map((key) => ({ id: key })),
|
|
280
|
+
});
|
|
281
|
+
break;
|
|
282
|
+
case "sync:clear":
|
|
283
|
+
entry.pushExternalSync({
|
|
284
|
+
type: "truncate",
|
|
285
|
+
});
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (debug) {
|
|
294
|
+
console.warn(
|
|
295
|
+
`[DrizzleIndexedDBProvider] No collection found for store: ${message.storeName}`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
[collections, schema, debug],
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// Call onSyncReady when handleProxySync is available
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
if (onSyncReady) {
|
|
305
|
+
onSyncReady(handleProxySync);
|
|
306
|
+
}
|
|
307
|
+
}, [onSyncReady, handleProxySync]);
|
|
308
|
+
|
|
309
|
+
const contextValue: DrizzleIndexedDBContextValue<TSchema> = useMemo(
|
|
310
|
+
() => ({
|
|
311
|
+
indexedDB,
|
|
312
|
+
getCollection,
|
|
313
|
+
incrementRefCount,
|
|
314
|
+
decrementRefCount,
|
|
315
|
+
handleProxySync,
|
|
316
|
+
}),
|
|
317
|
+
[
|
|
318
|
+
indexedDB,
|
|
319
|
+
getCollection,
|
|
320
|
+
incrementRefCount,
|
|
321
|
+
decrementRefCount,
|
|
322
|
+
handleProxySync,
|
|
323
|
+
],
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<DrizzleIndexedDBContext.Provider value={contextValue}>
|
|
328
|
+
{children}
|
|
329
|
+
</DrizzleIndexedDBContext.Provider>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Hook that components use to get a collection with automatic ref counting
|
|
334
|
+
export function useIndexedDBCollection<
|
|
335
|
+
TSchema extends Record<string, unknown>,
|
|
336
|
+
TTableName extends keyof TSchema & string,
|
|
337
|
+
>(
|
|
338
|
+
context: DrizzleIndexedDBContextValue<TSchema>,
|
|
339
|
+
tableName: TTableName,
|
|
340
|
+
): InferCollectionFromTable<GetTableFromSchema<TSchema, TTableName>> {
|
|
341
|
+
const { collection, unsubscribe } = useMemo(() => {
|
|
342
|
+
// Get the collection and increment ref count
|
|
343
|
+
const col = context.getCollection(tableName);
|
|
344
|
+
context.incrementRefCount(tableName);
|
|
345
|
+
|
|
346
|
+
// Return collection and unsubscribe function
|
|
347
|
+
return {
|
|
348
|
+
collection: col,
|
|
349
|
+
unsubscribe: () => {
|
|
350
|
+
context.decrementRefCount(tableName);
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}, [context, tableName]);
|
|
354
|
+
|
|
355
|
+
// Cleanup on unmount
|
|
356
|
+
useEffect(() => {
|
|
357
|
+
return () => {
|
|
358
|
+
unsubscribe();
|
|
359
|
+
};
|
|
360
|
+
}, [unsubscribe]);
|
|
361
|
+
|
|
362
|
+
return collection as unknown as InferCollectionFromTable<
|
|
363
|
+
GetTableFromSchema<TSchema, TTableName>
|
|
364
|
+
>;
|
|
365
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useContext } from "react";
|
|
1
|
+
import { useCallback, useContext } from "react";
|
|
2
2
|
import {
|
|
3
3
|
DrizzleIndexedDBContext,
|
|
4
4
|
useIndexedDBCollection,
|
|
@@ -28,10 +28,15 @@ export function useDrizzleIndexedDB<
|
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const useCollection = useCallback(
|
|
32
|
+
<TTableName extends keyof TSchema & string>(tableName: TTableName) =>
|
|
33
|
+
// biome-ignore lint/correctness/useHookAtTopLevel: This is on purpose.
|
|
34
|
+
useIndexedDBCollection(context, tableName),
|
|
35
|
+
[context],
|
|
36
|
+
);
|
|
37
|
+
|
|
31
38
|
return {
|
|
32
|
-
useCollection
|
|
33
|
-
tableName: TTableName,
|
|
34
|
-
) => useIndexedDBCollection(context, tableName),
|
|
39
|
+
useCollection,
|
|
35
40
|
indexedDB: context.indexedDB,
|
|
36
41
|
};
|
|
37
42
|
}
|
package/src/idb-operations.ts
CHANGED
|
@@ -23,9 +23,14 @@ export async function openIndexedDb(
|
|
|
23
23
|
*/
|
|
24
24
|
const defaultIDBDeleter: IDBDeleter = (name: string): Promise<void> => {
|
|
25
25
|
return new Promise((resolve, reject) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
try {
|
|
27
|
+
const request = indexedDB.deleteDatabase(name);
|
|
28
|
+
request.onerror = () => reject(request.error);
|
|
29
|
+
request.onsuccess = () => resolve();
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error("Error deleting database", error);
|
|
32
|
+
reject(error);
|
|
33
|
+
}
|
|
29
34
|
});
|
|
30
35
|
};
|
|
31
36
|
|
|
@@ -104,12 +104,17 @@ class NativeIDBDatabase implements IDBDatabaseLike {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
return new Promise((resolve, reject) => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
try {
|
|
108
|
+
const transaction = this.db.transaction(storeName, "readonly");
|
|
109
|
+
const store = transaction.objectStore(storeName);
|
|
110
|
+
const request = store.getAll();
|
|
111
|
+
|
|
112
|
+
request.onsuccess = () => resolve(request.result as T[]);
|
|
113
|
+
request.onerror = () => reject(request.error);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error("Error getting all items", error);
|
|
116
|
+
reject(error);
|
|
117
|
+
}
|
|
113
118
|
});
|
|
114
119
|
}
|
|
115
120
|
|
|
@@ -119,14 +124,19 @@ class NativeIDBDatabase implements IDBDatabaseLike {
|
|
|
119
124
|
keyRange?: KeyRangeSpec,
|
|
120
125
|
): Promise<T[]> {
|
|
121
126
|
return new Promise((resolve, reject) => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
try {
|
|
128
|
+
const transaction = this.db.transaction(storeName, "readonly");
|
|
129
|
+
const store = transaction.objectStore(storeName);
|
|
130
|
+
const index = store.index(indexName);
|
|
131
|
+
const range = keyRange ? createKeyRange(keyRange) : undefined;
|
|
132
|
+
const request = index.getAll(range);
|
|
133
|
+
|
|
134
|
+
request.onsuccess = () => resolve(request.result as T[]);
|
|
135
|
+
request.onerror = () => reject(request.error);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error("Error getting all items by index", error);
|
|
138
|
+
reject(error);
|
|
139
|
+
}
|
|
130
140
|
});
|
|
131
141
|
}
|
|
132
142
|
|
|
@@ -135,72 +145,98 @@ class NativeIDBDatabase implements IDBDatabaseLike {
|
|
|
135
145
|
key: IDBValidKey,
|
|
136
146
|
): Promise<T | undefined> {
|
|
137
147
|
return new Promise((resolve, reject) => {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
148
|
+
try {
|
|
149
|
+
const transaction = this.db.transaction(storeName, "readonly");
|
|
150
|
+
const store = transaction.objectStore(storeName);
|
|
151
|
+
const request = store.get(key);
|
|
152
|
+
|
|
153
|
+
request.onsuccess = () => resolve(request.result as T | undefined);
|
|
154
|
+
request.onerror = () => reject(request.error);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error("Error getting item", error);
|
|
157
|
+
reject(error);
|
|
158
|
+
}
|
|
144
159
|
});
|
|
145
160
|
}
|
|
146
161
|
|
|
147
162
|
async add(storeName: string, items: unknown[]): Promise<void> {
|
|
148
163
|
return new Promise((resolve, reject) => {
|
|
149
|
-
|
|
150
|
-
|
|
164
|
+
try {
|
|
165
|
+
const transaction = this.db.transaction(storeName, "readwrite");
|
|
166
|
+
const store = transaction.objectStore(storeName);
|
|
151
167
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
168
|
+
for (const item of items) {
|
|
169
|
+
store.add(item);
|
|
170
|
+
}
|
|
155
171
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
172
|
+
transaction.oncomplete = () => resolve();
|
|
173
|
+
transaction.onerror = () => reject(transaction.error);
|
|
174
|
+
transaction.onabort = () => reject(new Error("Transaction aborted"));
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error("Error adding items", error);
|
|
177
|
+
reject(error);
|
|
178
|
+
}
|
|
159
179
|
});
|
|
160
180
|
}
|
|
161
181
|
|
|
162
182
|
async put(storeName: string, items: unknown[]): Promise<void> {
|
|
163
183
|
return new Promise((resolve, reject) => {
|
|
164
|
-
|
|
165
|
-
|
|
184
|
+
try {
|
|
185
|
+
const transaction = this.db.transaction(storeName, "readwrite");
|
|
186
|
+
const store = transaction.objectStore(storeName);
|
|
166
187
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
188
|
+
for (const item of items) {
|
|
189
|
+
store.put(item);
|
|
190
|
+
}
|
|
170
191
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
192
|
+
transaction.oncomplete = () => resolve();
|
|
193
|
+
transaction.onerror = () => reject(transaction.error);
|
|
194
|
+
transaction.onabort = () => reject(new Error("Transaction aborted"));
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error("Error putting items", error);
|
|
197
|
+
reject(error);
|
|
198
|
+
}
|
|
174
199
|
});
|
|
175
200
|
}
|
|
176
201
|
|
|
177
202
|
async delete(storeName: string, keys: IDBValidKey[]): Promise<void> {
|
|
178
203
|
return new Promise((resolve, reject) => {
|
|
179
|
-
|
|
180
|
-
|
|
204
|
+
try {
|
|
205
|
+
const transaction = this.db.transaction(storeName, "readwrite");
|
|
206
|
+
const store = transaction.objectStore(storeName);
|
|
181
207
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
208
|
+
for (const key of keys) {
|
|
209
|
+
store.delete(key);
|
|
210
|
+
}
|
|
185
211
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
212
|
+
transaction.oncomplete = () => resolve();
|
|
213
|
+
transaction.onerror = () => reject(transaction.error);
|
|
214
|
+
transaction.onabort = () => reject(new Error("Transaction aborted"));
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error("Error deleting items", error);
|
|
217
|
+
reject(error);
|
|
218
|
+
}
|
|
189
219
|
});
|
|
190
220
|
}
|
|
191
221
|
|
|
192
222
|
async clear(storeName: string): Promise<void> {
|
|
193
223
|
return new Promise((resolve, reject) => {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
224
|
+
try {
|
|
225
|
+
const transaction = this.db.transaction(storeName, "readwrite");
|
|
226
|
+
const store = transaction.objectStore(storeName);
|
|
227
|
+
const request = store.clear();
|
|
228
|
+
|
|
229
|
+
request.onsuccess = () => resolve();
|
|
230
|
+
request.onerror = () => reject(request.error);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
console.error("Error clearing store", error);
|
|
233
|
+
reject(error);
|
|
234
|
+
}
|
|
200
235
|
});
|
|
201
236
|
}
|
|
202
237
|
|
|
203
238
|
close(): void {
|
|
239
|
+
console.log("Closing database");
|
|
204
240
|
this.db.close();
|
|
205
241
|
}
|
|
206
242
|
}
|
|
@@ -319,37 +355,42 @@ export const defaultIDBCreator: IDBCreator = (
|
|
|
319
355
|
options?: IDBOpenOptions,
|
|
320
356
|
): Promise<IDBDatabaseLike> => {
|
|
321
357
|
return new Promise((resolve, reject) => {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
request.onerror = () => reject(request.error);
|
|
358
|
+
try {
|
|
359
|
+
const request = options?.version
|
|
360
|
+
? indexedDB.open(name, options.version)
|
|
361
|
+
: indexedDB.open(name);
|
|
327
362
|
|
|
328
|
-
|
|
329
|
-
setTimeout(() => {
|
|
330
|
-
reject(new Error("Database upgrade blocked - close other tabs"));
|
|
331
|
-
}, 3000);
|
|
332
|
-
};
|
|
363
|
+
request.onerror = () => reject(request.error);
|
|
333
364
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
365
|
+
request.onblocked = () => {
|
|
366
|
+
setTimeout(() => {
|
|
367
|
+
reject(new Error("Database upgrade blocked - close other tabs"));
|
|
368
|
+
}, 3000);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
request.onupgradeneeded = (event) => {
|
|
372
|
+
if (options?.onUpgrade) {
|
|
373
|
+
const db = request.result;
|
|
374
|
+
const transaction = (event.target as IDBOpenDBRequest).transaction;
|
|
375
|
+
if (!transaction) {
|
|
376
|
+
reject(new Error("No transaction during upgrade"));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
// Create an upgrade-mode database wrapper
|
|
380
|
+
const upgradeDb = new UpgradeModeDatabase(db, transaction);
|
|
381
|
+
try {
|
|
382
|
+
options.onUpgrade(upgradeDb);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
transaction.abort();
|
|
385
|
+
reject(error);
|
|
386
|
+
}
|
|
341
387
|
}
|
|
342
|
-
|
|
343
|
-
const upgradeDb = new UpgradeModeDatabase(db, transaction);
|
|
344
|
-
try {
|
|
345
|
-
options.onUpgrade(upgradeDb);
|
|
346
|
-
} catch (error) {
|
|
347
|
-
transaction.abort();
|
|
348
|
-
reject(error);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
};
|
|
388
|
+
};
|
|
352
389
|
|
|
353
|
-
|
|
390
|
+
request.onsuccess = () => resolve(new NativeIDBDatabase(request.result));
|
|
391
|
+
} catch (error) {
|
|
392
|
+
console.error("Error creating database", error);
|
|
393
|
+
reject(error);
|
|
394
|
+
}
|
|
354
395
|
});
|
|
355
396
|
};
|