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.
- package/LICENSE +5 -0
- package/dist/adapter/index.cjs +314 -0
- package/dist/adapter/index.cjs.map +1 -0
- package/dist/adapter/index.d.cts +34 -0
- package/dist/adapter/index.d.ts +34 -0
- package/dist/adapter/index.js +6 -0
- package/dist/adapter/index.js.map +1 -0
- package/dist/adapter-BjJm71wW.d.cts +30 -0
- package/dist/adapter-DJSjDewg.d.ts +30 -0
- package/dist/adapters/sqlite/index.cjs +251 -0
- package/dist/adapters/sqlite/index.cjs.map +1 -0
- package/dist/adapters/sqlite/index.d.cts +21 -0
- package/dist/adapters/sqlite/index.d.ts +21 -0
- package/dist/adapters/sqlite/index.js +6 -0
- package/dist/adapters/sqlite/index.js.map +1 -0
- package/dist/chunk-2JGL2JVD.js +52 -0
- package/dist/chunk-2JGL2JVD.js.map +1 -0
- package/dist/chunk-AZECU6D7.js +27 -0
- package/dist/chunk-AZECU6D7.js.map +1 -0
- package/dist/chunk-BLOSGEQV.js +71 -0
- package/dist/chunk-BLOSGEQV.js.map +1 -0
- package/dist/chunk-CPVVIH5G.js +258 -0
- package/dist/chunk-CPVVIH5G.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +10 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-HIDZTOJ3.js +9 -0
- package/dist/chunk-HIDZTOJ3.js.map +1 -0
- package/dist/chunk-I65IM47D.js +243 -0
- package/dist/chunk-I65IM47D.js.map +1 -0
- package/dist/chunk-MXVIGMVT.js +3 -0
- package/dist/chunk-MXVIGMVT.js.map +1 -0
- package/dist/chunk-O6AO7KHA.js +3 -0
- package/dist/chunk-O6AO7KHA.js.map +1 -0
- package/dist/chunk-TNDOAHT5.js +1784 -0
- package/dist/chunk-TNDOAHT5.js.map +1 -0
- package/dist/chunk-ZS5MZR75.js +655 -0
- package/dist/chunk-ZS5MZR75.js.map +1 -0
- package/dist/cli.cjs +2053 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/client/index.cjs +947 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +75 -0
- package/dist/client/index.d.ts +75 -0
- package/dist/client/index.js +98 -0
- package/dist/client/index.js.map +1 -0
- package/dist/createDevToolsServer-B7NQ_5H4.d.cts +292 -0
- package/dist/createDevToolsServer-DlNMoCJR.d.ts +292 -0
- package/dist/handleDeviceSnapshot-CR-4QMdk.d.ts +116 -0
- package/dist/handleDeviceSnapshot-cYGuitSc.d.cts +116 -0
- package/dist/index.cjs +5877 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +61 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +2748 -0
- package/dist/index.js.map +1 -0
- package/dist/inspector/index.cjs +57 -0
- package/dist/inspector/index.cjs.map +1 -0
- package/dist/inspector/index.d.cts +26 -0
- package/dist/inspector/index.d.ts +26 -0
- package/dist/inspector/index.js +5 -0
- package/dist/inspector/index.js.map +1 -0
- package/dist/inspectors/sqlite/index.cjs +306 -0
- package/dist/inspectors/sqlite/index.cjs.map +1 -0
- package/dist/inspectors/sqlite/index.d.cts +53 -0
- package/dist/inspectors/sqlite/index.d.ts +53 -0
- package/dist/inspectors/sqlite/index.js +260 -0
- package/dist/inspectors/sqlite/index.js.map +1 -0
- package/dist/register-BuicFu9L.d.cts +32 -0
- package/dist/register-aITLN9Q9.d.ts +32 -0
- package/dist/server/index.cjs +1948 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +15 -0
- package/dist/server/index.d.ts +15 -0
- package/dist/server/index.js +7 -0
- package/dist/server/index.js.map +1 -0
- package/dist/snapshot-Dhyecgo2.d.cts +33 -0
- package/dist/snapshot-Dhyecgo2.d.ts +33 -0
- package/dist/types/protocol.cjs +309 -0
- package/dist/types/protocol.cjs.map +1 -0
- package/dist/types/protocol.d.cts +291 -0
- package/dist/types/protocol.d.ts +291 -0
- package/dist/types/protocol.js +4 -0
- package/dist/types/protocol.js.map +1 -0
- package/dist/types-BRLJdBRY.d.cts +16 -0
- package/dist/types-CF6qEU-_.d.ts +16 -0
- package/dist/types-CIA11diJ.d.cts +62 -0
- package/dist/types-CKVRCX-e.d.ts +62 -0
- package/dist/web/assets/index-CLEaIsUd.css +1 -0
- package/dist/web/assets/index-DQrPRTOy.js +100 -0
- package/dist/web/assets/sql-wasm-UFUCzYNW.wasm +0 -0
- package/dist/web/index.html +26 -0
- package/dist/write-C3toEIMs.d.cts +21 -0
- package/dist/write-C3toEIMs.d.ts +21 -0
- 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
|