@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
|
@@ -0,0 +1,313 @@
|
|
|
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:clear",
|
|
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
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IDBProxyRequest,
|
|
3
|
+
IDBProxyResponse,
|
|
4
|
+
IDBProxySyncMessage,
|
|
5
|
+
} from "./idb-proxy-types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Client-side transport interface.
|
|
9
|
+
* Implement this to connect to an IDB proxy server via any messaging system.
|
|
10
|
+
*
|
|
11
|
+
* Examples of transports:
|
|
12
|
+
* - Chrome extension: chrome.runtime.sendMessage
|
|
13
|
+
* - WebSocket: ws.send + onmessage
|
|
14
|
+
* - MessageChannel: port.postMessage
|
|
15
|
+
* - In-memory (for testing): direct function call
|
|
16
|
+
*/
|
|
17
|
+
export interface IDBProxyClientTransport {
|
|
18
|
+
/**
|
|
19
|
+
* Send a request to the server and wait for a response.
|
|
20
|
+
* The transport is responsible for correlating request/response by ID.
|
|
21
|
+
*/
|
|
22
|
+
sendRequest(request: IDBProxyRequest): Promise<IDBProxyResponse>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Register a handler for sync messages from the server.
|
|
26
|
+
* These are broadcasts when other clients modify data.
|
|
27
|
+
*/
|
|
28
|
+
onSync(handler: (message: IDBProxySyncMessage) => void): void;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Optional: Clean up resources when the client is done.
|
|
32
|
+
*/
|
|
33
|
+
dispose?(): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Server-side transport interface.
|
|
38
|
+
* Implement this to receive requests from IDB proxy clients.
|
|
39
|
+
*/
|
|
40
|
+
export interface IDBProxyServerTransport {
|
|
41
|
+
/**
|
|
42
|
+
* Register a handler for incoming requests.
|
|
43
|
+
* The handler should process the request and return a response.
|
|
44
|
+
* The transport is responsible for sending the response back to the client.
|
|
45
|
+
*/
|
|
46
|
+
onRequest(
|
|
47
|
+
handler: (request: IDBProxyRequest) => Promise<IDBProxyResponse>,
|
|
48
|
+
): void;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Broadcast a sync message to all clients except the one specified.
|
|
52
|
+
* @param message The sync message to broadcast
|
|
53
|
+
* @param excludeClientId Client ID to exclude from the broadcast (the initiator)
|
|
54
|
+
*/
|
|
55
|
+
broadcast(message: IDBProxySyncMessage, excludeClientId: string): void;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Optional: Clean up resources when the server is done.
|
|
59
|
+
*/
|
|
60
|
+
dispose?(): void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A simple in-memory transport for testing.
|
|
65
|
+
* Connects a client and server directly without any actual messaging.
|
|
66
|
+
*/
|
|
67
|
+
export function createInMemoryTransport(): {
|
|
68
|
+
clientTransport: IDBProxyClientTransport;
|
|
69
|
+
serverTransport: IDBProxyServerTransport;
|
|
70
|
+
} {
|
|
71
|
+
let requestHandler:
|
|
72
|
+
| ((request: IDBProxyRequest) => Promise<IDBProxyResponse>)
|
|
73
|
+
| null = null;
|
|
74
|
+
let syncHandler: ((message: IDBProxySyncMessage) => void) | null = null;
|
|
75
|
+
const clientId = `single-client-${Date.now()}`;
|
|
76
|
+
|
|
77
|
+
const clientTransport: IDBProxyClientTransport = {
|
|
78
|
+
async sendRequest(request): Promise<IDBProxyResponse> {
|
|
79
|
+
if (!requestHandler) {
|
|
80
|
+
return {
|
|
81
|
+
id: request.id,
|
|
82
|
+
type: "error",
|
|
83
|
+
error: "No server handler registered",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return requestHandler(request);
|
|
87
|
+
},
|
|
88
|
+
onSync(handler): void {
|
|
89
|
+
syncHandler = handler;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const serverTransport: IDBProxyServerTransport = {
|
|
94
|
+
onRequest(handler): void {
|
|
95
|
+
requestHandler = handler;
|
|
96
|
+
},
|
|
97
|
+
broadcast(message, excludeClientId): void {
|
|
98
|
+
// In single-client transport, only send if the client isn't excluded
|
|
99
|
+
if (syncHandler && excludeClientId !== clientId) {
|
|
100
|
+
syncHandler(message);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return { clientTransport, serverTransport };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A broadcast transport that supports multiple clients connecting to one server.
|
|
110
|
+
* Useful for testing N-client scenarios.
|
|
111
|
+
*/
|
|
112
|
+
export function createMultiClientTransport(): {
|
|
113
|
+
createClientTransport: () => IDBProxyClientTransport;
|
|
114
|
+
serverTransport: IDBProxyServerTransport;
|
|
115
|
+
} {
|
|
116
|
+
let requestHandler:
|
|
117
|
+
| ((request: IDBProxyRequest) => Promise<IDBProxyResponse>)
|
|
118
|
+
| null = null;
|
|
119
|
+
|
|
120
|
+
// Track all connected clients and their sync handlers
|
|
121
|
+
const clients = new Map<
|
|
122
|
+
string,
|
|
123
|
+
{ syncHandler: ((message: IDBProxySyncMessage) => void) | null }
|
|
124
|
+
>();
|
|
125
|
+
|
|
126
|
+
const createClientTransport = (): IDBProxyClientTransport => {
|
|
127
|
+
const clientId = `client-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
128
|
+
clients.set(clientId, { syncHandler: null });
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
async sendRequest(request: IDBProxyRequest): Promise<IDBProxyResponse> {
|
|
132
|
+
if (!requestHandler) {
|
|
133
|
+
return {
|
|
134
|
+
id: request.id,
|
|
135
|
+
type: "error",
|
|
136
|
+
error: "No server handler registered",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// Inject clientId into request
|
|
140
|
+
return requestHandler({ ...request, clientId });
|
|
141
|
+
},
|
|
142
|
+
onSync(handler: (message: IDBProxySyncMessage) => void): void {
|
|
143
|
+
const client = clients.get(clientId);
|
|
144
|
+
if (client) {
|
|
145
|
+
client.syncHandler = handler;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
dispose(): void {
|
|
149
|
+
clients.delete(clientId);
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const serverTransport: IDBProxyServerTransport = {
|
|
155
|
+
onRequest(
|
|
156
|
+
handler: (request: IDBProxyRequest) => Promise<IDBProxyResponse>,
|
|
157
|
+
): void {
|
|
158
|
+
requestHandler = handler;
|
|
159
|
+
},
|
|
160
|
+
broadcast(message: IDBProxySyncMessage, excludeClientId: string): void {
|
|
161
|
+
// Send to all clients except the one that initiated the change
|
|
162
|
+
for (const [clientId, client] of clients) {
|
|
163
|
+
if (clientId !== excludeClientId && client.syncHandler) {
|
|
164
|
+
client.syncHandler(message);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
dispose(): void {
|
|
169
|
+
clients.clear();
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return { createClientTransport, serverTransport };
|
|
174
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { KeyRangeSpec } from "../idb-types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Request body types (without the common fields)
|
|
5
|
+
*/
|
|
6
|
+
export type IDBProxyRequestBody =
|
|
7
|
+
// Connection (client session, not database lifecycle)
|
|
8
|
+
| { type: "connect" }
|
|
9
|
+
| { type: "disconnect" }
|
|
10
|
+
// Read operations
|
|
11
|
+
| { type: "getAll"; storeName: string }
|
|
12
|
+
| {
|
|
13
|
+
type: "getAllByIndex";
|
|
14
|
+
storeName: string;
|
|
15
|
+
indexName: string;
|
|
16
|
+
keyRange?: KeyRangeSpec;
|
|
17
|
+
}
|
|
18
|
+
| { type: "get"; storeName: string; key: IDBValidKey }
|
|
19
|
+
// Write operations
|
|
20
|
+
| { type: "add"; storeName: string; items: unknown[] }
|
|
21
|
+
| { type: "put"; storeName: string; items: unknown[] }
|
|
22
|
+
| { type: "delete"; storeName: string; keys: IDBValidKey[] }
|
|
23
|
+
| { type: "clear"; storeName: string }
|
|
24
|
+
// Metadata (read-only)
|
|
25
|
+
| { type: "getVersion" }
|
|
26
|
+
| { type: "hasStore"; storeName: string }
|
|
27
|
+
| { type: "getStoreNames" }
|
|
28
|
+
| { type: "getStoreIndexes"; storeName: string };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Full request type with all required fields
|
|
32
|
+
*/
|
|
33
|
+
export type IDBProxyRequest = {
|
|
34
|
+
/** Unique request ID for correlating responses */
|
|
35
|
+
id: string;
|
|
36
|
+
/** Unique client ID for tracking who sent the request */
|
|
37
|
+
clientId: string;
|
|
38
|
+
/** Database name */
|
|
39
|
+
dbName: string;
|
|
40
|
+
} & IDBProxyRequestBody;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Response types for IDB proxy operations
|
|
44
|
+
*/
|
|
45
|
+
export type IDBProxyResponse =
|
|
46
|
+
| { id: string; type: "success"; data?: unknown }
|
|
47
|
+
| { id: string; type: "error"; error: string };
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sync messages broadcast to clients when data changes.
|
|
51
|
+
* These are sent from server to clients to keep them in sync.
|
|
52
|
+
*/
|
|
53
|
+
export type IDBProxySyncMessage = {
|
|
54
|
+
/** Database that was modified */
|
|
55
|
+
dbName: string;
|
|
56
|
+
/** Store that was modified */
|
|
57
|
+
storeName: string;
|
|
58
|
+
} & (
|
|
59
|
+
| { type: "sync:add"; items: unknown[] }
|
|
60
|
+
| { type: "sync:put"; items: unknown[] }
|
|
61
|
+
| { type: "sync:delete"; keys: IDBValidKey[] }
|
|
62
|
+
| { type: "sync:clear" }
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate a unique request ID
|
|
67
|
+
*/
|
|
68
|
+
export function generateRequestId(): string {
|
|
69
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generate a unique client ID
|
|
74
|
+
*/
|
|
75
|
+
export function generateClientId(): string {
|
|
76
|
+
return `client-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
77
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ExternalSyncHandler } from "@firtoz/drizzle-utils";
|
|
2
|
+
import type { IDBProxySyncMessage } from "./idb-proxy-types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a sync message handler that translates proxy sync messages
|
|
6
|
+
* into collection external sync events.
|
|
7
|
+
*
|
|
8
|
+
* @param storeName The store name to handle sync for
|
|
9
|
+
* @param pushExternalSync The collection's external sync handler (from syncResult)
|
|
10
|
+
* @param options Optional configuration
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Get the sync handler from the collection
|
|
14
|
+
* const { pushExternalSync } = syncResult;
|
|
15
|
+
*
|
|
16
|
+
* // Create the adapter
|
|
17
|
+
* const handleSync = createCollectionSyncHandler(
|
|
18
|
+
* 'todo',
|
|
19
|
+
* pushExternalSync,
|
|
20
|
+
* { debug: true }
|
|
21
|
+
* );
|
|
22
|
+
*
|
|
23
|
+
* // Connect to proxy client
|
|
24
|
+
* proxyClient.onSync(handleSync);
|
|
25
|
+
*/
|
|
26
|
+
export function createCollectionSyncHandler<T = unknown>(
|
|
27
|
+
storeName: string,
|
|
28
|
+
pushExternalSync: ExternalSyncHandler<T>,
|
|
29
|
+
options?: { debug?: boolean },
|
|
30
|
+
): (message: IDBProxySyncMessage) => void {
|
|
31
|
+
const debug = options?.debug ?? false;
|
|
32
|
+
|
|
33
|
+
return (message: IDBProxySyncMessage) => {
|
|
34
|
+
// Ignore messages for other stores
|
|
35
|
+
if (message.storeName !== storeName) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (debug) {
|
|
40
|
+
console.log(`[SyncAdapter:${storeName}]`, message.type, message);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
switch (message.type) {
|
|
44
|
+
case "sync:add":
|
|
45
|
+
pushExternalSync({
|
|
46
|
+
type: "insert",
|
|
47
|
+
items: message.items as T[],
|
|
48
|
+
});
|
|
49
|
+
break;
|
|
50
|
+
|
|
51
|
+
case "sync:put":
|
|
52
|
+
pushExternalSync({
|
|
53
|
+
type: "update",
|
|
54
|
+
items: message.items as T[],
|
|
55
|
+
});
|
|
56
|
+
break;
|
|
57
|
+
|
|
58
|
+
case "sync:delete":
|
|
59
|
+
// For delete, we need the full items, but we only have keys
|
|
60
|
+
// The collection will handle this via the key
|
|
61
|
+
// We'll need to construct minimal items with just the id
|
|
62
|
+
pushExternalSync({
|
|
63
|
+
type: "delete",
|
|
64
|
+
items: message.keys.map((key) => ({ id: key })) as T[],
|
|
65
|
+
});
|
|
66
|
+
break;
|
|
67
|
+
|
|
68
|
+
case "sync:clear":
|
|
69
|
+
pushExternalSync({
|
|
70
|
+
type: "truncate",
|
|
71
|
+
});
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Combines multiple sync handlers into one.
|
|
79
|
+
* Use when you have multiple stores to sync from the same proxy client.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* const todoHandler = createCollectionSyncHandler('todo', todoSync);
|
|
83
|
+
* const userHandler = createCollectionSyncHandler('user', userSync);
|
|
84
|
+
*
|
|
85
|
+
* proxyClient.onSync(combineSyncHandlers([todoHandler, userHandler]));
|
|
86
|
+
*/
|
|
87
|
+
export function combineSyncHandlers(
|
|
88
|
+
handlers: Array<(message: IDBProxySyncMessage) => void>,
|
|
89
|
+
): (message: IDBProxySyncMessage) => void {
|
|
90
|
+
return (message: IDBProxySyncMessage) => {
|
|
91
|
+
for (const handler of handlers) {
|
|
92
|
+
handler(message);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Proxy types
|
|
2
|
+
export {
|
|
3
|
+
type IDBProxyRequest,
|
|
4
|
+
type IDBProxyRequestBody,
|
|
5
|
+
type IDBProxyResponse,
|
|
6
|
+
type IDBProxySyncMessage,
|
|
7
|
+
generateRequestId,
|
|
8
|
+
generateClientId,
|
|
9
|
+
} from "./idb-proxy-types";
|
|
10
|
+
|
|
11
|
+
// Transport interfaces
|
|
12
|
+
export {
|
|
13
|
+
type IDBProxyClientTransport,
|
|
14
|
+
type IDBProxyServerTransport,
|
|
15
|
+
createInMemoryTransport,
|
|
16
|
+
createMultiClientTransport,
|
|
17
|
+
} from "./idb-proxy-transport";
|
|
18
|
+
|
|
19
|
+
// Proxy client
|
|
20
|
+
export {
|
|
21
|
+
IDBProxyClient,
|
|
22
|
+
createProxyDbCreator,
|
|
23
|
+
type SyncHandler,
|
|
24
|
+
} from "./idb-proxy-client";
|
|
25
|
+
|
|
26
|
+
// Proxy server
|
|
27
|
+
export {
|
|
28
|
+
IDBProxyServer,
|
|
29
|
+
createProxyServer,
|
|
30
|
+
type IDBProxyServerOptions,
|
|
31
|
+
} from "./idb-proxy-server";
|
|
32
|
+
|
|
33
|
+
// Sync adapter (connects proxy sync to collection)
|
|
34
|
+
export {
|
|
35
|
+
createCollectionSyncHandler,
|
|
36
|
+
combineSyncHandlers,
|
|
37
|
+
} from "./idb-sync-adapter";
|