@firtoz/drizzle-indexeddb 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +162 -0
- package/package.json +2 -2
- package/src/collections/indexeddb-collection.ts +52 -204
- 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
|
@@ -1,128 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Index information returned by getStoreIndexes
|
|
13
|
-
*/
|
|
14
|
-
export interface IndexInfo {
|
|
15
|
-
name: string;
|
|
16
|
-
keyPath: string | string[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Options for creating an object store
|
|
21
|
-
*/
|
|
22
|
-
export interface CreateStoreOptions {
|
|
23
|
-
keyPath?: string;
|
|
24
|
-
autoIncrement?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Options for creating an index
|
|
29
|
-
*/
|
|
30
|
-
export interface CreateIndexOptions {
|
|
31
|
-
unique?: boolean;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Key range specification for index queries
|
|
36
|
-
*/
|
|
37
|
-
export interface KeyRangeSpec {
|
|
38
|
-
type: "only" | "lowerBound" | "upperBound" | "bound";
|
|
39
|
-
value?: unknown;
|
|
40
|
-
lower?: unknown;
|
|
41
|
-
upper?: unknown;
|
|
42
|
-
lowerOpen?: boolean;
|
|
43
|
-
upperOpen?: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Minimal database interface with high-level async operations.
|
|
48
|
-
* This is the interface that custom implementations (mocks, Chrome extension proxies, etc.) need to implement.
|
|
49
|
-
*
|
|
50
|
-
* All operations are simple async functions - no transactions, requests, or callbacks to deal with.
|
|
51
|
-
*/
|
|
52
|
-
export interface IDBDatabaseLike {
|
|
53
|
-
/** Database version number */
|
|
54
|
-
readonly version: number;
|
|
55
|
-
|
|
56
|
-
// =========================================================================
|
|
57
|
-
// Schema Operations (for migrations)
|
|
58
|
-
// =========================================================================
|
|
59
|
-
|
|
60
|
-
/** Check if a store exists */
|
|
61
|
-
hasStore(storeName: string): boolean;
|
|
62
|
-
|
|
63
|
-
/** Get list of all store names */
|
|
64
|
-
getStoreNames(): string[];
|
|
65
|
-
|
|
66
|
-
/** Create an object store (only valid during migrations) */
|
|
67
|
-
createStore(storeName: string, options?: CreateStoreOptions): void;
|
|
68
|
-
|
|
69
|
-
/** Delete an object store (only valid during migrations) */
|
|
70
|
-
deleteStore(storeName: string): void;
|
|
71
|
-
|
|
72
|
-
/** Create an index on a store (only valid during migrations) */
|
|
73
|
-
createIndex(
|
|
74
|
-
storeName: string,
|
|
75
|
-
indexName: string,
|
|
76
|
-
keyPath: string | string[],
|
|
77
|
-
options?: CreateIndexOptions,
|
|
78
|
-
): void;
|
|
79
|
-
|
|
80
|
-
/** Delete an index from a store (only valid during migrations) */
|
|
81
|
-
deleteIndex(storeName: string, indexName: string): void;
|
|
82
|
-
|
|
83
|
-
/** Get all indexes for a store (for index discovery) */
|
|
84
|
-
getStoreIndexes(storeName: string): IndexInfo[];
|
|
85
|
-
|
|
86
|
-
// =========================================================================
|
|
87
|
-
// Data Operations (all async, handle transactions internally)
|
|
88
|
-
// =========================================================================
|
|
89
|
-
|
|
90
|
-
/** Get all items from a store */
|
|
91
|
-
getAll<T = unknown>(storeName: string): Promise<T[]>;
|
|
92
|
-
|
|
93
|
-
/** Get items from a store using an index with optional key range */
|
|
94
|
-
getAllByIndex<T = unknown>(
|
|
95
|
-
storeName: string,
|
|
96
|
-
indexName: string,
|
|
97
|
-
keyRange?: KeyRangeSpec,
|
|
98
|
-
): Promise<T[]>;
|
|
99
|
-
|
|
100
|
-
/** Get a single item by key */
|
|
101
|
-
get<T = unknown>(storeName: string, key: IDBValidKey): Promise<T | undefined>;
|
|
102
|
-
|
|
103
|
-
/** Add items to a store (batch operation) */
|
|
104
|
-
add(storeName: string, items: unknown[]): Promise<void>;
|
|
105
|
-
|
|
106
|
-
/** Update items in a store (batch operation, uses put) */
|
|
107
|
-
put(storeName: string, items: unknown[]): Promise<void>;
|
|
108
|
-
|
|
109
|
-
/** Delete items from a store by keys (batch operation) */
|
|
110
|
-
delete(storeName: string, keys: IDBValidKey[]): Promise<void>;
|
|
111
|
-
|
|
112
|
-
/** Clear all items from a store */
|
|
113
|
-
clear(storeName: string): Promise<void>;
|
|
114
|
-
|
|
115
|
-
// =========================================================================
|
|
116
|
-
// Lifecycle
|
|
117
|
-
// =========================================================================
|
|
118
|
-
|
|
119
|
-
/** Close the database connection */
|
|
120
|
-
close(): void;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ============================================================================
|
|
124
|
-
// Default Implementation (wraps native IndexedDB)
|
|
125
|
-
// ============================================================================
|
|
1
|
+
import type {
|
|
2
|
+
IDBDatabaseLike,
|
|
3
|
+
IDBCreator,
|
|
4
|
+
IDBOpenOptions,
|
|
5
|
+
IndexInfo,
|
|
6
|
+
CreateStoreOptions,
|
|
7
|
+
CreateIndexOptions,
|
|
8
|
+
KeyRangeSpec,
|
|
9
|
+
} from "./idb-types";
|
|
126
10
|
|
|
127
11
|
/**
|
|
128
12
|
* Creates a KeyRange from a KeyRangeSpec
|
|
@@ -321,69 +205,6 @@ class NativeIDBDatabase implements IDBDatabaseLike {
|
|
|
321
205
|
}
|
|
322
206
|
}
|
|
323
207
|
|
|
324
|
-
// ============================================================================
|
|
325
|
-
// IDB Creator / Opener
|
|
326
|
-
// ============================================================================
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Options for opening a database with version upgrade support.
|
|
330
|
-
*/
|
|
331
|
-
export interface IDBOpenOptions {
|
|
332
|
-
/** Target version for the database. If higher than current, triggers upgrade. */
|
|
333
|
-
version?: number;
|
|
334
|
-
/** Called during version upgrade - this is where schema changes (createStore, createIndex) are allowed. */
|
|
335
|
-
onUpgrade?: (db: IDBDatabaseLike) => void;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Function type for creating/opening an IndexedDB-like database.
|
|
340
|
-
* Custom implementations can use this to provide proxy/mock/alternative backends.
|
|
341
|
-
*/
|
|
342
|
-
export type IDBCreator = (
|
|
343
|
-
name: string,
|
|
344
|
-
options?: IDBOpenOptions,
|
|
345
|
-
) => Promise<IDBDatabaseLike>;
|
|
346
|
-
|
|
347
|
-
const defaultIDBCreator: IDBCreator = (
|
|
348
|
-
name: string,
|
|
349
|
-
options?: IDBOpenOptions,
|
|
350
|
-
): Promise<IDBDatabaseLike> => {
|
|
351
|
-
return new Promise((resolve, reject) => {
|
|
352
|
-
const request = options?.version
|
|
353
|
-
? indexedDB.open(name, options.version)
|
|
354
|
-
: indexedDB.open(name);
|
|
355
|
-
|
|
356
|
-
request.onerror = () => reject(request.error);
|
|
357
|
-
|
|
358
|
-
request.onblocked = () => {
|
|
359
|
-
setTimeout(() => {
|
|
360
|
-
reject(new Error("Database upgrade blocked - close other tabs"));
|
|
361
|
-
}, 3000);
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
request.onupgradeneeded = (event) => {
|
|
365
|
-
if (options?.onUpgrade) {
|
|
366
|
-
const db = request.result;
|
|
367
|
-
const transaction = (event.target as IDBOpenDBRequest).transaction;
|
|
368
|
-
if (!transaction) {
|
|
369
|
-
reject(new Error("No transaction during upgrade"));
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
// Create an upgrade-mode database wrapper
|
|
373
|
-
const upgradeDb = new UpgradeModeDatabase(db, transaction);
|
|
374
|
-
try {
|
|
375
|
-
options.onUpgrade(upgradeDb);
|
|
376
|
-
} catch (error) {
|
|
377
|
-
transaction.abort();
|
|
378
|
-
reject(error);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
request.onsuccess = () => resolve(new NativeIDBDatabase(request.result));
|
|
384
|
-
});
|
|
385
|
-
};
|
|
386
|
-
|
|
387
208
|
/**
|
|
388
209
|
* Upgrade-mode database wrapper used during version changes.
|
|
389
210
|
* Provides IDBDatabaseLike interface with schema modification capabilities.
|
|
@@ -490,36 +311,45 @@ class UpgradeModeDatabase implements IDBDatabaseLike {
|
|
|
490
311
|
}
|
|
491
312
|
}
|
|
492
313
|
|
|
493
|
-
|
|
314
|
+
/**
|
|
315
|
+
* Default IDB creator that uses the native IndexedDB API.
|
|
316
|
+
*/
|
|
317
|
+
export const defaultIDBCreator: IDBCreator = (
|
|
494
318
|
name: string,
|
|
495
|
-
dbCreator?: IDBCreator,
|
|
496
319
|
options?: IDBOpenOptions,
|
|
497
|
-
): Promise<IDBDatabaseLike> {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
320
|
+
): Promise<IDBDatabaseLike> => {
|
|
321
|
+
return new Promise((resolve, reject) => {
|
|
322
|
+
const request = options?.version
|
|
323
|
+
? indexedDB.open(name, options.version)
|
|
324
|
+
: indexedDB.open(name);
|
|
501
325
|
|
|
502
|
-
|
|
503
|
-
// IDB Deleter
|
|
504
|
-
// ============================================================================
|
|
326
|
+
request.onerror = () => reject(request.error);
|
|
505
327
|
|
|
506
|
-
|
|
328
|
+
request.onblocked = () => {
|
|
329
|
+
setTimeout(() => {
|
|
330
|
+
reject(new Error("Database upgrade blocked - close other tabs"));
|
|
331
|
+
}, 3000);
|
|
332
|
+
};
|
|
507
333
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
334
|
+
request.onupgradeneeded = (event) => {
|
|
335
|
+
if (options?.onUpgrade) {
|
|
336
|
+
const db = request.result;
|
|
337
|
+
const transaction = (event.target as IDBOpenDBRequest).transaction;
|
|
338
|
+
if (!transaction) {
|
|
339
|
+
reject(new Error("No transaction during upgrade"));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// Create an upgrade-mode database wrapper
|
|
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
|
+
};
|
|
352
|
+
|
|
353
|
+
request.onsuccess = () => resolve(new NativeIDBDatabase(request.result));
|
|
513
354
|
});
|
|
514
355
|
};
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Deletes the database (useful for testing)
|
|
518
|
-
*/
|
|
519
|
-
export async function deleteIndexedDB(
|
|
520
|
-
dbName: string,
|
|
521
|
-
dbDeleter?: IDBDeleter,
|
|
522
|
-
): Promise<void> {
|
|
523
|
-
const dbDeleterToUse = dbDeleter ?? defaultIDBDeleter;
|
|
524
|
-
return dbDeleterToUse(dbName);
|
|
525
|
-
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IDBDatabaseLike,
|
|
3
|
+
IDBCreator,
|
|
4
|
+
IDBOpenOptions,
|
|
5
|
+
IndexInfo,
|
|
6
|
+
CreateStoreOptions,
|
|
7
|
+
CreateIndexOptions,
|
|
8
|
+
KeyRangeSpec,
|
|
9
|
+
} from "../idb-types";
|
|
10
|
+
import type { IDBProxyClientTransport } from "./idb-proxy-transport";
|
|
11
|
+
import type {
|
|
12
|
+
IDBProxyRequest,
|
|
13
|
+
IDBProxyRequestBody,
|
|
14
|
+
IDBProxyResponse,
|
|
15
|
+
IDBProxySyncMessage,
|
|
16
|
+
} from "./idb-proxy-types";
|
|
17
|
+
import { generateRequestId, generateClientId } from "./idb-proxy-types";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Handler for sync messages from the server
|
|
21
|
+
*/
|
|
22
|
+
export type SyncHandler = (message: IDBProxySyncMessage) => void;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A proxy implementation of IDBDatabaseLike that sends all operations
|
|
26
|
+
* to a remote server via a transport layer.
|
|
27
|
+
*
|
|
28
|
+
* This is used by content scripts or other clients that don't have
|
|
29
|
+
* direct access to IndexedDB (e.g., in a Chrome extension context).
|
|
30
|
+
*
|
|
31
|
+
* The client manages a "connection" (session) to the server.
|
|
32
|
+
* The server manages the actual database lifecycle.
|
|
33
|
+
*
|
|
34
|
+
* When other clients modify data, the server broadcasts sync messages
|
|
35
|
+
* which this client receives and can handle via onSync().
|
|
36
|
+
*/
|
|
37
|
+
export class IDBProxyClient implements IDBDatabaseLike {
|
|
38
|
+
private _version: number = 0;
|
|
39
|
+
private _storeNames: string[] = [];
|
|
40
|
+
private _storeIndexes: Map<string, IndexInfo[]> = new Map();
|
|
41
|
+
private _connected: boolean = false;
|
|
42
|
+
private _clientId: string;
|
|
43
|
+
private _syncHandlers: Set<SyncHandler> = new Set();
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
private dbName: string,
|
|
47
|
+
private transport: IDBProxyClientTransport,
|
|
48
|
+
clientId?: string,
|
|
49
|
+
) {
|
|
50
|
+
this._clientId = clientId ?? generateClientId();
|
|
51
|
+
|
|
52
|
+
// Listen for sync messages from server
|
|
53
|
+
this.transport.onSync((message) => {
|
|
54
|
+
// Only handle messages for this database
|
|
55
|
+
if (message.dbName === this.dbName) {
|
|
56
|
+
for (const handler of this._syncHandlers) {
|
|
57
|
+
handler(message);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get the unique client ID
|
|
65
|
+
*/
|
|
66
|
+
get clientId(): string {
|
|
67
|
+
return this._clientId;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Register a handler for sync messages from other clients.
|
|
72
|
+
* Returns an unsubscribe function.
|
|
73
|
+
*/
|
|
74
|
+
onSync(handler: SyncHandler): () => void {
|
|
75
|
+
this._syncHandlers.add(handler);
|
|
76
|
+
return () => {
|
|
77
|
+
this._syncHandlers.delete(handler);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Connect to the server and fetch database metadata.
|
|
83
|
+
* The server will ensure the database is open and migrated.
|
|
84
|
+
*/
|
|
85
|
+
async connect(): Promise<void> {
|
|
86
|
+
if (this._connected) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Tell the server we want to connect to this database
|
|
91
|
+
const connectResponse = await this.sendRequest({ type: "connect" });
|
|
92
|
+
|
|
93
|
+
if (connectResponse.type === "error") {
|
|
94
|
+
throw new Error(connectResponse.error || "Failed to connect to database");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Fetch metadata
|
|
98
|
+
const versionResponse = await this.sendRequest({ type: "getVersion" });
|
|
99
|
+
if (versionResponse.type === "success") {
|
|
100
|
+
this._version = versionResponse.data as number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const storeNamesResponse = await this.sendRequest({
|
|
104
|
+
type: "getStoreNames",
|
|
105
|
+
});
|
|
106
|
+
if (storeNamesResponse.type === "success") {
|
|
107
|
+
this._storeNames = storeNamesResponse.data as string[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this._connected = true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Disconnect from the server.
|
|
115
|
+
* This is a no-op since clients are cached and reused.
|
|
116
|
+
* The connection stays open for future use.
|
|
117
|
+
*/
|
|
118
|
+
disconnect(): void {
|
|
119
|
+
// Intentionally a no-op - clients are cached and reused
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private async sendRequest(
|
|
123
|
+
request: IDBProxyRequestBody,
|
|
124
|
+
): Promise<IDBProxyResponse> {
|
|
125
|
+
const fullRequest: IDBProxyRequest = {
|
|
126
|
+
...request,
|
|
127
|
+
id: generateRequestId(),
|
|
128
|
+
clientId: this._clientId,
|
|
129
|
+
dbName: this.dbName,
|
|
130
|
+
};
|
|
131
|
+
return this.transport.sendRequest(fullRequest);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private handleResponse<T>(response: IDBProxyResponse): T {
|
|
135
|
+
if (response.type === "error") {
|
|
136
|
+
throw new Error(response.error || "Unknown server error");
|
|
137
|
+
}
|
|
138
|
+
return response.data as T;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get version(): number {
|
|
142
|
+
return this._version;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// =========================================================================
|
|
146
|
+
// Schema Operations - Cached locally, read-only for clients
|
|
147
|
+
// =========================================================================
|
|
148
|
+
|
|
149
|
+
hasStore(storeName: string): boolean {
|
|
150
|
+
return this._storeNames.includes(storeName);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
getStoreNames(): string[] {
|
|
154
|
+
return [...this._storeNames];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
createStore(_storeName: string, _options?: CreateStoreOptions): void {
|
|
158
|
+
throw new Error(
|
|
159
|
+
"Schema modifications not supported on proxy client. Use server-side migrations.",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
deleteStore(_storeName: string): void {
|
|
164
|
+
throw new Error(
|
|
165
|
+
"Schema modifications not supported on proxy client. Use server-side migrations.",
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
createIndex(
|
|
170
|
+
_storeName: string,
|
|
171
|
+
_indexName: string,
|
|
172
|
+
_keyPath: string | string[],
|
|
173
|
+
_options?: CreateIndexOptions,
|
|
174
|
+
): void {
|
|
175
|
+
throw new Error(
|
|
176
|
+
"Schema modifications not supported on proxy client. Use server-side migrations.",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
deleteIndex(_storeName: string, _indexName: string): void {
|
|
181
|
+
throw new Error(
|
|
182
|
+
"Schema modifications not supported on proxy client. Use server-side migrations.",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
getStoreIndexes(storeName: string): IndexInfo[] {
|
|
187
|
+
const cached = this._storeIndexes.get(storeName);
|
|
188
|
+
if (cached) {
|
|
189
|
+
return cached;
|
|
190
|
+
}
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Async version to fetch indexes from server
|
|
196
|
+
*/
|
|
197
|
+
async fetchStoreIndexes(storeName: string): Promise<IndexInfo[]> {
|
|
198
|
+
const response = await this.sendRequest({
|
|
199
|
+
type: "getStoreIndexes",
|
|
200
|
+
storeName,
|
|
201
|
+
});
|
|
202
|
+
const indexes = this.handleResponse<IndexInfo[]>(response);
|
|
203
|
+
this._storeIndexes.set(storeName, indexes);
|
|
204
|
+
return indexes;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// =========================================================================
|
|
208
|
+
// Data Operations - All proxied to server
|
|
209
|
+
// =========================================================================
|
|
210
|
+
|
|
211
|
+
async getAll<T = unknown>(storeName: string): Promise<T[]> {
|
|
212
|
+
const response = await this.sendRequest({
|
|
213
|
+
type: "getAll",
|
|
214
|
+
storeName,
|
|
215
|
+
});
|
|
216
|
+
return this.handleResponse<T[]>(response);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async getAllByIndex<T = unknown>(
|
|
220
|
+
storeName: string,
|
|
221
|
+
indexName: string,
|
|
222
|
+
keyRange?: KeyRangeSpec,
|
|
223
|
+
): Promise<T[]> {
|
|
224
|
+
const response = await this.sendRequest({
|
|
225
|
+
type: "getAllByIndex",
|
|
226
|
+
storeName,
|
|
227
|
+
indexName,
|
|
228
|
+
keyRange,
|
|
229
|
+
});
|
|
230
|
+
return this.handleResponse<T[]>(response);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async get<T = unknown>(
|
|
234
|
+
storeName: string,
|
|
235
|
+
key: IDBValidKey,
|
|
236
|
+
): Promise<T | undefined> {
|
|
237
|
+
const response = await this.sendRequest({
|
|
238
|
+
type: "get",
|
|
239
|
+
storeName,
|
|
240
|
+
key,
|
|
241
|
+
});
|
|
242
|
+
return this.handleResponse<T | undefined>(response);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async add(storeName: string, items: unknown[]): Promise<void> {
|
|
246
|
+
const response = await this.sendRequest({
|
|
247
|
+
type: "add",
|
|
248
|
+
storeName,
|
|
249
|
+
items,
|
|
250
|
+
});
|
|
251
|
+
this.handleResponse<void>(response);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async put(storeName: string, items: unknown[]): Promise<void> {
|
|
255
|
+
const response = await this.sendRequest({
|
|
256
|
+
type: "put",
|
|
257
|
+
storeName,
|
|
258
|
+
items,
|
|
259
|
+
});
|
|
260
|
+
this.handleResponse<void>(response);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async delete(storeName: string, keys: IDBValidKey[]): Promise<void> {
|
|
264
|
+
const response = await this.sendRequest({
|
|
265
|
+
type: "delete",
|
|
266
|
+
storeName,
|
|
267
|
+
keys,
|
|
268
|
+
});
|
|
269
|
+
this.handleResponse<void>(response);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async clear(storeName: string): Promise<void> {
|
|
273
|
+
const response = await this.sendRequest({
|
|
274
|
+
type: "clear",
|
|
275
|
+
storeName,
|
|
276
|
+
});
|
|
277
|
+
this.handleResponse<void>(response);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Close is an alias for disconnect.
|
|
282
|
+
* Required by IDBDatabaseLike interface.
|
|
283
|
+
*/
|
|
284
|
+
close(): void {
|
|
285
|
+
this.disconnect();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Creates an IDBCreator that returns proxy clients connected to a remote server.
|
|
291
|
+
* Clients are cached by database name, so multiple calls return the same client.
|
|
292
|
+
*
|
|
293
|
+
* @param transport The transport to use for communication
|
|
294
|
+
* @param onSync Optional handler called when any sync message is received
|
|
295
|
+
*
|
|
296
|
+
* @example
|
|
297
|
+
* const dbCreator = createProxyDbCreator(transport, (msg) => {
|
|
298
|
+
* console.log('Sync:', msg.type, msg.storeName);
|
|
299
|
+
* });
|
|
300
|
+
*
|
|
301
|
+
* <DrizzleIndexedDBProvider dbCreator={dbCreator} ... />
|
|
302
|
+
*/
|
|
303
|
+
export function createProxyDbCreator(
|
|
304
|
+
transport: IDBProxyClientTransport,
|
|
305
|
+
onSync?: SyncHandler,
|
|
306
|
+
): IDBCreator {
|
|
307
|
+
// Cache clients by database name - React may call dbCreator multiple times
|
|
308
|
+
const clientCache = new Map<string, IDBProxyClient>();
|
|
309
|
+
const connectingCache = new Map<string, Promise<IDBProxyClient>>();
|
|
310
|
+
|
|
311
|
+
return async (
|
|
312
|
+
name: string,
|
|
313
|
+
_options?: IDBOpenOptions,
|
|
314
|
+
): Promise<IDBDatabaseLike> => {
|
|
315
|
+
// Return cached client if already connected
|
|
316
|
+
const cached = clientCache.get(name);
|
|
317
|
+
if (cached) {
|
|
318
|
+
return cached;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// If currently connecting, wait for that connection
|
|
322
|
+
const connecting = connectingCache.get(name);
|
|
323
|
+
if (connecting) {
|
|
324
|
+
return connecting;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Create new client and connect
|
|
328
|
+
const connectPromise = (async () => {
|
|
329
|
+
const proxy = new IDBProxyClient(name, transport);
|
|
330
|
+
|
|
331
|
+
// Register sync handler if provided
|
|
332
|
+
if (onSync) {
|
|
333
|
+
proxy.onSync(onSync);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
await proxy.connect();
|
|
337
|
+
clientCache.set(name, proxy);
|
|
338
|
+
connectingCache.delete(name);
|
|
339
|
+
return proxy;
|
|
340
|
+
})();
|
|
341
|
+
|
|
342
|
+
connectingCache.set(name, connectPromise);
|
|
343
|
+
return connectPromise;
|
|
344
|
+
};
|
|
345
|
+
}
|