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