@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.
- package/CHANGELOG.md +31 -0
- package/package.json +10 -7
- package/src/collections/drizzle-indexeddb-collection.ts +310 -0
- package/src/context/DrizzleIndexedDBProvider.tsx +6 -91
- package/src/function-migrator.ts +3 -0
- package/src/idb-types.ts +2 -12
- package/src/index.ts +4 -32
- package/src/instrumented-idb-database.ts +1 -1
- package/src/native-idb-database.ts +4 -1
- package/src/standalone-collection.ts +8 -12
- package/src/collections/indexeddb-collection.ts +0 -579
- package/src/proxy/idb-proxy-client.ts +0 -341
- package/src/proxy/idb-proxy-server.ts +0 -313
- package/src/proxy/idb-proxy-transport.ts +0 -174
- package/src/proxy/idb-proxy-types.ts +0 -77
- package/src/proxy/idb-sync-adapter.ts +0 -95
- package/src/proxy/index.ts +0 -37
|
@@ -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
|
-
}
|