database-devtools 0.1.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.
Files changed (94) hide show
  1. package/LICENSE +5 -0
  2. package/dist/adapter/index.cjs +314 -0
  3. package/dist/adapter/index.cjs.map +1 -0
  4. package/dist/adapter/index.d.cts +34 -0
  5. package/dist/adapter/index.d.ts +34 -0
  6. package/dist/adapter/index.js +6 -0
  7. package/dist/adapter/index.js.map +1 -0
  8. package/dist/adapter-BjJm71wW.d.cts +30 -0
  9. package/dist/adapter-DJSjDewg.d.ts +30 -0
  10. package/dist/adapters/sqlite/index.cjs +251 -0
  11. package/dist/adapters/sqlite/index.cjs.map +1 -0
  12. package/dist/adapters/sqlite/index.d.cts +21 -0
  13. package/dist/adapters/sqlite/index.d.ts +21 -0
  14. package/dist/adapters/sqlite/index.js +6 -0
  15. package/dist/adapters/sqlite/index.js.map +1 -0
  16. package/dist/chunk-2JGL2JVD.js +52 -0
  17. package/dist/chunk-2JGL2JVD.js.map +1 -0
  18. package/dist/chunk-AZECU6D7.js +27 -0
  19. package/dist/chunk-AZECU6D7.js.map +1 -0
  20. package/dist/chunk-BLOSGEQV.js +71 -0
  21. package/dist/chunk-BLOSGEQV.js.map +1 -0
  22. package/dist/chunk-CPVVIH5G.js +258 -0
  23. package/dist/chunk-CPVVIH5G.js.map +1 -0
  24. package/dist/chunk-DGUM43GV.js +10 -0
  25. package/dist/chunk-DGUM43GV.js.map +1 -0
  26. package/dist/chunk-HIDZTOJ3.js +9 -0
  27. package/dist/chunk-HIDZTOJ3.js.map +1 -0
  28. package/dist/chunk-I65IM47D.js +243 -0
  29. package/dist/chunk-I65IM47D.js.map +1 -0
  30. package/dist/chunk-MXVIGMVT.js +3 -0
  31. package/dist/chunk-MXVIGMVT.js.map +1 -0
  32. package/dist/chunk-O6AO7KHA.js +3 -0
  33. package/dist/chunk-O6AO7KHA.js.map +1 -0
  34. package/dist/chunk-TNDOAHT5.js +1784 -0
  35. package/dist/chunk-TNDOAHT5.js.map +1 -0
  36. package/dist/chunk-ZS5MZR75.js +655 -0
  37. package/dist/chunk-ZS5MZR75.js.map +1 -0
  38. package/dist/cli.cjs +2053 -0
  39. package/dist/cli.cjs.map +1 -0
  40. package/dist/client/index.cjs +947 -0
  41. package/dist/client/index.cjs.map +1 -0
  42. package/dist/client/index.d.cts +75 -0
  43. package/dist/client/index.d.ts +75 -0
  44. package/dist/client/index.js +98 -0
  45. package/dist/client/index.js.map +1 -0
  46. package/dist/createDevToolsServer-B7NQ_5H4.d.cts +292 -0
  47. package/dist/createDevToolsServer-DlNMoCJR.d.ts +292 -0
  48. package/dist/handleDeviceSnapshot-CR-4QMdk.d.ts +116 -0
  49. package/dist/handleDeviceSnapshot-cYGuitSc.d.cts +116 -0
  50. package/dist/index.cjs +5877 -0
  51. package/dist/index.cjs.map +1 -0
  52. package/dist/index.d.cts +61 -0
  53. package/dist/index.d.ts +61 -0
  54. package/dist/index.js +2748 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/inspector/index.cjs +57 -0
  57. package/dist/inspector/index.cjs.map +1 -0
  58. package/dist/inspector/index.d.cts +26 -0
  59. package/dist/inspector/index.d.ts +26 -0
  60. package/dist/inspector/index.js +5 -0
  61. package/dist/inspector/index.js.map +1 -0
  62. package/dist/inspectors/sqlite/index.cjs +306 -0
  63. package/dist/inspectors/sqlite/index.cjs.map +1 -0
  64. package/dist/inspectors/sqlite/index.d.cts +53 -0
  65. package/dist/inspectors/sqlite/index.d.ts +53 -0
  66. package/dist/inspectors/sqlite/index.js +260 -0
  67. package/dist/inspectors/sqlite/index.js.map +1 -0
  68. package/dist/register-BuicFu9L.d.cts +32 -0
  69. package/dist/register-aITLN9Q9.d.ts +32 -0
  70. package/dist/server/index.cjs +1948 -0
  71. package/dist/server/index.cjs.map +1 -0
  72. package/dist/server/index.d.cts +15 -0
  73. package/dist/server/index.d.ts +15 -0
  74. package/dist/server/index.js +7 -0
  75. package/dist/server/index.js.map +1 -0
  76. package/dist/snapshot-Dhyecgo2.d.cts +33 -0
  77. package/dist/snapshot-Dhyecgo2.d.ts +33 -0
  78. package/dist/types/protocol.cjs +309 -0
  79. package/dist/types/protocol.cjs.map +1 -0
  80. package/dist/types/protocol.d.cts +291 -0
  81. package/dist/types/protocol.d.ts +291 -0
  82. package/dist/types/protocol.js +4 -0
  83. package/dist/types/protocol.js.map +1 -0
  84. package/dist/types-BRLJdBRY.d.cts +16 -0
  85. package/dist/types-CF6qEU-_.d.ts +16 -0
  86. package/dist/types-CIA11diJ.d.cts +62 -0
  87. package/dist/types-CKVRCX-e.d.ts +62 -0
  88. package/dist/web/assets/index-CLEaIsUd.css +1 -0
  89. package/dist/web/assets/index-DQrPRTOy.js +100 -0
  90. package/dist/web/assets/sql-wasm-UFUCzYNW.wasm +0 -0
  91. package/dist/web/index.html +26 -0
  92. package/dist/write-C3toEIMs.d.cts +21 -0
  93. package/dist/write-C3toEIMs.d.ts +21 -0
  94. package/package.json +137 -0
@@ -0,0 +1,1784 @@
1
+ import { generateConnectionId, generatePingId } from './chunk-AZECU6D7.js';
2
+ import { DevToolsRole, HEARTBEAT_INTERVAL_MS, createMessage, MessageType, HEARTBEAT_TIMEOUT_MS, REFRESH_TIMEOUT_MS, DEFAULT_DEVTOOLS_HOST, DEFAULT_DEVTOOLS_PORT, DEVICE_SNAPSHOT_API_PATH, MAX_SNAPSHOT_BYTES, PROJECT_DATABASE_META_API_PATH, PROJECT_DATABASES_API_PATH, PROJECT_DEVICE_EXPORTS_API_PATH, PROJECT_DATABASE_API_PATH, buildDevToolsHttpUrl, buildDevToolsWsUrl, DEVTOOLS_WS_PATH, isRegisterMessage, isPongMessage, isRefreshRequestMessage, isExportSnapshotRequestMessage, isBeginTransactionRequestMessage, isCommitTransactionRequestMessage, isRollbackTransactionRequestMessage, isWriteRequestMessage, isTransactionAckMessage, isWriteAckMessage, WRITE_TRANSACTION_TIMEOUT_MS } from './chunk-CPVVIH5G.js';
3
+ import { SNAPSHOT_KIND_HEADER, SNAPSHOT_MIME_HEADER, SNAPSHOT_NAME_HEADER, SQLITE_SNAPSHOT_MIME_TYPE } from './chunk-HIDZTOJ3.js';
4
+ import express2 from 'express';
5
+ import { createServer } from 'http';
6
+ import { existsSync } from 'fs';
7
+ import path4 from 'path';
8
+ import { WebSocketServer } from 'ws';
9
+ import { mkdir, writeFile, readFile, stat, rm } from 'fs/promises';
10
+
11
+ // src/server/connectionManager.ts
12
+ var ConnectionManager = class {
13
+ clients = /* @__PURE__ */ new Map();
14
+ add(input) {
15
+ const now = Date.now();
16
+ const client = {
17
+ socket: input.socket,
18
+ connectionId: generateConnectionId(),
19
+ role: input.role,
20
+ deviceId: input.deviceId,
21
+ metadata: input.metadata,
22
+ connectedAt: now,
23
+ lastPongAt: now
24
+ };
25
+ this.clients.set(input.socket, client);
26
+ return client;
27
+ }
28
+ remove(socket) {
29
+ const client = this.clients.get(socket);
30
+ if (client) {
31
+ this.clients.delete(socket);
32
+ }
33
+ return client;
34
+ }
35
+ getBySocket(socket) {
36
+ return this.clients.get(socket);
37
+ }
38
+ getByConnectionId(connectionId) {
39
+ for (const client of this.clients.values()) {
40
+ if (client.connectionId === connectionId) {
41
+ return client;
42
+ }
43
+ }
44
+ return void 0;
45
+ }
46
+ getByDeviceId(deviceId) {
47
+ for (const client of this.clients.values()) {
48
+ if (client.deviceId === deviceId) {
49
+ return client;
50
+ }
51
+ }
52
+ return void 0;
53
+ }
54
+ getByRole(role) {
55
+ return [...this.clients.values()].filter((client) => client.role === role);
56
+ }
57
+ updateLastPong(socket) {
58
+ const client = this.clients.get(socket);
59
+ if (client) {
60
+ client.lastPongAt = Date.now();
61
+ }
62
+ }
63
+ getAll() {
64
+ return [...this.clients.values()];
65
+ }
66
+ };
67
+
68
+ // src/server/cors.ts
69
+ var ALLOWED_METHODS = "GET, POST, OPTIONS";
70
+ var ALLOWED_HEADERS = [
71
+ "Content-Type",
72
+ SNAPSHOT_KIND_HEADER,
73
+ SNAPSHOT_MIME_HEADER,
74
+ SNAPSHOT_NAME_HEADER
75
+ ].join(", ");
76
+ function resolveCorsOrigin(requestOrigin, configuredOrigin) {
77
+ if (configuredOrigin) {
78
+ return configuredOrigin;
79
+ }
80
+ if (!requestOrigin) {
81
+ return "*";
82
+ }
83
+ try {
84
+ const url = new URL(requestOrigin);
85
+ const isLocalDev = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "10.0.2.2";
86
+ if (isLocalDev) {
87
+ return requestOrigin;
88
+ }
89
+ } catch {
90
+ return "*";
91
+ }
92
+ return "*";
93
+ }
94
+ function createCorsMiddleware(options = {}) {
95
+ const configuredOrigin = options.allowedOrigin ?? process.env.DATABASE_DEVTOOLS_CORS_ORIGIN;
96
+ return (request, response, next) => {
97
+ const origin = resolveCorsOrigin(request.header("origin"), configuredOrigin);
98
+ response.setHeader("Access-Control-Allow-Origin", origin);
99
+ response.setHeader("Access-Control-Allow-Methods", ALLOWED_METHODS);
100
+ response.setHeader("Access-Control-Allow-Headers", ALLOWED_HEADERS);
101
+ response.setHeader("Vary", "Origin");
102
+ if (request.method === "OPTIONS") {
103
+ response.status(204).end();
104
+ return;
105
+ }
106
+ next();
107
+ };
108
+ }
109
+
110
+ // src/server/deviceRegistry.ts
111
+ var DeviceRegistry = class {
112
+ constructor(connectionManager) {
113
+ this.connectionManager = connectionManager;
114
+ }
115
+ connectionManager;
116
+ snapshot() {
117
+ const browsers = this.connectionManager.getByRole(DevToolsRole.BROWSER).map((client) => ({
118
+ connectionId: client.connectionId,
119
+ connectedAt: client.connectedAt
120
+ }));
121
+ const mobiles = this.connectionManager.getByRole(DevToolsRole.MOBILE).map((client) => ({
122
+ deviceId: client.deviceId ?? client.connectionId,
123
+ connectionId: client.connectionId,
124
+ connectedAt: client.connectedAt,
125
+ metadata: client.metadata
126
+ }));
127
+ return {
128
+ browserCount: browsers.length,
129
+ mobileCount: mobiles.length,
130
+ browsers,
131
+ mobiles
132
+ };
133
+ }
134
+ };
135
+
136
+ // src/utils/logger.ts
137
+ var RESET = "\x1B[0m";
138
+ var GREEN = "\x1B[32m";
139
+ var YELLOW = "\x1B[33m";
140
+ var CYAN = "\x1B[36m";
141
+ var RED = "\x1B[31m";
142
+ var DIM = "\x1B[2m";
143
+ var PREFIX = `${DIM}[database-devtools]${RESET}`;
144
+ function log(color, symbol, message) {
145
+ console.log(`${PREFIX} ${color}${symbol}${RESET} ${message}`);
146
+ }
147
+ var logger = {
148
+ browserConnected(connectionId) {
149
+ const suffix = connectionId ? ` ${DIM}(${connectionId})${RESET}` : "";
150
+ log(GREEN, "\u2713", `Browser Connected${suffix}`);
151
+ },
152
+ browserDisconnected(connectionId) {
153
+ const suffix = connectionId ? ` ${DIM}(${connectionId})${RESET}` : "";
154
+ log(YELLOW, "\u2713", `Browser Disconnected${suffix}`);
155
+ },
156
+ mobileConnected(deviceId) {
157
+ const suffix = deviceId ? ` ${DIM}(deviceId: ${deviceId})${RESET}` : "";
158
+ log(GREEN, "\u2713", `Mobile Connected${suffix}`);
159
+ },
160
+ mobileDisconnected(deviceId) {
161
+ const suffix = deviceId ? ` ${DIM}(deviceId: ${deviceId})${RESET}` : "";
162
+ log(YELLOW, "\u2713", `Mobile Disconnected${suffix}`);
163
+ },
164
+ broadcastSent(target) {
165
+ const suffix = target ? ` ${DIM}\u2192 ${target}${RESET}` : "";
166
+ log(CYAN, "\u2713", `Broadcast Sent${suffix}`);
167
+ },
168
+ serverStarted(url, wsUrl, webUiEnabled = false) {
169
+ log(GREEN, "\u2713", `Server started at ${url}`);
170
+ log(GREEN, "\u2713", `WebSocket endpoint ${wsUrl}`);
171
+ if (webUiEnabled) {
172
+ log(GREEN, "\u2713", `Browser UI available at ${url}`);
173
+ }
174
+ },
175
+ heartbeatTimeout(connectionId) {
176
+ log(RED, "\u2713", `Heartbeat timeout \u2014 removing ${connectionId}`);
177
+ },
178
+ refreshStarted(deviceId) {
179
+ log(CYAN, "\u21BB", `Refresh started ${DIM}(${deviceId})${RESET}`);
180
+ },
181
+ refreshUploaded(deviceId, bytes) {
182
+ log(GREEN, "\u21BB", `Snapshot uploaded ${DIM}(${deviceId}, ${bytes} bytes)${RESET}`);
183
+ },
184
+ refreshFailed(deviceId, code, message) {
185
+ log(RED, "\u21BB", `Refresh failed ${DIM}(${deviceId}: ${code})${RESET} ${message}`);
186
+ },
187
+ /** @deprecated Use refreshStarted */
188
+ syncStarted(syncId, deviceId) {
189
+ this.refreshStarted(deviceId);
190
+ },
191
+ /** @deprecated Use refreshUploaded */
192
+ syncUploaded(syncId, bytes) {
193
+ this.refreshUploaded(syncId, bytes);
194
+ },
195
+ /** @deprecated Use refreshFailed */
196
+ syncFailed(syncId, code, message) {
197
+ this.refreshFailed(syncId, code, message);
198
+ },
199
+ error(message) {
200
+ log(RED, "\u2717", message);
201
+ }
202
+ };
203
+
204
+ // src/server/heartbeat.ts
205
+ var Heartbeat = class {
206
+ constructor(connectionManager, router) {
207
+ this.connectionManager = connectionManager;
208
+ this.router = router;
209
+ }
210
+ connectionManager;
211
+ router;
212
+ interval = null;
213
+ start() {
214
+ if (this.interval !== null) {
215
+ return;
216
+ }
217
+ this.interval = setInterval(() => {
218
+ this.pingAll();
219
+ this.removeStaleConnections();
220
+ }, HEARTBEAT_INTERVAL_MS);
221
+ }
222
+ stop() {
223
+ if (this.interval !== null) {
224
+ clearInterval(this.interval);
225
+ this.interval = null;
226
+ }
227
+ }
228
+ pingAll() {
229
+ const pingId = generatePingId();
230
+ const message = createMessage({
231
+ type: MessageType.PING,
232
+ pingId
233
+ });
234
+ for (const client of this.connectionManager.getAll()) {
235
+ this.router.sendToSocket(client.socket, message);
236
+ }
237
+ }
238
+ removeStaleConnections() {
239
+ const now = Date.now();
240
+ for (const client of this.connectionManager.getAll()) {
241
+ if (now - client.lastPongAt > HEARTBEAT_INTERVAL_MS + HEARTBEAT_TIMEOUT_MS) {
242
+ logger.heartbeatTimeout(client.connectionId);
243
+ client.socket.terminate();
244
+ this.connectionManager.remove(client.socket);
245
+ }
246
+ }
247
+ }
248
+ };
249
+
250
+ // src/server/messageRouter.ts
251
+ var MessageRouter = class {
252
+ constructor(connectionManager, deviceRegistry) {
253
+ this.connectionManager = connectionManager;
254
+ this.deviceRegistry = deviceRegistry;
255
+ }
256
+ connectionManager;
257
+ deviceRegistry;
258
+ sendToSocket(socket, message) {
259
+ if (socket.readyState === socket.OPEN) {
260
+ socket.send(JSON.stringify(message));
261
+ }
262
+ }
263
+ sendToBrowser(connectionId, message) {
264
+ const client = this.connectionManager.getByConnectionId(connectionId);
265
+ if (client?.role === DevToolsRole.BROWSER) {
266
+ this.sendToSocket(client.socket, message);
267
+ }
268
+ }
269
+ sendToMobile(deviceId, message) {
270
+ const client = this.connectionManager.getByDeviceId(deviceId);
271
+ if (client?.role === DevToolsRole.MOBILE) {
272
+ this.sendToSocket(client.socket, message);
273
+ }
274
+ }
275
+ broadcastToBrowsers(message) {
276
+ for (const client of this.connectionManager.getByRole(DevToolsRole.BROWSER)) {
277
+ this.sendToSocket(client.socket, message);
278
+ }
279
+ logger.broadcastSent("browsers");
280
+ }
281
+ broadcastToMobiles(message) {
282
+ for (const client of this.connectionManager.getByRole(DevToolsRole.MOBILE)) {
283
+ this.sendToSocket(client.socket, message);
284
+ }
285
+ logger.broadcastSent("mobiles");
286
+ }
287
+ broadcastToAll(message) {
288
+ for (const client of this.connectionManager.getAll()) {
289
+ this.sendToSocket(client.socket, message);
290
+ }
291
+ logger.broadcastSent("all");
292
+ }
293
+ broadcast(message, target = "all") {
294
+ const broadcastMessage = createMessage({
295
+ type: MessageType.BROADCAST,
296
+ payload: message
297
+ });
298
+ if (target === "browsers") {
299
+ this.broadcastToBrowsers(broadcastMessage);
300
+ return;
301
+ }
302
+ if (target === "mobiles") {
303
+ this.broadcastToMobiles(broadcastMessage);
304
+ return;
305
+ }
306
+ this.broadcastToAll(broadcastMessage);
307
+ }
308
+ broadcastDeviceStatus() {
309
+ const message = createMessage({
310
+ type: MessageType.DEVICE_STATUS,
311
+ payload: this.deviceRegistry.snapshot()
312
+ });
313
+ this.broadcastToAll(message);
314
+ }
315
+ };
316
+
317
+ // src/server/pendingRefreshStore.ts
318
+ var PendingRefreshStore = class {
319
+ pendingByDevice = /* @__PURE__ */ new Map();
320
+ create(input) {
321
+ this.pendingByDevice.set(input.deviceId, input);
322
+ }
323
+ get(deviceId) {
324
+ return this.pendingByDevice.get(deviceId);
325
+ }
326
+ has(deviceId) {
327
+ return this.pendingByDevice.has(deviceId);
328
+ }
329
+ remove(deviceId) {
330
+ this.pendingByDevice.delete(deviceId);
331
+ }
332
+ getExpired(now = Date.now()) {
333
+ return [...this.pendingByDevice.values()].filter(
334
+ (pending) => now - pending.startedAt >= REFRESH_TIMEOUT_MS
335
+ );
336
+ }
337
+ };
338
+
339
+ // src/server/refreshCoordinator.ts
340
+ var RefreshCoordinator = class {
341
+ constructor(connectionManager, router, pending, snapshots, snapshotFiles) {
342
+ this.connectionManager = connectionManager;
343
+ this.router = router;
344
+ this.pending = pending;
345
+ this.snapshots = snapshots;
346
+ this.snapshotFiles = snapshotFiles;
347
+ }
348
+ connectionManager;
349
+ router;
350
+ pending;
351
+ snapshots;
352
+ snapshotFiles;
353
+ handleRefreshRequest(browserSocket, message) {
354
+ const browser = this.connectionManager.getBySocket(browserSocket);
355
+ if (!browser || browser.role !== DevToolsRole.BROWSER) {
356
+ return;
357
+ }
358
+ if (message.refreshType !== "snapshot") {
359
+ this.sendRefreshError(browser.connectionId, {
360
+ deviceId: message.deviceId,
361
+ code: "INVALID_REQUEST",
362
+ message: `Unsupported refresh type: ${message.refreshType}`
363
+ });
364
+ return;
365
+ }
366
+ const mobile = this.connectionManager.getByDeviceId(message.deviceId);
367
+ if (!mobile) {
368
+ this.sendRefreshError(browser.connectionId, {
369
+ deviceId: message.deviceId,
370
+ code: "DEVICE_OFFLINE",
371
+ message: `Device ${message.deviceId} is not connected`
372
+ });
373
+ return;
374
+ }
375
+ if (this.pending.has(message.deviceId)) {
376
+ this.sendRefreshError(browser.connectionId, {
377
+ deviceId: message.deviceId,
378
+ code: "REFRESH_IN_PROGRESS",
379
+ message: "A refresh is already in progress for this device"
380
+ });
381
+ return;
382
+ }
383
+ this.pending.create({
384
+ deviceId: message.deviceId,
385
+ initiator: "browser",
386
+ browserConnectionId: browser.connectionId,
387
+ refreshType: message.refreshType,
388
+ startedAt: Date.now()
389
+ });
390
+ logger.refreshStarted(message.deviceId);
391
+ this.sendRefreshStatus(browser.connectionId, message.deviceId, "requested");
392
+ this.sendRefreshStatus(browser.connectionId, message.deviceId, "exporting");
393
+ this.requestMobileUpload(message.deviceId, message.refreshType);
394
+ }
395
+ handleMobileExportRequest(mobileSocket, message) {
396
+ const mobile = this.connectionManager.getBySocket(mobileSocket);
397
+ if (!mobile || mobile.role !== DevToolsRole.MOBILE || !mobile.deviceId) {
398
+ return;
399
+ }
400
+ if (message.refreshType !== "snapshot") {
401
+ this.sendExportSnapshotError(mobileSocket, {
402
+ code: "INVALID_REQUEST",
403
+ message: `Unsupported refresh type: ${message.refreshType}`
404
+ });
405
+ return;
406
+ }
407
+ if (this.pending.has(mobile.deviceId)) {
408
+ this.sendExportSnapshotError(mobileSocket, {
409
+ code: "REFRESH_IN_PROGRESS",
410
+ message: "An export is already in progress for this device"
411
+ });
412
+ return;
413
+ }
414
+ this.pending.create({
415
+ deviceId: mobile.deviceId,
416
+ initiator: "mobile",
417
+ notifyAllBrowsers: true,
418
+ refreshType: message.refreshType,
419
+ startedAt: Date.now()
420
+ });
421
+ logger.refreshStarted(mobile.deviceId);
422
+ this.requestMobileUpload(mobile.deviceId, message.refreshType);
423
+ }
424
+ async handleSnapshotUpload(deviceId, body, metadata) {
425
+ const pending = this.pending.get(deviceId);
426
+ if (!pending) {
427
+ return { ok: false, code: "SNAPSHOT_NOT_FOUND" };
428
+ }
429
+ if (pending.browserConnectionId) {
430
+ this.sendRefreshStatus(pending.browserConnectionId, deviceId, "uploading");
431
+ }
432
+ const stored = this.snapshots.set(deviceId, body, {
433
+ kind: metadata?.kind ?? "sqlite",
434
+ mimeType: metadata?.mimeType ?? "application/x-sqlite3",
435
+ databaseName: metadata?.databaseName
436
+ });
437
+ const mobile = this.connectionManager.getByDeviceId(deviceId);
438
+ await this.snapshotFiles?.persistDeviceSnapshot({
439
+ deviceId,
440
+ bytes: body,
441
+ kind: stored.kind,
442
+ mimeType: stored.mimeType,
443
+ databaseName: stored.databaseName,
444
+ bundleId: mobile?.metadata?.bundleId
445
+ });
446
+ logger.refreshUploaded(deviceId, body.byteLength);
447
+ const readyMessage = createMessage({
448
+ type: MessageType.SNAPSHOT_READY,
449
+ deviceId,
450
+ size: body.byteLength,
451
+ exportedAt: stored.exportedAt,
452
+ kind: stored.kind,
453
+ mimeType: stored.mimeType,
454
+ ...stored.databaseName ? { databaseName: stored.databaseName } : {}
455
+ });
456
+ if (pending.notifyAllBrowsers) {
457
+ this.router.broadcastToBrowsers(readyMessage);
458
+ } else if (pending.browserConnectionId) {
459
+ this.sendRefreshStatus(pending.browserConnectionId, deviceId, "ready");
460
+ this.router.sendToBrowser(pending.browserConnectionId, readyMessage);
461
+ }
462
+ this.pending.remove(deviceId);
463
+ return { ok: true, stored: readyMessage };
464
+ }
465
+ getSnapshot(deviceId) {
466
+ return this.snapshots.getBytes(deviceId);
467
+ }
468
+ async getSnapshotAsync(deviceId) {
469
+ if (this.snapshotFiles) {
470
+ const fromDisk = await this.snapshotFiles.readDeviceSnapshot(deviceId);
471
+ if (fromDisk) {
472
+ return fromDisk;
473
+ }
474
+ }
475
+ return this.snapshots.getBytes(deviceId);
476
+ }
477
+ checkTimeouts() {
478
+ for (const pending of this.pending.getExpired()) {
479
+ if (pending.initiator === "mobile") {
480
+ this.failMobileExport(
481
+ pending.deviceId,
482
+ "TIMEOUT",
483
+ "Export timed out waiting for device upload"
484
+ );
485
+ continue;
486
+ }
487
+ if (pending.browserConnectionId) {
488
+ this.failRefresh(
489
+ pending.deviceId,
490
+ pending.browserConnectionId,
491
+ "TIMEOUT",
492
+ "Refresh timed out waiting for device upload"
493
+ );
494
+ } else {
495
+ this.pending.remove(pending.deviceId);
496
+ }
497
+ }
498
+ }
499
+ requestMobileUpload(deviceId, refreshType) {
500
+ this.router.sendToMobile(
501
+ deviceId,
502
+ createMessage({
503
+ type: MessageType.SNAPSHOT_UPLOAD_REQUESTED,
504
+ deviceId,
505
+ refreshType
506
+ })
507
+ );
508
+ }
509
+ failRefresh(deviceId, browserConnectionId, code, message) {
510
+ logger.refreshFailed(deviceId, code, message);
511
+ this.sendRefreshError(browserConnectionId, {
512
+ deviceId,
513
+ code,
514
+ message
515
+ });
516
+ this.pending.remove(deviceId);
517
+ }
518
+ failMobileExport(deviceId, code, message) {
519
+ logger.refreshFailed(deviceId, code, message);
520
+ const mobile = this.connectionManager.getByDeviceId(deviceId);
521
+ if (mobile) {
522
+ this.sendExportSnapshotError(mobile.socket, { code, message });
523
+ }
524
+ const refreshError = createMessage({
525
+ type: MessageType.REFRESH_ERROR,
526
+ deviceId,
527
+ code,
528
+ message
529
+ });
530
+ this.router.broadcastToBrowsers(refreshError);
531
+ this.pending.remove(deviceId);
532
+ }
533
+ sendRefreshStatus(browserConnectionId, deviceId, state) {
534
+ const statusMessage = createMessage({
535
+ type: MessageType.REFRESH_STATUS,
536
+ deviceId,
537
+ state
538
+ });
539
+ this.router.sendToBrowser(browserConnectionId, statusMessage);
540
+ }
541
+ sendRefreshError(browserConnectionId, input) {
542
+ const message = createMessage({
543
+ type: MessageType.REFRESH_ERROR,
544
+ deviceId: input.deviceId,
545
+ code: input.code,
546
+ message: input.message
547
+ });
548
+ this.router.sendToBrowser(browserConnectionId, message);
549
+ logger.refreshFailed(input.deviceId, input.code, input.message);
550
+ }
551
+ sendExportSnapshotError(mobileSocket, input) {
552
+ const message = createMessage({
553
+ type: MessageType.EXPORT_SNAPSHOT_ERROR,
554
+ code: input.code,
555
+ message: input.message
556
+ });
557
+ this.router.sendToSocket(mobileSocket, message);
558
+ }
559
+ };
560
+
561
+ // src/server/snapshotStore.ts
562
+ var SnapshotStore = class {
563
+ snapshots = /* @__PURE__ */ new Map();
564
+ set(deviceId, bytes, metadata) {
565
+ const stored = {
566
+ deviceId,
567
+ bytes,
568
+ kind: metadata.kind,
569
+ mimeType: metadata.mimeType,
570
+ databaseName: metadata.databaseName,
571
+ exportedAt: Date.now()
572
+ };
573
+ this.snapshots.set(deviceId, stored);
574
+ return stored;
575
+ }
576
+ get(deviceId) {
577
+ return this.snapshots.get(deviceId);
578
+ }
579
+ getBytes(deviceId) {
580
+ return this.snapshots.get(deviceId)?.bytes;
581
+ }
582
+ };
583
+ function shouldServeSpa(request) {
584
+ if (request.method !== "GET" && request.method !== "HEAD") {
585
+ return false;
586
+ }
587
+ if (request.path.startsWith("/api")) {
588
+ return false;
589
+ }
590
+ if (request.path === "/health") {
591
+ return false;
592
+ }
593
+ return true;
594
+ }
595
+ function attachWebUi(app, webDistPath) {
596
+ const indexPath = path4.join(webDistPath, "index.html");
597
+ if (!existsSync(indexPath)) {
598
+ return;
599
+ }
600
+ app.use(
601
+ express2.static(webDistPath, {
602
+ index: false,
603
+ setHeaders(response, filePath) {
604
+ if (filePath.endsWith(".wasm")) {
605
+ response.setHeader("Content-Type", "application/wasm");
606
+ }
607
+ }
608
+ })
609
+ );
610
+ app.get("*", (request, response, next) => {
611
+ if (!shouldServeSpa(request)) {
612
+ next();
613
+ return;
614
+ }
615
+ response.sendFile(indexPath);
616
+ });
617
+ }
618
+ function attachWebSocket(httpServer, connectionManager, router, heartbeat, refreshCoordinator, writeCoordinator, options) {
619
+ const wss = new WebSocketServer({ server: httpServer, path: DEVTOOLS_WS_PATH });
620
+ const handleDisconnect = (socket) => {
621
+ const client = connectionManager.remove(socket);
622
+ if (!client) {
623
+ return;
624
+ }
625
+ if (client.role === DevToolsRole.MOBILE) {
626
+ if (client.deviceId) {
627
+ writeCoordinator.rollbackSessionsForDevice(client.deviceId);
628
+ }
629
+ logger.mobileDisconnected(client.deviceId);
630
+ } else {
631
+ writeCoordinator.rollbackSessionsOnBrowserDisconnect(client.connectionId);
632
+ logger.browserDisconnected(client.connectionId);
633
+ }
634
+ router.broadcastDeviceStatus();
635
+ };
636
+ wss.on("connection", (socket) => {
637
+ socket.on("message", (raw) => {
638
+ let parsed;
639
+ try {
640
+ parsed = JSON.parse(String(raw));
641
+ } catch {
642
+ return;
643
+ }
644
+ if (isRegisterMessage(parsed)) {
645
+ const existing = connectionManager.getBySocket(socket);
646
+ if (existing) {
647
+ return;
648
+ }
649
+ const client = connectionManager.add({
650
+ socket,
651
+ role: parsed.role,
652
+ deviceId: parsed.deviceId,
653
+ metadata: parsed.metadata
654
+ });
655
+ if (client.role === DevToolsRole.MOBILE) {
656
+ logger.mobileConnected(client.deviceId);
657
+ if (client.deviceId && options?.snapshotFileStore) {
658
+ void options.snapshotFileStore.reconcileOnMobileConnect({
659
+ deviceId: client.deviceId,
660
+ bundleId: client.metadata?.bundleId
661
+ }).finally(() => {
662
+ router.broadcastDeviceStatus();
663
+ });
664
+ return;
665
+ }
666
+ } else {
667
+ logger.browserConnected(client.connectionId);
668
+ }
669
+ router.broadcastDeviceStatus();
670
+ return;
671
+ }
672
+ if (isPongMessage(parsed)) {
673
+ connectionManager.updateLastPong(socket);
674
+ return;
675
+ }
676
+ if (isRefreshRequestMessage(parsed)) {
677
+ refreshCoordinator.handleRefreshRequest(socket, parsed);
678
+ return;
679
+ }
680
+ if (isExportSnapshotRequestMessage(parsed)) {
681
+ refreshCoordinator.handleMobileExportRequest(socket, parsed);
682
+ return;
683
+ }
684
+ if (isBeginTransactionRequestMessage(parsed)) {
685
+ writeCoordinator.handleBeginTransactionRequest(socket, parsed);
686
+ return;
687
+ }
688
+ if (isCommitTransactionRequestMessage(parsed)) {
689
+ writeCoordinator.handleCommitTransactionRequest(socket, parsed);
690
+ return;
691
+ }
692
+ if (isRollbackTransactionRequestMessage(parsed)) {
693
+ writeCoordinator.handleRollbackTransactionRequest(socket, parsed);
694
+ return;
695
+ }
696
+ if (isWriteRequestMessage(parsed)) {
697
+ writeCoordinator.handleWriteRequest(socket, parsed);
698
+ return;
699
+ }
700
+ if (isTransactionAckMessage(parsed)) {
701
+ writeCoordinator.handleTransactionAck(parsed);
702
+ return;
703
+ }
704
+ if (isWriteAckMessage(parsed)) {
705
+ writeCoordinator.handleWriteAck(parsed);
706
+ }
707
+ });
708
+ socket.on("close", () => {
709
+ handleDisconnect(socket);
710
+ });
711
+ socket.on("error", () => {
712
+ handleDisconnect(socket);
713
+ });
714
+ });
715
+ heartbeat.start();
716
+ return { wss };
717
+ }
718
+ var DEFAULT_DATA_DIR = ".devtools";
719
+ var DEFAULT_DATABASE_RELATIVE_PATH = "databases/active.db";
720
+ var DEFAULT_CONFIG = {
721
+ deviceSync: {
722
+ promoteLatestToActive: false
723
+ }
724
+ };
725
+ function resolveDataDir(explicitDir) {
726
+ const fromEnv = process.env.DATABASE_DEVTOOLS_DATA_DIR?.trim();
727
+ if (explicitDir?.trim()) {
728
+ return path4.resolve(explicitDir.trim());
729
+ }
730
+ if (fromEnv) {
731
+ return path4.resolve(fromEnv);
732
+ }
733
+ return path4.resolve(process.cwd(), DEFAULT_DATA_DIR);
734
+ }
735
+ function isPersistenceEnabled() {
736
+ const flag = process.env.DATABASE_DEVTOOLS_SNAPSHOT_PERSIST?.trim().toLowerCase();
737
+ if (flag === "0" || flag === "false" || flag === "off") {
738
+ return false;
739
+ }
740
+ return true;
741
+ }
742
+ function sanitizePathSegment(value) {
743
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "_");
744
+ }
745
+ function resolveProjectPaths(dataDir) {
746
+ const databasesDir = path4.join(dataDir, "databases");
747
+ const devicesDir = path4.join(databasesDir, "devices");
748
+ const configPath = path4.join(dataDir, "devtools.json");
749
+ const manifestPath = path4.join(dataDir, "manifest.json");
750
+ return {
751
+ dataDir,
752
+ databasesDir,
753
+ devicesDir,
754
+ configPath,
755
+ manifestPath
756
+ };
757
+ }
758
+ async function ensureProjectDirectories(dataDir) {
759
+ const paths = resolveProjectPaths(dataDir);
760
+ await mkdir(paths.devicesDir, { recursive: true });
761
+ }
762
+ async function writeDefaultProjectConfig(dataDir) {
763
+ const { configPath } = resolveProjectPaths(dataDir);
764
+ try {
765
+ await readFile(configPath, "utf8");
766
+ } catch {
767
+ await mkdir(dataDir, { recursive: true });
768
+ await writeFile(configPath, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}
769
+ `, "utf8");
770
+ }
771
+ }
772
+ function deviceDatabaseFileName(deviceId) {
773
+ return `Database-${sanitizePathSegment(deviceId)}.db`;
774
+ }
775
+ var DEVICE_EXPORT_FILE_NAME = "database.db";
776
+ function resolveDeviceStorageKey(input) {
777
+ const bundleId = input.bundleId?.trim();
778
+ if (bundleId) {
779
+ return sanitizePathSegment(bundleId);
780
+ }
781
+ return sanitizePathSegment(input.deviceId);
782
+ }
783
+ function deviceStorageManifestId(storageKey) {
784
+ return `device:${storageKey}`;
785
+ }
786
+ function deviceStorageRelativePath(storageKey) {
787
+ return path4.join("databases", "devices", storageKey, DEVICE_EXPORT_FILE_NAME).replace(/\\/g, "/");
788
+ }
789
+ function deviceDatabaseRelativePath(deviceId) {
790
+ const segment = sanitizePathSegment(deviceId);
791
+ return path4.join("databases", "devices", segment, deviceDatabaseFileName(deviceId)).replace(/\\/g, "/");
792
+ }
793
+ function deviceLatestRelativePath(deviceId) {
794
+ return path4.join("databases", "devices", sanitizePathSegment(deviceId), "latest.db").replace(/\\/g, "/");
795
+ }
796
+ async function readManifest(dataDir) {
797
+ const { manifestPath } = resolveProjectPaths(dataDir);
798
+ try {
799
+ const raw = await readFile(manifestPath, "utf8");
800
+ const parsed = JSON.parse(raw);
801
+ if (parsed.version !== 1 || !Array.isArray(parsed.entries)) {
802
+ return { version: 1, entries: [] };
803
+ }
804
+ return parsed;
805
+ } catch {
806
+ return { version: 1, entries: [] };
807
+ }
808
+ }
809
+ async function writeManifest(dataDir, manifest) {
810
+ const { manifestPath } = resolveProjectPaths(dataDir);
811
+ await mkdir(dataDir, { recursive: true });
812
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
813
+ `, "utf8");
814
+ }
815
+ function upsertManifestEntry(manifest, entry) {
816
+ const entries = manifest.entries.filter((item) => item.id !== entry.id);
817
+ entries.push(entry);
818
+ entries.sort((left, right) => right.updatedAt - left.updatedAt);
819
+ return {
820
+ version: manifest.version,
821
+ entries
822
+ };
823
+ }
824
+
825
+ // src/server/deviceExportStorage.ts
826
+ function deriveStorageKeyFromEntry(entry) {
827
+ if (entry.storageKey) {
828
+ return entry.storageKey;
829
+ }
830
+ if (entry.bundleId) {
831
+ return sanitizePathSegment(entry.bundleId);
832
+ }
833
+ const match = entry.relativePath.match(/databases\/devices\/([^/]+)\//);
834
+ if (match?.[1]) {
835
+ return match[1];
836
+ }
837
+ if (entry.deviceId) {
838
+ return sanitizePathSegment(entry.deviceId);
839
+ }
840
+ return entry.id;
841
+ }
842
+ function findManifestEntryByDeviceId(entries, deviceId) {
843
+ return entries.find((entry) => entry.source === "device" && entry.deviceId === deviceId);
844
+ }
845
+ function selectDeviceExportEntriesToPrune(entries, keep) {
846
+ const keepManifestId = deviceStorageManifestId(keep.keepStorageKey);
847
+ return entries.filter((entry) => {
848
+ if (entry.source !== "device") {
849
+ return false;
850
+ }
851
+ if (entry.id === keepManifestId) {
852
+ return false;
853
+ }
854
+ const entryStorageKey = deriveStorageKeyFromEntry(entry);
855
+ if (entryStorageKey === keep.keepStorageKey) {
856
+ return true;
857
+ }
858
+ if (keep.bundleId && entry.bundleId === keep.bundleId) {
859
+ return true;
860
+ }
861
+ if (keep.bundleId && keep.databaseName && !entry.bundleId && entry.databaseName === keep.databaseName) {
862
+ return true;
863
+ }
864
+ if (!keep.bundleId && keep.databaseName && entry.databaseName === keep.databaseName && entry.deviceId !== keep.keepDeviceId) {
865
+ return true;
866
+ }
867
+ return false;
868
+ });
869
+ }
870
+ function dedupeListedDeviceExports(entries) {
871
+ const byStorageKey = /* @__PURE__ */ new Map();
872
+ for (const entry of entries) {
873
+ const storageKey = entry.storageKey ? entry.storageKey : entry.bundleId ? sanitizePathSegment(entry.bundleId) : entry.deviceId ? sanitizePathSegment(entry.deviceId) : entry.id;
874
+ const existing = byStorageKey.get(storageKey);
875
+ if (!existing || entry.updatedAt > existing.updatedAt) {
876
+ byStorageKey.set(storageKey, entry);
877
+ }
878
+ }
879
+ return [...byStorageKey.values()].sort((left, right) => right.updatedAt - left.updatedAt);
880
+ }
881
+ function reconcileLegacyManifestEntries(input) {
882
+ const deviceEntries = input.entries.filter((entry) => entry.source === "device");
883
+ const otherEntries = input.entries.filter((entry) => entry.source !== "device");
884
+ const groups = /* @__PURE__ */ new Map();
885
+ for (const entry of deviceEntries) {
886
+ const groupKey = entry.bundleId ? `bundle:${entry.bundleId}` : entry.databaseName ? `db:${entry.databaseName}` : `device:${deriveStorageKeyFromEntry(entry)}`;
887
+ const group = groups.get(groupKey) ?? [];
888
+ group.push(entry);
889
+ groups.set(groupKey, group);
890
+ }
891
+ const keptDeviceEntries = [];
892
+ for (const group of groups.values()) {
893
+ if (group.length === 1) {
894
+ keptDeviceEntries.push(group[0]);
895
+ continue;
896
+ }
897
+ const preferred = input.preferredDeviceId ? group.find((entry) => entry.deviceId === input.preferredDeviceId) : void 0;
898
+ const newest = [...group].sort((left, right) => right.updatedAt - left.updatedAt)[0];
899
+ keptDeviceEntries.push(preferred ?? newest);
900
+ }
901
+ return [...otherEntries, ...keptDeviceEntries].sort((left, right) => right.updatedAt - left.updatedAt);
902
+ }
903
+ function removedManifestEntries(before, after) {
904
+ const afterIds = new Set(after.map((entry) => entry.id));
905
+ return before.filter((entry) => !afterIds.has(entry.id));
906
+ }
907
+ var NORMALIZED_CONFIG = {
908
+ deviceSync: {
909
+ promoteLatestToActive: false
910
+ }
911
+ };
912
+ function stripLegacyActiveManifest(manifest) {
913
+ const entries = manifest.entries.filter(
914
+ (entry) => entry.source !== "active" && entry.id !== "active" && !entry.id.startsWith("import:")
915
+ );
916
+ return {
917
+ version: 1,
918
+ entries
919
+ };
920
+ }
921
+ function normalizeProjectConfig(config) {
922
+ return {
923
+ ...NORMALIZED_CONFIG,
924
+ deviceSync: {
925
+ ...NORMALIZED_CONFIG.deviceSync,
926
+ promoteLatestToActive: false
927
+ }
928
+ };
929
+ }
930
+ async function migrateLegacyActiveDatabase(dataDir) {
931
+ const { configPath } = resolveProjectPaths(dataDir);
932
+ const manifest = await readManifest(dataDir);
933
+ const stripped = stripLegacyActiveManifest(manifest);
934
+ const activeDbPath = path4.join(dataDir, DEFAULT_DATABASE_RELATIVE_PATH);
935
+ let changed = stripped.entries.length !== manifest.entries.length || Boolean(manifest.activeDatabase);
936
+ if (changed) {
937
+ await writeManifest(dataDir, stripped);
938
+ }
939
+ try {
940
+ await stat(activeDbPath);
941
+ const hasDeviceExports = stripped.entries.some((entry) => entry.source === "device");
942
+ if (hasDeviceExports || manifest.entries.some((entry) => entry.source === "active")) {
943
+ await rm(activeDbPath, { force: true });
944
+ changed = true;
945
+ }
946
+ } catch {
947
+ }
948
+ try {
949
+ const raw = await readFile(configPath, "utf8");
950
+ const parsed = JSON.parse(raw);
951
+ if (parsed.defaultDatabase || parsed.deviceSync?.promoteLatestToActive) {
952
+ await writeFile(
953
+ configPath,
954
+ `${JSON.stringify(normalizeProjectConfig(parsed), null, 2)}
955
+ `,
956
+ "utf8"
957
+ );
958
+ changed = true;
959
+ }
960
+ } catch {
961
+ }
962
+ const importsDir = path4.join(dataDir, "databases", "imports");
963
+ try {
964
+ await stat(importsDir);
965
+ await rm(importsDir, { recursive: true, force: true });
966
+ changed = true;
967
+ } catch {
968
+ }
969
+ return changed;
970
+ }
971
+
972
+ // src/server/snapshotFileStore.ts
973
+ var SnapshotFileStore = class {
974
+ dataDir;
975
+ enabled;
976
+ constructor(options) {
977
+ this.dataDir = resolveDataDir(options?.dataDir);
978
+ this.enabled = options?.enabled ?? isPersistenceEnabled();
979
+ }
980
+ getDataDir() {
981
+ return this.dataDir;
982
+ }
983
+ isEnabled() {
984
+ return this.enabled;
985
+ }
986
+ async persistDeviceSnapshot(input) {
987
+ if (!this.enabled) {
988
+ return null;
989
+ }
990
+ await writeDefaultProjectConfig(this.dataDir);
991
+ await ensureProjectDirectories(this.dataDir);
992
+ const storageKey = resolveDeviceStorageKey({
993
+ bundleId: input.bundleId,
994
+ deviceId: input.deviceId
995
+ });
996
+ const deviceRelativePath = deviceStorageRelativePath(storageKey);
997
+ const deviceAbsolutePath = path4.join(this.dataDir, deviceRelativePath);
998
+ const exportedAt = Date.now();
999
+ await this.pruneDeviceExports({
1000
+ keepStorageKey: storageKey,
1001
+ keepDeviceId: input.deviceId,
1002
+ bundleId: input.bundleId,
1003
+ databaseName: input.databaseName
1004
+ });
1005
+ await mkdir(path4.dirname(deviceAbsolutePath), { recursive: true });
1006
+ await writeFile(deviceAbsolutePath, input.bytes);
1007
+ const manifest = await readManifest(this.dataDir);
1008
+ const deviceEntry = {
1009
+ id: deviceStorageManifestId(storageKey),
1010
+ label: input.databaseName ?? `Device ${input.deviceId}`,
1011
+ relativePath: deviceRelativePath.replace(/\\/g, "/"),
1012
+ source: "device",
1013
+ deviceId: input.deviceId,
1014
+ bundleId: input.bundleId,
1015
+ storageKey,
1016
+ kind: input.kind,
1017
+ mimeType: input.mimeType,
1018
+ databaseName: input.databaseName,
1019
+ size: input.bytes.byteLength,
1020
+ updatedAt: exportedAt
1021
+ };
1022
+ const nextManifest = upsertManifestEntry(manifest, deviceEntry);
1023
+ await writeManifest(this.dataDir, nextManifest);
1024
+ return {
1025
+ deviceRelativePath: deviceRelativePath.replace(/\\/g, "/"),
1026
+ exportedAt,
1027
+ size: input.bytes.byteLength
1028
+ };
1029
+ }
1030
+ async reconcileOnMobileConnect(input) {
1031
+ if (!this.enabled) {
1032
+ return;
1033
+ }
1034
+ await this.reconcileLegacyExports(input.deviceId);
1035
+ const storageKey = resolveDeviceStorageKey({
1036
+ bundleId: input.bundleId,
1037
+ deviceId: input.deviceId
1038
+ });
1039
+ const manifest = await readManifest(this.dataDir);
1040
+ const canonicalEntry = manifest.entries.find(
1041
+ (entry) => entry.source === "device" && entry.storageKey === storageKey
1042
+ );
1043
+ if (canonicalEntry && canonicalEntry.deviceId !== input.deviceId) {
1044
+ const updated = upsertManifestEntry(manifest, {
1045
+ ...canonicalEntry,
1046
+ deviceId: input.deviceId,
1047
+ bundleId: input.bundleId ?? canonicalEntry.bundleId,
1048
+ updatedAt: Date.now()
1049
+ });
1050
+ await writeManifest(this.dataDir, updated);
1051
+ }
1052
+ await this.pruneDeviceExports({
1053
+ keepStorageKey: storageKey,
1054
+ keepDeviceId: input.deviceId,
1055
+ bundleId: input.bundleId,
1056
+ databaseName: canonicalEntry?.databaseName
1057
+ });
1058
+ }
1059
+ async readDeviceSnapshot(deviceId) {
1060
+ return this.readDeviceDatabase(deviceId);
1061
+ }
1062
+ async readDeviceDatabase(deviceId) {
1063
+ if (!this.enabled) {
1064
+ return void 0;
1065
+ }
1066
+ const resolved = await this.resolveDeviceDatabaseFile(deviceId);
1067
+ if (!resolved) {
1068
+ return void 0;
1069
+ }
1070
+ try {
1071
+ return await readFile(resolved.absolutePath);
1072
+ } catch {
1073
+ return void 0;
1074
+ }
1075
+ }
1076
+ async getDeviceDatabaseMeta(deviceId) {
1077
+ if (!this.enabled) {
1078
+ return { exists: false, deviceId };
1079
+ }
1080
+ const resolved = await this.resolveDeviceDatabaseFile(deviceId);
1081
+ if (!resolved) {
1082
+ return { exists: false, deviceId };
1083
+ }
1084
+ try {
1085
+ const fileStat = await stat(resolved.absolutePath);
1086
+ const manifest = await readManifest(this.dataDir);
1087
+ const entry = manifest.entries.find(
1088
+ (item) => item.deviceId === deviceId && item.source === "device"
1089
+ );
1090
+ return {
1091
+ exists: true,
1092
+ relativePath: resolved.relativePath,
1093
+ absolutePath: resolved.absolutePath,
1094
+ kind: entry?.kind ?? "sqlite",
1095
+ mimeType: entry?.mimeType ?? SQLITE_SNAPSHOT_MIME_TYPE,
1096
+ databaseName: entry?.databaseName ?? path4.basename(resolved.absolutePath),
1097
+ size: fileStat.size,
1098
+ updatedAt: entry?.updatedAt ?? fileStat.mtimeMs,
1099
+ source: "device",
1100
+ deviceId
1101
+ };
1102
+ } catch {
1103
+ return { exists: false, deviceId, relativePath: resolved.relativePath };
1104
+ }
1105
+ }
1106
+ async listDeviceExports() {
1107
+ await this.reconcileLegacyExports();
1108
+ const databases = await this.listDatabases();
1109
+ const deviceExports = databases.filter((entry) => entry.source === "device" && entry.deviceId);
1110
+ return dedupeListedDeviceExports(deviceExports);
1111
+ }
1112
+ async reconcileLegacyExports(preferredDeviceId) {
1113
+ if (!this.enabled) {
1114
+ return;
1115
+ }
1116
+ await migrateLegacyActiveDatabase(this.dataDir);
1117
+ const manifest = await readManifest(this.dataDir);
1118
+ const nextEntries = reconcileLegacyManifestEntries({
1119
+ entries: manifest.entries,
1120
+ preferredDeviceId
1121
+ });
1122
+ if (nextEntries.length === manifest.entries.length) {
1123
+ const sameOrder = nextEntries.every((entry, index) => entry.id === manifest.entries[index]?.id);
1124
+ if (sameOrder) {
1125
+ return;
1126
+ }
1127
+ }
1128
+ const removed = removedManifestEntries(manifest.entries, nextEntries);
1129
+ for (const entry of removed) {
1130
+ await this.deleteEntryFiles(entry);
1131
+ }
1132
+ await writeManifest(this.dataDir, {
1133
+ ...manifest,
1134
+ entries: nextEntries
1135
+ });
1136
+ }
1137
+ async pruneDeviceExports(input) {
1138
+ const manifest = await readManifest(this.dataDir);
1139
+ const removed = selectDeviceExportEntriesToPrune(manifest.entries, input);
1140
+ if (removed.length === 0) {
1141
+ return;
1142
+ }
1143
+ for (const entry of removed) {
1144
+ await this.deleteEntryFiles(entry);
1145
+ }
1146
+ const removedIds = new Set(removed.map((entry) => entry.id));
1147
+ await writeManifest(this.dataDir, {
1148
+ ...manifest,
1149
+ entries: manifest.entries.filter((entry) => !removedIds.has(entry.id))
1150
+ });
1151
+ }
1152
+ async deleteEntryFiles(entry) {
1153
+ const absolutePath = path4.join(this.dataDir, entry.relativePath);
1154
+ try {
1155
+ await rm(absolutePath, { force: true });
1156
+ } catch {
1157
+ }
1158
+ try {
1159
+ await rm(path4.dirname(absolutePath), { recursive: true, force: true });
1160
+ } catch {
1161
+ }
1162
+ }
1163
+ manifestEntryToListed(entry, fileStat) {
1164
+ return {
1165
+ id: entry.id,
1166
+ label: entry.label,
1167
+ relativePath: entry.relativePath,
1168
+ source: entry.source,
1169
+ deviceId: entry.deviceId,
1170
+ bundleId: entry.bundleId,
1171
+ storageKey: entry.storageKey,
1172
+ databaseName: entry.databaseName,
1173
+ size: fileStat.size,
1174
+ updatedAt: entry.updatedAt
1175
+ };
1176
+ }
1177
+ async resolveDeviceDatabaseFile(deviceId) {
1178
+ const manifest = await readManifest(this.dataDir);
1179
+ const manifestEntry = findManifestEntryByDeviceId(manifest.entries, deviceId);
1180
+ if (manifestEntry) {
1181
+ const absolutePath2 = path4.join(this.dataDir, manifestEntry.relativePath);
1182
+ try {
1183
+ await stat(absolutePath2);
1184
+ return { relativePath: manifestEntry.relativePath, absolutePath: absolutePath2 };
1185
+ } catch {
1186
+ }
1187
+ }
1188
+ const candidates = [
1189
+ deviceDatabaseRelativePath(deviceId),
1190
+ deviceLatestRelativePath(deviceId)
1191
+ ];
1192
+ for (const relativePath of candidates) {
1193
+ const absolutePath2 = path4.join(this.dataDir, relativePath);
1194
+ try {
1195
+ await stat(absolutePath2);
1196
+ return { relativePath, absolutePath: absolutePath2 };
1197
+ } catch {
1198
+ }
1199
+ }
1200
+ if (!manifestEntry) {
1201
+ return void 0;
1202
+ }
1203
+ const absolutePath = path4.join(this.dataDir, manifestEntry.relativePath);
1204
+ try {
1205
+ await stat(absolutePath);
1206
+ return { relativePath: manifestEntry.relativePath, absolutePath };
1207
+ } catch {
1208
+ return void 0;
1209
+ }
1210
+ }
1211
+ /** @deprecated Proxies to the newest device export for backward-compatible API callers. */
1212
+ async readActiveDatabase() {
1213
+ if (!this.enabled) {
1214
+ return void 0;
1215
+ }
1216
+ const meta = await this.getActiveDatabaseMeta();
1217
+ if (!meta.exists || !meta.deviceId) {
1218
+ return void 0;
1219
+ }
1220
+ return this.readDeviceDatabase(meta.deviceId);
1221
+ }
1222
+ /** @deprecated Proxies to the newest device export for backward-compatible API callers. */
1223
+ async getActiveDatabaseMeta() {
1224
+ if (!this.enabled) {
1225
+ return { exists: false };
1226
+ }
1227
+ await migrateLegacyActiveDatabase(this.dataDir);
1228
+ const exports = await this.listDeviceExports();
1229
+ const latest = exports[0];
1230
+ if (!latest?.deviceId) {
1231
+ return { exists: false };
1232
+ }
1233
+ return this.getDeviceDatabaseMeta(latest.deviceId);
1234
+ }
1235
+ async listDatabases() {
1236
+ if (!this.enabled) {
1237
+ return [];
1238
+ }
1239
+ await ensureProjectDirectories(this.dataDir);
1240
+ await this.reconcileLegacyExports();
1241
+ const manifest = await readManifest(this.dataDir);
1242
+ const listed = /* @__PURE__ */ new Map();
1243
+ for (const entry of manifest.entries) {
1244
+ if (entry.source === "active" || entry.id.startsWith("import:")) {
1245
+ continue;
1246
+ }
1247
+ const absolutePath = path4.join(this.dataDir, entry.relativePath);
1248
+ try {
1249
+ const fileStat = await stat(absolutePath);
1250
+ listed.set(entry.id, this.manifestEntryToListed(entry, fileStat));
1251
+ } catch {
1252
+ }
1253
+ }
1254
+ return [...listed.values()].sort((left, right) => right.updatedAt - left.updatedAt);
1255
+ }
1256
+ async readDatabaseByRelativePath(relativePath) {
1257
+ if (!this.enabled) {
1258
+ return void 0;
1259
+ }
1260
+ const normalized = relativePath.replace(/\\/g, "/");
1261
+ const absolutePath = path4.resolve(this.dataDir, normalized);
1262
+ const dataDirResolved = path4.resolve(this.dataDir);
1263
+ if (!absolutePath.startsWith(dataDirResolved)) {
1264
+ return void 0;
1265
+ }
1266
+ if (!normalized.toLowerCase().endsWith(".db")) {
1267
+ return void 0;
1268
+ }
1269
+ try {
1270
+ return await readFile(absolutePath);
1271
+ } catch {
1272
+ return void 0;
1273
+ }
1274
+ }
1275
+ getDeviceDirectoryName(deviceId) {
1276
+ return sanitizePathSegment(deviceId);
1277
+ }
1278
+ };
1279
+
1280
+ // src/server/writeCoordinator.ts
1281
+ var WriteCoordinator = class {
1282
+ constructor(connectionManager, router, sessions) {
1283
+ this.connectionManager = connectionManager;
1284
+ this.router = router;
1285
+ this.sessions = sessions;
1286
+ }
1287
+ connectionManager;
1288
+ router;
1289
+ sessions;
1290
+ handleBeginTransactionRequest(browserSocket, message) {
1291
+ const browser = this.connectionManager.getBySocket(browserSocket);
1292
+ if (!browser || browser.role !== DevToolsRole.BROWSER) {
1293
+ return;
1294
+ }
1295
+ const mobile = this.connectionManager.getByDeviceId(message.deviceId);
1296
+ if (!mobile) {
1297
+ this.sendWriteError(browser.connectionId, {
1298
+ transactionId: message.transactionId,
1299
+ code: "DEVICE_OFFLINE",
1300
+ message: `Device ${message.deviceId} is not connected`
1301
+ });
1302
+ return;
1303
+ }
1304
+ if (this.sessions.getActiveForDevice(message.deviceId)) {
1305
+ this.sendWriteError(browser.connectionId, {
1306
+ transactionId: message.transactionId,
1307
+ code: "TRANSACTION_BUSY",
1308
+ message: "A transaction is already open for this device"
1309
+ });
1310
+ return;
1311
+ }
1312
+ this.sessions.create({
1313
+ transactionId: message.transactionId,
1314
+ deviceId: message.deviceId,
1315
+ browserConnectionId: browser.connectionId
1316
+ });
1317
+ logger.syncStarted(message.transactionId, message.deviceId);
1318
+ this.router.sendToMobile(
1319
+ message.deviceId,
1320
+ createMessage({
1321
+ type: MessageType.BEGIN_TRANSACTION,
1322
+ transactionId: message.transactionId
1323
+ })
1324
+ );
1325
+ }
1326
+ handleCommitTransactionRequest(browserSocket, message) {
1327
+ const browser = this.connectionManager.getBySocket(browserSocket);
1328
+ const session = this.sessions.get(message.transactionId);
1329
+ if (!browser || browser.role !== DevToolsRole.BROWSER || !session) {
1330
+ return;
1331
+ }
1332
+ if (session.browserConnectionId !== browser.connectionId) {
1333
+ return;
1334
+ }
1335
+ if (session.state !== "open") {
1336
+ this.sendWriteError(browser.connectionId, {
1337
+ transactionId: message.transactionId,
1338
+ code: "NO_TRANSACTION",
1339
+ message: "Transaction is not open"
1340
+ });
1341
+ return;
1342
+ }
1343
+ this.sessions.setState(message.transactionId, "committing");
1344
+ this.router.sendToMobile(
1345
+ session.deviceId,
1346
+ createMessage({
1347
+ type: MessageType.COMMIT_TRANSACTION,
1348
+ transactionId: message.transactionId
1349
+ })
1350
+ );
1351
+ }
1352
+ handleRollbackTransactionRequest(browserSocket, message) {
1353
+ const browser = this.connectionManager.getBySocket(browserSocket);
1354
+ const session = this.sessions.get(message.transactionId);
1355
+ if (!browser || browser.role !== DevToolsRole.BROWSER || !session) {
1356
+ return;
1357
+ }
1358
+ if (session.browserConnectionId !== browser.connectionId) {
1359
+ return;
1360
+ }
1361
+ if (session.state !== "open" && session.state !== "committing") {
1362
+ this.sendWriteError(browser.connectionId, {
1363
+ transactionId: message.transactionId,
1364
+ code: "NO_TRANSACTION",
1365
+ message: "Transaction is not open"
1366
+ });
1367
+ return;
1368
+ }
1369
+ this.sessions.setState(message.transactionId, "rolling_back");
1370
+ this.router.sendToMobile(
1371
+ session.deviceId,
1372
+ createMessage({
1373
+ type: MessageType.ROLLBACK_TRANSACTION,
1374
+ transactionId: message.transactionId
1375
+ })
1376
+ );
1377
+ }
1378
+ handleWriteRequest(browserSocket, message) {
1379
+ const browser = this.connectionManager.getBySocket(browserSocket);
1380
+ const session = this.sessions.get(message.transactionId);
1381
+ if (!browser || browser.role !== DevToolsRole.BROWSER || !session) {
1382
+ return;
1383
+ }
1384
+ if (session.browserConnectionId !== browser.connectionId) {
1385
+ return;
1386
+ }
1387
+ if (session.state !== "open") {
1388
+ this.sendWriteError(browser.connectionId, {
1389
+ transactionId: message.transactionId,
1390
+ writeId: message.writeId,
1391
+ code: "NO_TRANSACTION",
1392
+ message: "Transaction is not open"
1393
+ });
1394
+ return;
1395
+ }
1396
+ const mobile = this.connectionManager.getByDeviceId(session.deviceId);
1397
+ if (!mobile) {
1398
+ this.sendWriteError(browser.connectionId, {
1399
+ transactionId: message.transactionId,
1400
+ writeId: message.writeId,
1401
+ code: "DEVICE_OFFLINE",
1402
+ message: `Device ${session.deviceId} is not connected`
1403
+ });
1404
+ return;
1405
+ }
1406
+ this.sessions.incrementPendingWrites(message.transactionId);
1407
+ this.router.sendToMobile(
1408
+ session.deviceId,
1409
+ createMessage({
1410
+ type: MessageType.EXECUTE_WRITE,
1411
+ writeId: message.writeId,
1412
+ transactionId: message.transactionId,
1413
+ operation: message.operation
1414
+ })
1415
+ );
1416
+ }
1417
+ handleTransactionAck(message) {
1418
+ const session = this.sessions.get(message.transactionId);
1419
+ if (!session) {
1420
+ return;
1421
+ }
1422
+ if (!message.ok) {
1423
+ this.sessions.setState(message.transactionId, "failed");
1424
+ this.sendWriteError(session.browserConnectionId, {
1425
+ transactionId: message.transactionId,
1426
+ code: message.action === "begin" ? "ADAPTER_ERROR" : "WRITE_FAILED",
1427
+ message: message.message ?? `Transaction ${message.action} failed`
1428
+ });
1429
+ this.sessions.remove(message.transactionId);
1430
+ return;
1431
+ }
1432
+ if (message.action === "begin") {
1433
+ this.sendTransactionStatus(session.browserConnectionId, {
1434
+ transactionId: message.transactionId,
1435
+ deviceId: session.deviceId,
1436
+ state: "open"
1437
+ });
1438
+ return;
1439
+ }
1440
+ if (message.action === "commit") {
1441
+ this.sendTransactionStatus(session.browserConnectionId, {
1442
+ transactionId: message.transactionId,
1443
+ deviceId: session.deviceId,
1444
+ state: "idle"
1445
+ });
1446
+ this.sessions.remove(message.transactionId);
1447
+ return;
1448
+ }
1449
+ this.sendTransactionStatus(session.browserConnectionId, {
1450
+ transactionId: message.transactionId,
1451
+ deviceId: session.deviceId,
1452
+ state: "idle",
1453
+ message: message.message
1454
+ });
1455
+ this.sessions.remove(message.transactionId);
1456
+ }
1457
+ handleWriteAck(message) {
1458
+ const session = this.sessions.get(message.transactionId);
1459
+ if (!session) {
1460
+ return;
1461
+ }
1462
+ this.sessions.decrementPendingWrites(message.transactionId);
1463
+ if (!message.ok) {
1464
+ this.sendWriteError(session.browserConnectionId, {
1465
+ transactionId: message.transactionId,
1466
+ writeId: message.writeId,
1467
+ code: "WRITE_FAILED",
1468
+ message: message.message ?? "Write operation failed"
1469
+ });
1470
+ return;
1471
+ }
1472
+ this.router.sendToBrowser(
1473
+ session.browserConnectionId,
1474
+ createMessage({
1475
+ type: MessageType.WRITE_RESULT,
1476
+ writeId: message.writeId,
1477
+ transactionId: message.transactionId,
1478
+ rowsAffected: message.rowsAffected ?? 0
1479
+ })
1480
+ );
1481
+ }
1482
+ checkTimeouts() {
1483
+ for (const session of this.sessions.getExpired()) {
1484
+ this.rollbackSession(session, "TIMEOUT", "Write transaction timed out");
1485
+ }
1486
+ }
1487
+ rollbackSessionsOnBrowserDisconnect(browserConnectionId) {
1488
+ for (const session of this.sessions.getByBrowserConnectionId(browserConnectionId)) {
1489
+ this.rollbackSession(session, "NO_TRANSACTION", "Browser disconnected");
1490
+ }
1491
+ }
1492
+ rollbackSessionsForDevice(deviceId) {
1493
+ for (const session of this.sessions.getByDeviceId(deviceId)) {
1494
+ this.rollbackSession(session, "NO_TRANSACTION", "Device disconnected");
1495
+ }
1496
+ }
1497
+ rollbackSession(session, code, message) {
1498
+ logger.syncFailed(session.transactionId, code, message);
1499
+ this.sendWriteError(session.browserConnectionId, {
1500
+ transactionId: session.transactionId,
1501
+ code,
1502
+ message
1503
+ });
1504
+ this.router.sendToMobile(
1505
+ session.deviceId,
1506
+ createMessage({
1507
+ type: MessageType.ROLLBACK_TRANSACTION,
1508
+ transactionId: session.transactionId
1509
+ })
1510
+ );
1511
+ this.sessions.remove(session.transactionId);
1512
+ }
1513
+ sendTransactionStatus(browserConnectionId, payload) {
1514
+ this.router.sendToBrowser(
1515
+ browserConnectionId,
1516
+ createMessage({
1517
+ type: MessageType.TRANSACTION_STATUS,
1518
+ ...payload
1519
+ })
1520
+ );
1521
+ }
1522
+ sendWriteError(browserConnectionId, payload) {
1523
+ this.router.sendToBrowser(
1524
+ browserConnectionId,
1525
+ createMessage({
1526
+ type: MessageType.WRITE_ERROR,
1527
+ ...payload
1528
+ })
1529
+ );
1530
+ }
1531
+ };
1532
+
1533
+ // src/server/writeSessionManager.ts
1534
+ var WriteSessionManager = class {
1535
+ sessions = /* @__PURE__ */ new Map();
1536
+ activeByDevice = /* @__PURE__ */ new Map();
1537
+ create(input) {
1538
+ const session = {
1539
+ transactionId: input.transactionId,
1540
+ deviceId: input.deviceId,
1541
+ browserConnectionId: input.browserConnectionId,
1542
+ state: "opening",
1543
+ pendingWrites: 0,
1544
+ createdAt: Date.now()
1545
+ };
1546
+ this.sessions.set(input.transactionId, session);
1547
+ this.activeByDevice.set(input.deviceId, input.transactionId);
1548
+ return session;
1549
+ }
1550
+ get(transactionId) {
1551
+ return this.sessions.get(transactionId);
1552
+ }
1553
+ getActiveForDevice(deviceId) {
1554
+ const transactionId = this.activeByDevice.get(deviceId);
1555
+ if (!transactionId) {
1556
+ return void 0;
1557
+ }
1558
+ return this.sessions.get(transactionId);
1559
+ }
1560
+ setState(transactionId, state) {
1561
+ const session = this.sessions.get(transactionId);
1562
+ if (!session) {
1563
+ return void 0;
1564
+ }
1565
+ session.state = state;
1566
+ return session;
1567
+ }
1568
+ incrementPendingWrites(transactionId) {
1569
+ const session = this.sessions.get(transactionId);
1570
+ if (!session) {
1571
+ return void 0;
1572
+ }
1573
+ session.pendingWrites += 1;
1574
+ return session;
1575
+ }
1576
+ decrementPendingWrites(transactionId) {
1577
+ const session = this.sessions.get(transactionId);
1578
+ if (!session) {
1579
+ return void 0;
1580
+ }
1581
+ session.pendingWrites = Math.max(0, session.pendingWrites - 1);
1582
+ return session;
1583
+ }
1584
+ remove(transactionId) {
1585
+ const session = this.sessions.get(transactionId);
1586
+ if (!session) {
1587
+ return;
1588
+ }
1589
+ if (this.activeByDevice.get(session.deviceId) === transactionId) {
1590
+ this.activeByDevice.delete(session.deviceId);
1591
+ }
1592
+ this.sessions.delete(transactionId);
1593
+ }
1594
+ getExpired(now = Date.now()) {
1595
+ return [...this.sessions.values()].filter((session) => {
1596
+ if (session.state === "idle" || session.state === "failed") {
1597
+ return false;
1598
+ }
1599
+ return now - session.createdAt >= WRITE_TRANSACTION_TIMEOUT_MS;
1600
+ });
1601
+ }
1602
+ getByBrowserConnectionId(browserConnectionId) {
1603
+ return [...this.sessions.values()].filter(
1604
+ (session) => session.browserConnectionId === browserConnectionId
1605
+ );
1606
+ }
1607
+ getByDeviceId(deviceId) {
1608
+ return [...this.sessions.values()].filter((session) => session.deviceId === deviceId);
1609
+ }
1610
+ };
1611
+
1612
+ // src/server/createDevToolsServer.ts
1613
+ async function createDevToolsServer(options = {}) {
1614
+ const host = options.host ?? DEFAULT_DEVTOOLS_HOST;
1615
+ const port = options.port ?? DEFAULT_DEVTOOLS_PORT;
1616
+ const app = express2();
1617
+ const connectionManager = new ConnectionManager();
1618
+ const deviceRegistry = new DeviceRegistry(connectionManager);
1619
+ const router = new MessageRouter(connectionManager, deviceRegistry);
1620
+ const heartbeat = new Heartbeat(connectionManager, router);
1621
+ const snapshotStore = new SnapshotStore();
1622
+ const snapshotFileStore = new SnapshotFileStore({
1623
+ dataDir: options.dataDir,
1624
+ enabled: options.snapshotPersistence
1625
+ });
1626
+ const pendingRefreshStore = new PendingRefreshStore();
1627
+ const writeSessions = new WriteSessionManager();
1628
+ const refreshCoordinator = new RefreshCoordinator(
1629
+ connectionManager,
1630
+ router,
1631
+ pendingRefreshStore,
1632
+ snapshotStore,
1633
+ snapshotFileStore
1634
+ );
1635
+ const writeCoordinator = new WriteCoordinator(connectionManager, router, writeSessions);
1636
+ app.use(createCorsMiddleware());
1637
+ app.get("/health", (_request, response) => {
1638
+ response.json({ ok: true, ...deviceRegistry.snapshot() });
1639
+ });
1640
+ app.post(
1641
+ `${DEVICE_SNAPSHOT_API_PATH}/:deviceId/snapshot`,
1642
+ express2.raw({ type: "application/octet-stream", limit: MAX_SNAPSHOT_BYTES }),
1643
+ async (request, response) => {
1644
+ const deviceId = decodeURIComponent(request.params.deviceId);
1645
+ const body = request.body;
1646
+ if (!Buffer.isBuffer(body) || body.length === 0) {
1647
+ response.status(400).json({ ok: false, error: "Empty snapshot body" });
1648
+ return;
1649
+ }
1650
+ const kindHeader = request.header(SNAPSHOT_KIND_HEADER);
1651
+ const mimeHeader = request.header(SNAPSHOT_MIME_HEADER);
1652
+ const nameHeader = request.header(SNAPSHOT_NAME_HEADER);
1653
+ const result = await refreshCoordinator.handleSnapshotUpload(deviceId, body, {
1654
+ kind: typeof kindHeader === "string" ? kindHeader : void 0,
1655
+ mimeType: typeof mimeHeader === "string" ? mimeHeader : void 0,
1656
+ databaseName: typeof nameHeader === "string" ? nameHeader : void 0
1657
+ });
1658
+ if (!result.ok) {
1659
+ response.status(result.code === "SNAPSHOT_NOT_FOUND" ? 404 : 400).json({
1660
+ ok: false,
1661
+ error: result.code
1662
+ });
1663
+ return;
1664
+ }
1665
+ response.status(201).json({ ok: true, deviceId, size: body.byteLength });
1666
+ }
1667
+ );
1668
+ app.get(`${DEVICE_SNAPSHOT_API_PATH}/:deviceId/snapshot`, async (request, response) => {
1669
+ const deviceId = decodeURIComponent(request.params.deviceId);
1670
+ const snapshot = await refreshCoordinator.getSnapshotAsync(deviceId);
1671
+ if (!snapshot) {
1672
+ response.status(404).json({ ok: false, error: "SNAPSHOT_NOT_FOUND" });
1673
+ return;
1674
+ }
1675
+ response.status(200).set("Content-Type", "application/octet-stream").send(snapshot);
1676
+ });
1677
+ app.get(PROJECT_DATABASE_META_API_PATH, async (_request, response) => {
1678
+ const meta = await snapshotFileStore.getActiveDatabaseMeta();
1679
+ response.json(meta);
1680
+ });
1681
+ app.get(PROJECT_DATABASES_API_PATH, async (_request, response) => {
1682
+ const databases = await snapshotFileStore.listDatabases();
1683
+ response.json({ databases });
1684
+ });
1685
+ app.get(PROJECT_DEVICE_EXPORTS_API_PATH, async (_request, response) => {
1686
+ const devices = await snapshotFileStore.listDeviceExports();
1687
+ response.json({ devices });
1688
+ });
1689
+ app.get(`${PROJECT_DEVICE_EXPORTS_API_PATH}/:deviceId/database/meta`, async (request, response) => {
1690
+ const deviceId = decodeURIComponent(request.params.deviceId);
1691
+ const meta = await snapshotFileStore.getDeviceDatabaseMeta(deviceId);
1692
+ response.json(meta);
1693
+ });
1694
+ app.get(`${PROJECT_DEVICE_EXPORTS_API_PATH}/:deviceId/database`, async (request, response) => {
1695
+ const deviceId = decodeURIComponent(request.params.deviceId);
1696
+ const meta = await snapshotFileStore.getDeviceDatabaseMeta(deviceId);
1697
+ if (!meta.exists) {
1698
+ response.status(404).json({ ok: false, error: "DEVICE_DATABASE_NOT_FOUND", deviceId });
1699
+ return;
1700
+ }
1701
+ const bytes = await snapshotFileStore.readDeviceDatabase(deviceId);
1702
+ if (!bytes) {
1703
+ response.status(404).json({ ok: false, error: "DEVICE_DATABASE_NOT_FOUND", deviceId });
1704
+ return;
1705
+ }
1706
+ response.status(200).set("Content-Type", meta.mimeType ?? SQLITE_SNAPSHOT_MIME_TYPE).send(bytes);
1707
+ });
1708
+ app.get(PROJECT_DATABASE_API_PATH, async (_request, response) => {
1709
+ const meta = await snapshotFileStore.getActiveDatabaseMeta();
1710
+ if (!meta.exists) {
1711
+ response.status(404).json({ ok: false, error: "PROJECT_DATABASE_NOT_FOUND" });
1712
+ return;
1713
+ }
1714
+ const bytes = await snapshotFileStore.readActiveDatabase();
1715
+ if (!bytes) {
1716
+ response.status(404).json({ ok: false, error: "PROJECT_DATABASE_NOT_FOUND" });
1717
+ return;
1718
+ }
1719
+ response.status(200).set("Content-Type", meta.mimeType ?? SQLITE_SNAPSHOT_MIME_TYPE).send(bytes);
1720
+ });
1721
+ if (options.webDistPath) {
1722
+ attachWebUi(app, options.webDistPath);
1723
+ }
1724
+ const httpServer = createServer(app);
1725
+ attachWebSocket(
1726
+ httpServer,
1727
+ connectionManager,
1728
+ router,
1729
+ heartbeat,
1730
+ refreshCoordinator,
1731
+ writeCoordinator,
1732
+ { snapshotFileStore }
1733
+ );
1734
+ const refreshTimeoutInterval = setInterval(() => {
1735
+ refreshCoordinator.checkTimeouts();
1736
+ writeCoordinator.checkTimeouts();
1737
+ }, 5e3);
1738
+ if (snapshotFileStore.isEnabled()) {
1739
+ await snapshotFileStore.listDeviceExports();
1740
+ }
1741
+ await new Promise((resolve, reject) => {
1742
+ httpServer.once("error", reject);
1743
+ httpServer.listen(port, host, () => resolve());
1744
+ });
1745
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
1746
+ const httpUrl = buildDevToolsHttpUrl(displayHost, port);
1747
+ const wsUrl = buildDevToolsWsUrl(displayHost, port);
1748
+ const webUiEnabled = Boolean(options.webDistPath);
1749
+ logger.serverStarted(httpUrl, wsUrl, webUiEnabled);
1750
+ return {
1751
+ app,
1752
+ httpServer,
1753
+ connectionManager,
1754
+ deviceRegistry,
1755
+ router,
1756
+ heartbeat,
1757
+ snapshotStore,
1758
+ snapshotFileStore,
1759
+ pendingRefreshStore,
1760
+ writeSessions,
1761
+ refreshCoordinator,
1762
+ writeCoordinator,
1763
+ urls: {
1764
+ httpUrl,
1765
+ wsUrl,
1766
+ webUiEnabled
1767
+ },
1768
+ close: () => new Promise((resolve, reject) => {
1769
+ clearInterval(refreshTimeoutInterval);
1770
+ heartbeat.stop();
1771
+ httpServer.close((error) => {
1772
+ if (error) {
1773
+ reject(error);
1774
+ return;
1775
+ }
1776
+ resolve();
1777
+ });
1778
+ })
1779
+ };
1780
+ }
1781
+
1782
+ export { ConnectionManager, DeviceRegistry, Heartbeat, MessageRouter, PendingRefreshStore, RefreshCoordinator, SnapshotStore, createCorsMiddleware, createDevToolsServer, resolveCorsOrigin };
1783
+ //# sourceMappingURL=chunk-TNDOAHT5.js.map
1784
+ //# sourceMappingURL=chunk-TNDOAHT5.js.map