@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
|
@@ -0,0 +1,374 @@
|
|
|
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
|
+
const initDB = async () => {
|
|
138
|
+
try {
|
|
139
|
+
let db: IDBDatabaseLike;
|
|
140
|
+
|
|
141
|
+
if (migrations.length === 0) {
|
|
142
|
+
// Open database directly without migration logic
|
|
143
|
+
db = await openIndexedDb(dbName, dbCreator);
|
|
144
|
+
} else {
|
|
145
|
+
db = await migrateFunction(dbName, migrations, debug, dbCreator);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
indexedDBRef.current = db;
|
|
149
|
+
setIndexedDB(db);
|
|
150
|
+
readyPromise.resolve();
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error(
|
|
153
|
+
`[DrizzleIndexedDBProvider] Failed to initialize database ${dbName}:`,
|
|
154
|
+
error,
|
|
155
|
+
);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
initDB();
|
|
161
|
+
|
|
162
|
+
// Cleanup on unmount
|
|
163
|
+
return () => {
|
|
164
|
+
if (indexedDB) {
|
|
165
|
+
indexedDB.close();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}, [dbName, migrations, migrateFunction, debug, readyPromise]);
|
|
169
|
+
|
|
170
|
+
// Collection cache with ref counting
|
|
171
|
+
const collections = useMemo<Map<string, CollectionCacheEntry>>(
|
|
172
|
+
() => new Map(),
|
|
173
|
+
[],
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const getCollection = useCallback<
|
|
177
|
+
DrizzleIndexedDBContextValue<TSchema>["getCollection"]
|
|
178
|
+
>(
|
|
179
|
+
<TTableName extends keyof TSchema & string>(tableName: TTableName) => {
|
|
180
|
+
const cacheKey = tableName;
|
|
181
|
+
|
|
182
|
+
// Check if collection already exists in cache
|
|
183
|
+
if (!collections.has(cacheKey)) {
|
|
184
|
+
// Get the table definition from schema
|
|
185
|
+
const table = schema[tableName] as Table;
|
|
186
|
+
|
|
187
|
+
if (!table) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Table "${tableName}" not found in schema. Available tables: ${Object.keys(schema).join(", ")}`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Extract the actual store/table name from the table definition
|
|
194
|
+
const actualTableName = getTableName(table);
|
|
195
|
+
|
|
196
|
+
// Create collection options
|
|
197
|
+
const collectionConfig = indexedDBCollectionOptions({
|
|
198
|
+
indexedDBRef,
|
|
199
|
+
dbName,
|
|
200
|
+
table,
|
|
201
|
+
storeName: actualTableName,
|
|
202
|
+
readyPromise: readyPromise.promise,
|
|
203
|
+
debug,
|
|
204
|
+
syncMode,
|
|
205
|
+
} as IndexedDBCollectionConfig<Table>);
|
|
206
|
+
|
|
207
|
+
// Create new collection and cache it with ref count 0
|
|
208
|
+
// The collection will wait for readyPromise before accessing the database
|
|
209
|
+
const collection = createCollection(collectionConfig);
|
|
210
|
+
|
|
211
|
+
collections.set(cacheKey, {
|
|
212
|
+
collection,
|
|
213
|
+
refCount: 0,
|
|
214
|
+
pushExternalSync: collectionConfig.utils.pushExternalSync,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// biome-ignore lint/style/noNonNullAssertion: We just ensured the collection exists
|
|
219
|
+
return collections.get(cacheKey)!
|
|
220
|
+
.collection as unknown as IndexedDbCollection<TSchema, TTableName>;
|
|
221
|
+
},
|
|
222
|
+
[
|
|
223
|
+
indexedDBRef,
|
|
224
|
+
collections,
|
|
225
|
+
schema,
|
|
226
|
+
readyPromise.promise,
|
|
227
|
+
debug,
|
|
228
|
+
dbName,
|
|
229
|
+
syncMode,
|
|
230
|
+
],
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const incrementRefCount: DrizzleIndexedDBContextValue<TSchema>["incrementRefCount"] =
|
|
234
|
+
useCallback(
|
|
235
|
+
(tableName: string) => {
|
|
236
|
+
const entry = collections.get(tableName);
|
|
237
|
+
if (entry) {
|
|
238
|
+
entry.refCount++;
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
[collections],
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const decrementRefCount: DrizzleIndexedDBContextValue<TSchema>["decrementRefCount"] =
|
|
245
|
+
useCallback(
|
|
246
|
+
(tableName: string) => {
|
|
247
|
+
const entry = collections.get(tableName);
|
|
248
|
+
if (entry) {
|
|
249
|
+
entry.refCount--;
|
|
250
|
+
|
|
251
|
+
// If ref count reaches 0, remove from cache
|
|
252
|
+
if (entry.refCount <= 0) {
|
|
253
|
+
collections.delete(tableName);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
[collections],
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Handle proxy sync messages by routing to the appropriate collection
|
|
261
|
+
const handleProxySync: DrizzleIndexedDBContextValue<TSchema>["handleProxySync"] =
|
|
262
|
+
useCallback(
|
|
263
|
+
(message: IDBProxySyncMessage) => {
|
|
264
|
+
// Find the collection for this store by checking the schema
|
|
265
|
+
for (const [tableName, table] of Object.entries(schema)) {
|
|
266
|
+
const actualStoreName = getTableName(table as Table);
|
|
267
|
+
if (actualStoreName === message.storeName) {
|
|
268
|
+
const entry = collections.get(tableName);
|
|
269
|
+
if (entry?.pushExternalSync) {
|
|
270
|
+
// Route sync message to collection
|
|
271
|
+
switch (message.type) {
|
|
272
|
+
case "sync:add":
|
|
273
|
+
entry.pushExternalSync({
|
|
274
|
+
type: "insert",
|
|
275
|
+
items: message.items,
|
|
276
|
+
});
|
|
277
|
+
break;
|
|
278
|
+
case "sync:put":
|
|
279
|
+
entry.pushExternalSync({
|
|
280
|
+
type: "update",
|
|
281
|
+
items: message.items,
|
|
282
|
+
});
|
|
283
|
+
break;
|
|
284
|
+
case "sync:delete":
|
|
285
|
+
// For delete, construct items with id
|
|
286
|
+
entry.pushExternalSync({
|
|
287
|
+
type: "delete",
|
|
288
|
+
items: message.keys.map((key) => ({ id: key })),
|
|
289
|
+
});
|
|
290
|
+
break;
|
|
291
|
+
case "sync:clear":
|
|
292
|
+
entry.pushExternalSync({
|
|
293
|
+
type: "truncate",
|
|
294
|
+
});
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (debug) {
|
|
303
|
+
console.warn(
|
|
304
|
+
`[DrizzleIndexedDBProvider] No collection found for store: ${message.storeName}`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
[collections, schema, debug],
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// Call onSyncReady when handleProxySync is available
|
|
312
|
+
useEffect(() => {
|
|
313
|
+
if (onSyncReady) {
|
|
314
|
+
onSyncReady(handleProxySync);
|
|
315
|
+
}
|
|
316
|
+
}, [onSyncReady, handleProxySync]);
|
|
317
|
+
|
|
318
|
+
const contextValue: DrizzleIndexedDBContextValue<TSchema> = useMemo(
|
|
319
|
+
() => ({
|
|
320
|
+
indexedDB,
|
|
321
|
+
getCollection,
|
|
322
|
+
incrementRefCount,
|
|
323
|
+
decrementRefCount,
|
|
324
|
+
handleProxySync,
|
|
325
|
+
}),
|
|
326
|
+
[
|
|
327
|
+
indexedDB,
|
|
328
|
+
getCollection,
|
|
329
|
+
incrementRefCount,
|
|
330
|
+
decrementRefCount,
|
|
331
|
+
handleProxySync,
|
|
332
|
+
],
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<DrizzleIndexedDBContext.Provider value={contextValue}>
|
|
337
|
+
{children}
|
|
338
|
+
</DrizzleIndexedDBContext.Provider>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Hook that components use to get a collection with automatic ref counting
|
|
343
|
+
export function useIndexedDBCollection<
|
|
344
|
+
TSchema extends Record<string, unknown>,
|
|
345
|
+
TTableName extends keyof TSchema & string,
|
|
346
|
+
>(
|
|
347
|
+
context: DrizzleIndexedDBContextValue<TSchema>,
|
|
348
|
+
tableName: TTableName,
|
|
349
|
+
): InferCollectionFromTable<GetTableFromSchema<TSchema, TTableName>> {
|
|
350
|
+
const { collection, unsubscribe } = useMemo(() => {
|
|
351
|
+
// Get the collection and increment ref count
|
|
352
|
+
const col = context.getCollection(tableName);
|
|
353
|
+
context.incrementRefCount(tableName);
|
|
354
|
+
|
|
355
|
+
// Return collection and unsubscribe function
|
|
356
|
+
return {
|
|
357
|
+
collection: col,
|
|
358
|
+
unsubscribe: () => {
|
|
359
|
+
context.decrementRefCount(tableName);
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
}, [context, tableName]);
|
|
363
|
+
|
|
364
|
+
// Cleanup on unmount
|
|
365
|
+
useEffect(() => {
|
|
366
|
+
return () => {
|
|
367
|
+
unsubscribe();
|
|
368
|
+
};
|
|
369
|
+
}, [unsubscribe]);
|
|
370
|
+
|
|
371
|
+
return collection as unknown as InferCollectionFromTable<
|
|
372
|
+
GetTableFromSchema<TSchema, TTableName>
|
|
373
|
+
>;
|
|
374
|
+
}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
useIndexedDBCollection,
|
|
5
5
|
type DrizzleIndexedDBContextValue,
|
|
6
6
|
} from "./DrizzleIndexedDBProvider";
|
|
7
|
-
import type { IDBDatabaseLike } from "../
|
|
7
|
+
import type { IDBDatabaseLike } from "../idb-types";
|
|
8
8
|
|
|
9
9
|
export type UseDrizzleIndexedDBContextReturn<
|
|
10
10
|
TSchema extends Record<string, unknown>,
|
package/src/function-migrator.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// IndexedDB migrator with declarative migration format
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { IDBCreator, IDBDatabaseLike } from "./idb-types";
|
|
4
|
+
import { openIndexedDb } from "./idb-operations";
|
|
4
5
|
|
|
5
6
|
// ============================================================================
|
|
6
7
|
// Declarative Migration Types
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operation tracking for IndexedDB queries
|
|
3
|
+
* Useful for testing and debugging to verify what operations are actually performed
|
|
4
|
+
*
|
|
5
|
+
* Uses discriminated unions for type safety - TypeScript can narrow the type based on the 'type' field
|
|
6
|
+
*/
|
|
7
|
+
export type IDBOperation =
|
|
8
|
+
| {
|
|
9
|
+
type: "getAll";
|
|
10
|
+
storeName: string;
|
|
11
|
+
itemsReturned: unknown[];
|
|
12
|
+
itemCount: number;
|
|
13
|
+
context?: string;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
type: "index-getAll";
|
|
18
|
+
storeName: string;
|
|
19
|
+
indexName: string;
|
|
20
|
+
keyRange?: IDBKeyRange;
|
|
21
|
+
itemsReturned: unknown[];
|
|
22
|
+
itemCount: number;
|
|
23
|
+
context?: string;
|
|
24
|
+
timestamp: number;
|
|
25
|
+
}
|
|
26
|
+
| {
|
|
27
|
+
type: "write";
|
|
28
|
+
storeName: string;
|
|
29
|
+
itemsWritten: unknown[];
|
|
30
|
+
writeCount: number;
|
|
31
|
+
context?: string;
|
|
32
|
+
timestamp: number;
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
type: "get";
|
|
36
|
+
storeName: string;
|
|
37
|
+
key: IDBValidKey;
|
|
38
|
+
itemReturned?: unknown;
|
|
39
|
+
timestamp: number;
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
type: "put";
|
|
43
|
+
storeName: string;
|
|
44
|
+
items: unknown[];
|
|
45
|
+
itemCount: number;
|
|
46
|
+
timestamp: number;
|
|
47
|
+
}
|
|
48
|
+
| {
|
|
49
|
+
type: "add";
|
|
50
|
+
storeName: string;
|
|
51
|
+
items: unknown[];
|
|
52
|
+
itemCount: number;
|
|
53
|
+
timestamp: number;
|
|
54
|
+
}
|
|
55
|
+
| {
|
|
56
|
+
type: "delete";
|
|
57
|
+
storeName: string;
|
|
58
|
+
keys: IDBValidKey[];
|
|
59
|
+
keyCount: number;
|
|
60
|
+
timestamp: number;
|
|
61
|
+
}
|
|
62
|
+
| {
|
|
63
|
+
type: "clear";
|
|
64
|
+
storeName: string;
|
|
65
|
+
timestamp: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Interceptor interface for tracking IndexedDB operations
|
|
70
|
+
* Allows tests and debugging tools to observe what operations are performed
|
|
71
|
+
*/
|
|
72
|
+
export interface IDBInterceptor {
|
|
73
|
+
/** Called when any IndexedDB operation is performed */
|
|
74
|
+
onOperation?: (operation: IDBOperation) => void;
|
|
75
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IDBDatabaseLike,
|
|
3
|
+
IDBCreator,
|
|
4
|
+
IDBOpenOptions,
|
|
5
|
+
IDBDeleter,
|
|
6
|
+
} from "./idb-types";
|
|
7
|
+
import { defaultIDBCreator } from "./native-idb-database";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Opens an IndexedDB database using the provided creator or the default native implementation.
|
|
11
|
+
*/
|
|
12
|
+
export async function openIndexedDb(
|
|
13
|
+
name: string,
|
|
14
|
+
dbCreator?: IDBCreator,
|
|
15
|
+
options?: IDBOpenOptions,
|
|
16
|
+
): Promise<IDBDatabaseLike> {
|
|
17
|
+
const dbCreatorToUse = dbCreator ?? defaultIDBCreator;
|
|
18
|
+
return dbCreatorToUse(name, options);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Default IDB deleter that uses the native IndexedDB API.
|
|
23
|
+
*/
|
|
24
|
+
const defaultIDBDeleter: IDBDeleter = (name: string): Promise<void> => {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const request = indexedDB.deleteDatabase(name);
|
|
27
|
+
request.onerror = () => reject(request.error);
|
|
28
|
+
request.onsuccess = () => resolve();
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Deletes an IndexedDB database (useful for testing)
|
|
34
|
+
*/
|
|
35
|
+
export async function deleteIndexedDB(
|
|
36
|
+
dbName: string,
|
|
37
|
+
dbDeleter?: IDBDeleter,
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
const dbDeleterToUse = dbDeleter ?? defaultIDBDeleter;
|
|
40
|
+
return dbDeleterToUse(dbName);
|
|
41
|
+
}
|
package/src/idb-types.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index information returned by getStoreIndexes
|
|
3
|
+
*/
|
|
4
|
+
export interface IndexInfo {
|
|
5
|
+
name: string;
|
|
6
|
+
keyPath: string | string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options for creating an object store
|
|
11
|
+
*/
|
|
12
|
+
export interface CreateStoreOptions {
|
|
13
|
+
keyPath?: string;
|
|
14
|
+
autoIncrement?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Options for creating an index
|
|
19
|
+
*/
|
|
20
|
+
export interface CreateIndexOptions {
|
|
21
|
+
unique?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Key range specification for index queries
|
|
26
|
+
*/
|
|
27
|
+
export interface KeyRangeSpec {
|
|
28
|
+
type: "only" | "lowerBound" | "upperBound" | "bound";
|
|
29
|
+
value?: unknown;
|
|
30
|
+
lower?: unknown;
|
|
31
|
+
upper?: unknown;
|
|
32
|
+
lowerOpen?: boolean;
|
|
33
|
+
upperOpen?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Minimal database interface with high-level async operations.
|
|
38
|
+
* This is the interface that custom implementations (mocks, Chrome extension proxies, etc.) need to implement.
|
|
39
|
+
*
|
|
40
|
+
* All operations are simple async functions - no transactions, requests, or callbacks to deal with.
|
|
41
|
+
*/
|
|
42
|
+
export interface IDBDatabaseLike {
|
|
43
|
+
/** Database version number */
|
|
44
|
+
readonly version: number;
|
|
45
|
+
|
|
46
|
+
// =========================================================================
|
|
47
|
+
// Schema Operations (for migrations)
|
|
48
|
+
// =========================================================================
|
|
49
|
+
|
|
50
|
+
/** Check if a store exists */
|
|
51
|
+
hasStore(storeName: string): boolean;
|
|
52
|
+
|
|
53
|
+
/** Get list of all store names */
|
|
54
|
+
getStoreNames(): string[];
|
|
55
|
+
|
|
56
|
+
/** Create an object store (only valid during migrations) */
|
|
57
|
+
createStore(storeName: string, options?: CreateStoreOptions): void;
|
|
58
|
+
|
|
59
|
+
/** Delete an object store (only valid during migrations) */
|
|
60
|
+
deleteStore(storeName: string): void;
|
|
61
|
+
|
|
62
|
+
/** Create an index on a store (only valid during migrations) */
|
|
63
|
+
createIndex(
|
|
64
|
+
storeName: string,
|
|
65
|
+
indexName: string,
|
|
66
|
+
keyPath: string | string[],
|
|
67
|
+
options?: CreateIndexOptions,
|
|
68
|
+
): void;
|
|
69
|
+
|
|
70
|
+
/** Delete an index from a store (only valid during migrations) */
|
|
71
|
+
deleteIndex(storeName: string, indexName: string): void;
|
|
72
|
+
|
|
73
|
+
/** Get all indexes for a store (for index discovery) */
|
|
74
|
+
getStoreIndexes(storeName: string): IndexInfo[];
|
|
75
|
+
|
|
76
|
+
// =========================================================================
|
|
77
|
+
// Data Operations (all async, handle transactions internally)
|
|
78
|
+
// =========================================================================
|
|
79
|
+
|
|
80
|
+
/** Get all items from a store */
|
|
81
|
+
getAll<T = unknown>(storeName: string): Promise<T[]>;
|
|
82
|
+
|
|
83
|
+
/** Get items from a store using an index with optional key range */
|
|
84
|
+
getAllByIndex<T = unknown>(
|
|
85
|
+
storeName: string,
|
|
86
|
+
indexName: string,
|
|
87
|
+
keyRange?: KeyRangeSpec,
|
|
88
|
+
): Promise<T[]>;
|
|
89
|
+
|
|
90
|
+
/** Get a single item by key */
|
|
91
|
+
get<T = unknown>(storeName: string, key: IDBValidKey): Promise<T | undefined>;
|
|
92
|
+
|
|
93
|
+
/** Add items to a store (batch operation) */
|
|
94
|
+
add(storeName: string, items: unknown[]): Promise<void>;
|
|
95
|
+
|
|
96
|
+
/** Update items in a store (batch operation, uses put) */
|
|
97
|
+
put(storeName: string, items: unknown[]): Promise<void>;
|
|
98
|
+
|
|
99
|
+
/** Delete items from a store by keys (batch operation) */
|
|
100
|
+
delete(storeName: string, keys: IDBValidKey[]): Promise<void>;
|
|
101
|
+
|
|
102
|
+
/** Clear all items from a store */
|
|
103
|
+
clear(storeName: string): Promise<void>;
|
|
104
|
+
|
|
105
|
+
// =========================================================================
|
|
106
|
+
// Lifecycle
|
|
107
|
+
// =========================================================================
|
|
108
|
+
|
|
109
|
+
/** Close the database connection */
|
|
110
|
+
close(): void;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Options for opening a database with version upgrade support.
|
|
115
|
+
*/
|
|
116
|
+
export interface IDBOpenOptions {
|
|
117
|
+
/** Target version for the database. If higher than current, triggers upgrade. */
|
|
118
|
+
version?: number;
|
|
119
|
+
/** Called during version upgrade - this is where schema changes (createStore, createIndex) are allowed. */
|
|
120
|
+
onUpgrade?: (db: IDBDatabaseLike) => void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Function type for creating/opening an IndexedDB-like database.
|
|
125
|
+
* Custom implementations can use this to provide proxy/mock/alternative backends.
|
|
126
|
+
*/
|
|
127
|
+
export type IDBCreator = (
|
|
128
|
+
name: string,
|
|
129
|
+
options?: IDBOpenOptions,
|
|
130
|
+
) => Promise<IDBDatabaseLike>;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Function type for deleting an IndexedDB database.
|
|
134
|
+
*/
|
|
135
|
+
export type IDBDeleter = (name: string) => Promise<void>;
|