@firtoz/drizzle-indexeddb 0.6.2 → 2.0.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.
@@ -1,341 +0,0 @@
1
- import type {
2
- IDBDatabaseLike,
3
- IDBCreator,
4
- IndexInfo,
5
- CreateStoreOptions,
6
- CreateIndexOptions,
7
- KeyRangeSpec,
8
- } from "../idb-types";
9
- import type { IDBProxyClientTransport } from "./idb-proxy-transport";
10
- import type {
11
- IDBProxyRequest,
12
- IDBProxyRequestBody,
13
- IDBProxyResponse,
14
- IDBProxySyncMessage,
15
- } from "./idb-proxy-types";
16
- import { generateRequestId, generateClientId } from "./idb-proxy-types";
17
-
18
- /**
19
- * Handler for sync messages from the server
20
- */
21
- export type SyncHandler = (message: IDBProxySyncMessage) => void;
22
-
23
- /**
24
- * A proxy implementation of IDBDatabaseLike that sends all operations
25
- * to a remote server via a transport layer.
26
- *
27
- * This is used by content scripts or other clients that don't have
28
- * direct access to IndexedDB (e.g., in a Chrome extension context).
29
- *
30
- * The client manages a "connection" (session) to the server.
31
- * The server manages the actual database lifecycle.
32
- *
33
- * When other clients modify data, the server broadcasts sync messages
34
- * which this client receives and can handle via onSync().
35
- */
36
- export class IDBProxyClient implements IDBDatabaseLike {
37
- private _version: number = 0;
38
- private _storeNames: string[] = [];
39
- private _storeIndexes: Map<string, IndexInfo[]> = new Map();
40
- private _connected: boolean = false;
41
- private _clientId: string;
42
- private _syncHandlers: Set<SyncHandler> = new Set();
43
-
44
- constructor(
45
- private dbName: string,
46
- private transport: IDBProxyClientTransport,
47
- clientId?: string,
48
- ) {
49
- this._clientId = clientId ?? generateClientId();
50
-
51
- // Listen for sync messages from server
52
- this.transport.onSync((message) => {
53
- // Only handle messages for this database
54
- if (message.dbName === this.dbName) {
55
- for (const handler of this._syncHandlers) {
56
- handler(message);
57
- }
58
- }
59
- });
60
- }
61
-
62
- /**
63
- * Get the unique client ID
64
- */
65
- get clientId(): string {
66
- return this._clientId;
67
- }
68
-
69
- /**
70
- * Register a handler for sync messages from other clients.
71
- * Returns an unsubscribe function.
72
- */
73
- onSync(handler: SyncHandler): () => void {
74
- this._syncHandlers.add(handler);
75
- return () => {
76
- this._syncHandlers.delete(handler);
77
- };
78
- }
79
-
80
- /**
81
- * Connect to the server and fetch database metadata.
82
- * The server will ensure the database is open and migrated.
83
- */
84
- async connect(): Promise<void> {
85
- if (this._connected) {
86
- return;
87
- }
88
-
89
- // Tell the server we want to connect to this database
90
- const connectResponse = await this.sendRequest({ type: "connect" });
91
-
92
- if (connectResponse.type === "error") {
93
- throw new Error(connectResponse.error || "Failed to connect to database");
94
- }
95
-
96
- // Fetch metadata
97
- const versionResponse = await this.sendRequest({ type: "getVersion" });
98
- if (versionResponse.type === "success") {
99
- this._version = versionResponse.data as number;
100
- }
101
-
102
- const storeNamesResponse = await this.sendRequest({
103
- type: "getStoreNames",
104
- });
105
- if (storeNamesResponse.type === "success") {
106
- this._storeNames = storeNamesResponse.data as string[];
107
- }
108
-
109
- this._connected = true;
110
- }
111
-
112
- /**
113
- * Disconnect from the server.
114
- * This is a no-op since clients are cached and reused.
115
- * The connection stays open for future use.
116
- */
117
- disconnect(): void {
118
- // Intentionally a no-op - clients are cached and reused
119
- }
120
-
121
- private async sendRequest(
122
- request: IDBProxyRequestBody,
123
- ): Promise<IDBProxyResponse> {
124
- const fullRequest: IDBProxyRequest = {
125
- ...request,
126
- id: generateRequestId(),
127
- clientId: this._clientId,
128
- dbName: this.dbName,
129
- };
130
- return this.transport.sendRequest(fullRequest);
131
- }
132
-
133
- private handleResponse<T>(response: IDBProxyResponse): T {
134
- if (response.type === "error") {
135
- throw new Error(response.error || "Unknown server error");
136
- }
137
- return response.data as T;
138
- }
139
-
140
- get version(): number {
141
- return this._version;
142
- }
143
-
144
- // =========================================================================
145
- // Schema Operations - Cached locally, read-only for clients
146
- // =========================================================================
147
-
148
- hasStore(storeName: string): boolean {
149
- return this._storeNames.includes(storeName);
150
- }
151
-
152
- getStoreNames(): string[] {
153
- return [...this._storeNames];
154
- }
155
-
156
- createStore(_storeName: string, _options?: CreateStoreOptions): void {
157
- throw new Error(
158
- "Schema modifications not supported on proxy client. Use server-side migrations.",
159
- );
160
- }
161
-
162
- deleteStore(_storeName: string): void {
163
- throw new Error(
164
- "Schema modifications not supported on proxy client. Use server-side migrations.",
165
- );
166
- }
167
-
168
- createIndex(
169
- _storeName: string,
170
- _indexName: string,
171
- _keyPath: string | string[],
172
- _options?: CreateIndexOptions,
173
- ): void {
174
- throw new Error(
175
- "Schema modifications not supported on proxy client. Use server-side migrations.",
176
- );
177
- }
178
-
179
- deleteIndex(_storeName: string, _indexName: string): void {
180
- throw new Error(
181
- "Schema modifications not supported on proxy client. Use server-side migrations.",
182
- );
183
- }
184
-
185
- getStoreIndexes(storeName: string): IndexInfo[] {
186
- const cached = this._storeIndexes.get(storeName);
187
- if (cached) {
188
- return cached;
189
- }
190
- return [];
191
- }
192
-
193
- /**
194
- * Async version to fetch indexes from server
195
- */
196
- async fetchStoreIndexes(storeName: string): Promise<IndexInfo[]> {
197
- const response = await this.sendRequest({
198
- type: "getStoreIndexes",
199
- storeName,
200
- });
201
- const indexes = this.handleResponse<IndexInfo[]>(response);
202
- this._storeIndexes.set(storeName, indexes);
203
- return indexes;
204
- }
205
-
206
- // =========================================================================
207
- // Data Operations - All proxied to server
208
- // =========================================================================
209
-
210
- async getAll<T = unknown>(storeName: string): Promise<T[]> {
211
- const response = await this.sendRequest({
212
- type: "getAll",
213
- storeName,
214
- });
215
- return this.handleResponse<T[]>(response);
216
- }
217
-
218
- async getAllByIndex<T = unknown>(
219
- storeName: string,
220
- indexName: string,
221
- keyRange?: KeyRangeSpec,
222
- ): Promise<T[]> {
223
- const response = await this.sendRequest({
224
- type: "getAllByIndex",
225
- storeName,
226
- indexName,
227
- keyRange,
228
- });
229
- return this.handleResponse<T[]>(response);
230
- }
231
-
232
- async get<T = unknown>(
233
- storeName: string,
234
- key: IDBValidKey,
235
- ): Promise<T | undefined> {
236
- const response = await this.sendRequest({
237
- type: "get",
238
- storeName,
239
- key,
240
- });
241
- return this.handleResponse<T | undefined>(response);
242
- }
243
-
244
- async add(storeName: string, items: unknown[]): Promise<void> {
245
- const response = await this.sendRequest({
246
- type: "add",
247
- storeName,
248
- items,
249
- });
250
- this.handleResponse<void>(response);
251
- }
252
-
253
- async put(storeName: string, items: unknown[]): Promise<void> {
254
- const response = await this.sendRequest({
255
- type: "put",
256
- storeName,
257
- items,
258
- });
259
- this.handleResponse<void>(response);
260
- }
261
-
262
- async delete(storeName: string, keys: IDBValidKey[]): Promise<void> {
263
- const response = await this.sendRequest({
264
- type: "delete",
265
- storeName,
266
- keys,
267
- });
268
- this.handleResponse<void>(response);
269
- }
270
-
271
- async clear(storeName: string): Promise<void> {
272
- const response = await this.sendRequest({
273
- type: "clear",
274
- storeName,
275
- });
276
- this.handleResponse<void>(response);
277
- }
278
-
279
- /**
280
- * Close is an alias for disconnect.
281
- * Required by IDBDatabaseLike interface.
282
- */
283
- close(): void {
284
- this.disconnect();
285
- }
286
- }
287
-
288
- /**
289
- * Creates an IDBCreator that returns proxy clients connected to a remote server.
290
- * Clients are cached by database name, so multiple calls return the same client.
291
- *
292
- * @param transport The transport to use for communication
293
- * @param onSync Optional handler called when any sync message is received
294
- *
295
- * @example
296
- * const dbCreator = createProxyIDbCreator(transport, (msg) => {
297
- * console.log('Sync:', msg.type, msg.storeName);
298
- * });
299
- *
300
- * <DrizzleIndexedDBProvider dbCreator={dbCreator} ... />
301
- */
302
- export function createProxyIDbCreator(
303
- transport: IDBProxyClientTransport,
304
- onSync?: SyncHandler,
305
- ): IDBCreator {
306
- // Cache clients by database name - React may call dbCreator multiple times
307
- const clientCache = new Map<string, IDBProxyClient>();
308
- const connectingCache = new Map<string, Promise<IDBProxyClient>>();
309
-
310
- return async (name: string): Promise<IDBDatabaseLike> => {
311
- // Return cached client if already connected
312
- const cached = clientCache.get(name);
313
- if (cached) {
314
- return cached;
315
- }
316
-
317
- // If currently connecting, wait for that connection
318
- const connecting = connectingCache.get(name);
319
- if (connecting) {
320
- return connecting;
321
- }
322
-
323
- // Create new client and connect
324
- const connectPromise = (async () => {
325
- const proxy = new IDBProxyClient(name, transport);
326
-
327
- // Register sync handler if provided
328
- if (onSync) {
329
- proxy.onSync(onSync);
330
- }
331
-
332
- await proxy.connect();
333
- clientCache.set(name, proxy);
334
- connectingCache.delete(name);
335
- return proxy;
336
- })();
337
-
338
- connectingCache.set(name, connectPromise);
339
- return connectPromise;
340
- };
341
- }
@@ -1,313 +0,0 @@
1
- import type { IDBDatabaseLike, IDBCreator } from "../idb-types";
2
- import { defaultIDBCreator } from "../native-idb-database";
3
- import type { IDBProxyServerTransport } from "./idb-proxy-transport";
4
- import type { IDBProxyRequest, IDBProxyResponse } from "./idb-proxy-types";
5
-
6
- /**
7
- * Options for creating an IDB proxy server
8
- */
9
- export interface IDBProxyServerOptions {
10
- /**
11
- * Transport to receive requests from clients
12
- */
13
- transport: IDBProxyServerTransport;
14
-
15
- /**
16
- * Optional custom IDB creator (uses native IndexedDB by default)
17
- */
18
- dbCreator?: IDBCreator;
19
-
20
- /**
21
- * Called when a database needs to be initialized (first client connects).
22
- * Use this to run migrations before the database is used.
23
- */
24
- onDatabaseInit?: (dbName: string, db: IDBDatabaseLike) => Promise<void>;
25
-
26
- /**
27
- * Enable debug logging
28
- */
29
- debug?: boolean;
30
- }
31
-
32
- /**
33
- * IDB Proxy Server - Handles requests from proxy clients and performs
34
- * actual IndexedDB operations.
35
- *
36
- * The server manages database lifecycle:
37
- * - Databases are opened on first client connection
38
- * - Databases stay open for all clients to share
39
- * - Databases are only closed when the server stops
40
- *
41
- * When data is mutated, the server broadcasts sync messages to all other
42
- * clients so they can update their local state.
43
- *
44
- * @example
45
- * const server = new IDBProxyServer({
46
- * transport: createChromeExtensionServerTransport(),
47
- * onDatabaseInit: async (dbName) => {
48
- * await migrateIndexedDBWithFunctions(dbName, migrations);
49
- * }
50
- * });
51
- * server.start();
52
- */
53
- export class IDBProxyServer {
54
- private databases: Map<string, IDBDatabaseLike> = new Map();
55
- private pendingDatabases: Map<string, Promise<void>> = new Map();
56
- private dbCreator: IDBCreator;
57
- private debug: boolean;
58
-
59
- constructor(private options: IDBProxyServerOptions) {
60
- this.dbCreator = options.dbCreator ?? defaultIDBCreator;
61
- this.debug = options.debug ?? false;
62
- }
63
-
64
- /**
65
- * Start listening for requests from clients
66
- */
67
- start(): void {
68
- this.options.transport.onRequest(async (request) => {
69
- return this.handleRequest(request);
70
- });
71
-
72
- if (this.debug) {
73
- console.log("[IDBProxyServer] Started");
74
- }
75
- }
76
-
77
- /**
78
- * Stop the server and close all databases
79
- */
80
- stop(): void {
81
- for (const [name, db] of this.databases.entries()) {
82
- db.close();
83
- if (this.debug) {
84
- console.log(`[IDBProxyServer] Closed database "${name}"`);
85
- }
86
- }
87
- this.databases.clear();
88
- this.options.transport.dispose?.();
89
-
90
- if (this.debug) {
91
- console.log("[IDBProxyServer] Stopped");
92
- }
93
- }
94
-
95
- /**
96
- * Handle an incoming request from a client
97
- */
98
- private async handleRequest(
99
- request: IDBProxyRequest,
100
- ): Promise<IDBProxyResponse> {
101
- if (this.debug) {
102
- console.log(
103
- "[IDBProxyServer] Request:",
104
- request.type,
105
- request.dbName,
106
- "from",
107
- request.clientId,
108
- );
109
- }
110
-
111
- try {
112
- const result = await this.processRequest(request);
113
- return { id: request.id, type: "success", data: result };
114
- } catch (error) {
115
- let errorMessage: string;
116
- if (error instanceof Error) {
117
- errorMessage = error.message || error.name || "Unknown error";
118
- console.error("[IDBProxyServer] Error:", error.stack || error);
119
- } else {
120
- errorMessage = String(error) || "Unknown error";
121
- console.error("[IDBProxyServer] Error:", error);
122
- }
123
- return { id: request.id, type: "error", error: errorMessage };
124
- }
125
- }
126
-
127
- /**
128
- * Process a request and return the result
129
- */
130
- private async processRequest(request: IDBProxyRequest): Promise<unknown> {
131
- switch (request.type) {
132
- case "connect":
133
- await this.ensureDatabase(request.dbName);
134
- return { connected: true };
135
-
136
- case "disconnect":
137
- return { disconnected: true };
138
-
139
- case "getVersion":
140
- return (await this.getDatabase(request.dbName)).version;
141
-
142
- case "hasStore":
143
- return (await this.getDatabase(request.dbName)).hasStore(
144
- request.storeName,
145
- );
146
-
147
- case "getStoreNames":
148
- return (await this.getDatabase(request.dbName)).getStoreNames();
149
-
150
- case "getStoreIndexes":
151
- return (await this.getDatabase(request.dbName)).getStoreIndexes(
152
- request.storeName,
153
- );
154
-
155
- case "getAll":
156
- return (await this.getDatabase(request.dbName)).getAll(
157
- request.storeName,
158
- );
159
-
160
- case "getAllByIndex":
161
- return (await this.getDatabase(request.dbName)).getAllByIndex(
162
- request.storeName,
163
- request.indexName,
164
- request.keyRange,
165
- );
166
-
167
- case "get":
168
- return (await this.getDatabase(request.dbName)).get(
169
- request.storeName,
170
- request.key,
171
- );
172
-
173
- case "add": {
174
- const db = await this.getDatabase(request.dbName);
175
- await db.add(request.storeName, request.items);
176
- // Broadcast to other clients
177
- this.options.transport.broadcast(
178
- {
179
- type: "sync:add",
180
- dbName: request.dbName,
181
- storeName: request.storeName,
182
- items: request.items,
183
- },
184
- request.clientId,
185
- );
186
- return undefined;
187
- }
188
-
189
- case "put": {
190
- const db = await this.getDatabase(request.dbName);
191
- await db.put(request.storeName, request.items);
192
- // Broadcast to other clients
193
- this.options.transport.broadcast(
194
- {
195
- type: "sync:put",
196
- dbName: request.dbName,
197
- storeName: request.storeName,
198
- items: request.items,
199
- },
200
- request.clientId,
201
- );
202
- return undefined;
203
- }
204
-
205
- case "delete": {
206
- const db = await this.getDatabase(request.dbName);
207
- await db.delete(request.storeName, request.keys);
208
- // Broadcast to other clients
209
- this.options.transport.broadcast(
210
- {
211
- type: "sync:delete",
212
- dbName: request.dbName,
213
- storeName: request.storeName,
214
- keys: request.keys,
215
- },
216
- request.clientId,
217
- );
218
- return undefined;
219
- }
220
-
221
- case "clear": {
222
- const db = await this.getDatabase(request.dbName);
223
- await db.clear(request.storeName);
224
- // Broadcast to other clients
225
- this.options.transport.broadcast(
226
- {
227
- type: "sync:truncate",
228
- dbName: request.dbName,
229
- storeName: request.storeName,
230
- },
231
- request.clientId,
232
- );
233
- return undefined;
234
- }
235
-
236
- default:
237
- throw new Error(
238
- `Unknown request type: ${(request as { type: string }).type}`,
239
- );
240
- }
241
- }
242
-
243
- /**
244
- * Get a database, opening it if needed
245
- */
246
- private async getDatabase(dbName: string): Promise<IDBDatabaseLike> {
247
- await this.ensureDatabase(dbName);
248
- // biome-ignore lint/style/noNonNullAssertion: ensureDatabase guarantees it exists
249
- return this.databases.get(dbName)!;
250
- }
251
-
252
- /**
253
- * Ensure a database is open and initialized.
254
- * Handles concurrent connection requests by having them wait for the same promise.
255
- */
256
- private async ensureDatabase(dbName: string): Promise<void> {
257
- // Already open
258
- if (this.databases.has(dbName)) {
259
- return;
260
- }
261
-
262
- // Currently being opened - wait for it
263
- const pending = this.pendingDatabases.get(dbName);
264
- if (pending) {
265
- await pending;
266
- return;
267
- }
268
-
269
- // Start opening
270
- if (this.debug) {
271
- console.log(`[IDBProxyServer] Opening database "${dbName}"`);
272
- }
273
-
274
- const openPromise = (async () => {
275
- const db = await this.dbCreator(dbName);
276
-
277
- if (!db) {
278
- throw new Error(
279
- `dbCreator returned null/undefined for database "${dbName}"`,
280
- );
281
- }
282
-
283
- this.databases.set(dbName, db);
284
-
285
- if (this.options.onDatabaseInit) {
286
- await this.options.onDatabaseInit(dbName, db);
287
- }
288
-
289
- if (this.debug) {
290
- console.log(`[IDBProxyServer] Database "${dbName}" ready`);
291
- }
292
- })();
293
-
294
- this.pendingDatabases.set(dbName, openPromise);
295
-
296
- try {
297
- await openPromise;
298
- } finally {
299
- this.pendingDatabases.delete(dbName);
300
- }
301
- }
302
- }
303
-
304
- /**
305
- * Convenience function to create and start a proxy server
306
- */
307
- export function createProxyServer(
308
- options: IDBProxyServerOptions,
309
- ): IDBProxyServer {
310
- const server = new IDBProxyServer(options);
311
- server.start();
312
- return server;
313
- }