syncorejs 0.2.3 → 0.2.5
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/dist/_dashboard/assets/ConfirmActionDialog-Db4VzVp6.js +1 -0
- package/dist/_dashboard/assets/circle-x-VsB4Z8W4.js +1 -0
- package/dist/_dashboard/assets/data.lazy-DjdU9CzX.js +18 -0
- package/dist/_dashboard/assets/file-code-BrOKjG4n.js +1 -0
- package/dist/_dashboard/assets/functions.lazy-DvDwAGHq.js +1 -0
- package/dist/_dashboard/assets/funnel-BH8EMMJI.js +1 -0
- package/dist/_dashboard/assets/index-DT9ZEELb.css +1 -0
- package/dist/_dashboard/assets/index-DrSG4qZZ.js +54 -0
- package/dist/_dashboard/assets/loader-circle-CmJFSYga.js +1 -0
- package/dist/_dashboard/assets/logs.lazy-50KTk5yd.js +1 -0
- package/dist/_dashboard/assets/play-DS52VsLN.js +1 -0
- package/dist/_dashboard/assets/queries.lazy-CfysRWkz.js +1 -0
- package/dist/_dashboard/assets/scheduler.lazy-BB88mZk-.js +1 -0
- package/dist/_dashboard/assets/select-THYcR8Wt.js +1 -0
- package/dist/_dashboard/assets/separator-BU7xg615.js +1 -0
- package/dist/_dashboard/assets/shared-Bh0wwC2k.js +1 -0
- package/dist/_dashboard/assets/sql.lazy-CHtU9Qnt.js +13 -0
- package/dist/_dashboard/assets/storage.lazy-CneN7wVU.js +1 -0
- package/dist/_dashboard/assets/table-2-CH8JoMXf.js +1 -0
- package/dist/_dashboard/index.html +18 -0
- package/dist/_vendor/cli/app.d.mts.map +1 -1
- package/dist/_vendor/cli/app.mjs +16 -5
- package/dist/_vendor/cli/app.mjs.map +1 -1
- package/dist/_vendor/core/cli.d.mts.map +1 -1
- package/dist/_vendor/core/cli.mjs +358 -16
- package/dist/_vendor/core/cli.mjs.map +1 -1
- package/dist/_vendor/core/index.d.mts +3 -3
- package/dist/_vendor/core/runtime/devtools.d.mts.map +1 -1
- package/dist/_vendor/core/runtime/devtools.mjs +131 -0
- package/dist/_vendor/core/runtime/devtools.mjs.map +1 -1
- package/dist/_vendor/core/runtime/functions.d.mts +3 -3
- package/dist/_vendor/core/runtime/functions.mjs.map +1 -1
- package/dist/_vendor/core/runtime/internal/engines/devtoolsEngine.mjs +1 -1
- package/dist/_vendor/core/runtime/internal/engines/devtoolsEngine.mjs.map +1 -1
- package/dist/_vendor/core/runtime/internal/engines/executionEngine.mjs +4 -1
- package/dist/_vendor/core/runtime/internal/engines/executionEngine.mjs.map +1 -1
- package/dist/_vendor/core/runtime/internal/engines/reactivityEngine.mjs +6 -3
- package/dist/_vendor/core/runtime/internal/engines/reactivityEngine.mjs.map +1 -1
- package/dist/_vendor/core/runtime/internal/engines/shared.mjs +5 -1
- package/dist/_vendor/core/runtime/internal/engines/shared.mjs.map +1 -1
- package/dist/_vendor/core/runtime/internal/engines/storageEngine.mjs +99 -13
- package/dist/_vendor/core/runtime/internal/engines/storageEngine.mjs.map +1 -1
- package/dist/_vendor/core/runtime/internal/runtimeKernel.mjs +38 -4
- package/dist/_vendor/core/runtime/internal/runtimeKernel.mjs.map +1 -1
- package/dist/_vendor/core/runtime/runtime.d.mts +65 -8
- package/dist/_vendor/core/runtime/runtime.d.mts.map +1 -1
- package/dist/_vendor/core/runtime/runtime.mjs.map +1 -1
- package/dist/_vendor/core/transport.d.mts.map +1 -1
- package/dist/_vendor/core/transport.mjs +30 -5
- package/dist/_vendor/core/transport.mjs.map +1 -1
- package/dist/_vendor/devtools-protocol/index.d.ts +75 -1
- package/dist/_vendor/devtools-protocol/index.d.ts.map +1 -1
- package/dist/_vendor/devtools-protocol/index.js.map +1 -1
- package/dist/_vendor/next/index.js +9 -1
- package/dist/_vendor/next/index.js.map +1 -1
- package/dist/_vendor/platform-expo/index.d.ts +1 -1
- package/dist/_vendor/platform-expo/index.d.ts.map +1 -1
- package/dist/_vendor/platform-expo/index.js +6 -1
- package/dist/_vendor/platform-expo/index.js.map +1 -1
- package/dist/_vendor/platform-node/index.d.mts +2 -1
- package/dist/_vendor/platform-node/index.d.mts.map +1 -1
- package/dist/_vendor/platform-node/index.mjs +27 -2
- package/dist/_vendor/platform-node/index.mjs.map +1 -1
- package/dist/_vendor/platform-node/ipc-react.mjs +4 -0
- package/dist/_vendor/platform-node/ipc-react.mjs.map +1 -1
- package/dist/_vendor/platform-web/external-change.d.ts +2 -2
- package/dist/_vendor/platform-web/external-change.js +2 -2
- package/dist/_vendor/platform-web/external-change.js.map +1 -1
- package/dist/_vendor/platform-web/index.d.ts +13 -10
- package/dist/_vendor/platform-web/index.d.ts.map +1 -1
- package/dist/_vendor/platform-web/index.js +66 -10
- package/dist/_vendor/platform-web/index.js.map +1 -1
- package/dist/_vendor/platform-web/indexeddb.d.ts +3 -3
- package/dist/_vendor/platform-web/indexeddb.js +3 -3
- package/dist/_vendor/platform-web/indexeddb.js.map +1 -1
- package/dist/_vendor/platform-web/opfs.d.ts +3 -1
- package/dist/_vendor/platform-web/opfs.d.ts.map +1 -1
- package/dist/_vendor/platform-web/opfs.js +29 -3
- package/dist/_vendor/platform-web/opfs.js.map +1 -1
- package/dist/_vendor/platform-web/persistence.d.ts +31 -1
- package/dist/_vendor/platform-web/persistence.d.ts.map +1 -1
- package/dist/_vendor/platform-web/persistence.js.map +1 -1
- package/dist/_vendor/platform-web/react.d.ts.map +1 -1
- package/dist/_vendor/platform-web/react.js +9 -1
- package/dist/_vendor/platform-web/react.js.map +1 -1
- package/dist/_vendor/react/index.d.ts +6 -5
- package/dist/_vendor/react/index.d.ts.map +1 -1
- package/dist/_vendor/react/index.js +6 -5
- package/dist/_vendor/react/index.js.map +1 -1
- package/dist/_vendor/svelte/index.d.ts +8 -6
- package/dist/_vendor/svelte/index.d.ts.map +1 -1
- package/dist/_vendor/svelte/index.js +7 -5
- package/dist/_vendor/svelte/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -4,7 +4,8 @@ import { SyncoreRuntime } from "./runtime/runtime.mjs";
|
|
|
4
4
|
import { createDevtoolsCommandHandler, createDevtoolsSubscriptionHost } from "./runtime/devtools.mjs";
|
|
5
5
|
import { src_exports } from "./index.mjs";
|
|
6
6
|
import { generateDevtoolsToken, isAllowedDashboardOrigin, isAuthorizedDashboardRequest, sanitizeDevtoolsToken } from "./devtools-auth.mjs";
|
|
7
|
-
import { appendFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
7
|
+
import { appendFile, mkdir, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
8
9
|
import { createServer } from "node:http";
|
|
9
10
|
import { connect } from "node:net";
|
|
10
11
|
import path from "node:path";
|
|
@@ -36,6 +37,9 @@ const VALID_SYNCORE_TEMPLATES = [
|
|
|
36
37
|
let pendingDevBootstrap;
|
|
37
38
|
let devBootstrapInFlight = false;
|
|
38
39
|
const PROJECT_TARGET_RUNTIME_ID = "syncore-project-target";
|
|
40
|
+
const STORAGE_ACCESS_TICKET_TTL_MS = 300 * 1e3;
|
|
41
|
+
const STORAGE_ACCESS_CHUNK_BYTES = 1024 * 1024;
|
|
42
|
+
const STORAGE_ACCESS_MAX_PREVIEW_BYTES = 8e4;
|
|
39
43
|
program.name("syncorejs").description("Syncore local-first toolkit CLI").version("0.1.0");
|
|
40
44
|
program.command("init").description("Scaffold Syncore in the current directory").option("--template <template>", `Template to scaffold (${VALID_SYNCORE_TEMPLATES.join(", ")}, or auto)`, "auto").option("--force", "Overwrite Syncore-managed files when they already exist").action(async (options) => {
|
|
41
45
|
const cwd = process.cwd();
|
|
@@ -1161,6 +1165,19 @@ var HubFileStorageAdapter = class {
|
|
|
1161
1165
|
return null;
|
|
1162
1166
|
}
|
|
1163
1167
|
}
|
|
1168
|
+
async readRange(id, offset, length) {
|
|
1169
|
+
let handle;
|
|
1170
|
+
try {
|
|
1171
|
+
handle = await open(this.filePath(id), "r");
|
|
1172
|
+
const buffer = Buffer.alloc(Math.max(length, 0));
|
|
1173
|
+
const result = await handle.read(buffer, 0, buffer.byteLength, Math.max(offset, 0));
|
|
1174
|
+
return buffer.subarray(0, result.bytesRead);
|
|
1175
|
+
} catch {
|
|
1176
|
+
return null;
|
|
1177
|
+
} finally {
|
|
1178
|
+
await handle?.close();
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1164
1181
|
async delete(id) {
|
|
1165
1182
|
await rm(this.filePath(id), { force: true });
|
|
1166
1183
|
}
|
|
@@ -1252,6 +1269,11 @@ async function createProjectTargetBackend(cwd, externalChangeSignal) {
|
|
|
1252
1269
|
driver,
|
|
1253
1270
|
storage: new HubFileStorageAdapter(storageDirectory),
|
|
1254
1271
|
platform: "project",
|
|
1272
|
+
runtimeCapabilities: { storage: {
|
|
1273
|
+
available: true,
|
|
1274
|
+
protocol: "file",
|
|
1275
|
+
supportsRange: true
|
|
1276
|
+
} },
|
|
1255
1277
|
...externalChangeSignal ? { externalChangeSignal } : {}
|
|
1256
1278
|
});
|
|
1257
1279
|
await runtime.start();
|
|
@@ -1310,6 +1332,13 @@ function createProjectDevtoolsCapabilities() {
|
|
|
1310
1332
|
mutate: true,
|
|
1311
1333
|
importExport: true
|
|
1312
1334
|
},
|
|
1335
|
+
storage: {
|
|
1336
|
+
browse: true,
|
|
1337
|
+
download: true,
|
|
1338
|
+
readRange: true,
|
|
1339
|
+
delete: true,
|
|
1340
|
+
maxPreviewBytes: 8e4
|
|
1341
|
+
},
|
|
1313
1342
|
scheduler: {
|
|
1314
1343
|
read: true,
|
|
1315
1344
|
edit: true
|
|
@@ -1493,18 +1522,22 @@ async function startDevHub(options) {
|
|
|
1493
1522
|
await runDevProjectBootstrap(options.cwd, options.template);
|
|
1494
1523
|
await setupDevProjectWatch(options.cwd, options.template);
|
|
1495
1524
|
if (await isLocalPortInUse(devtoolsPort)) {
|
|
1525
|
+
const activeSessionState = await readDevtoolsSessionState(options.cwd) ?? sessionState;
|
|
1496
1526
|
console.log(`Syncore devtools hub already running at ws://localhost:${devtoolsPort}. Reusing existing hub/dashboard.`);
|
|
1497
|
-
|
|
1527
|
+
console.log(`Devtools dashboard token: ${activeSessionState.token}`);
|
|
1528
|
+
console.log(`Dashboard shell: ${activeSessionState.authenticatedDashboardUrl}`);
|
|
1529
|
+
return activeSessionState;
|
|
1498
1530
|
}
|
|
1499
1531
|
await writeDevtoolsSessionState(options.cwd, sessionState);
|
|
1500
|
-
const httpServer = createServer((
|
|
1501
|
-
response.
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
}));
|
|
1532
|
+
const httpServer = createServer((request, response) => {
|
|
1533
|
+
handleStorageAccessHttpRequest(request, response).catch((error) => {
|
|
1534
|
+
if (!response.headersSent) writeJsonResponse(response, 500, { error: formatError(error) });
|
|
1535
|
+
else response.destroy(error instanceof Error ? error : void 0);
|
|
1536
|
+
});
|
|
1506
1537
|
});
|
|
1507
1538
|
const websocketServer = new WebSocketServer({ server: httpServer });
|
|
1539
|
+
const storageAccessTickets = /* @__PURE__ */ new Map();
|
|
1540
|
+
const pendingHubCommands = /* @__PURE__ */ new Map();
|
|
1508
1541
|
const runtimeSockets = /* @__PURE__ */ new Map();
|
|
1509
1542
|
const runtimeHellos = /* @__PURE__ */ new Map();
|
|
1510
1543
|
const runtimeEvents = /* @__PURE__ */ new Map();
|
|
@@ -1571,6 +1604,135 @@ async function startDevHub(options) {
|
|
|
1571
1604
|
event
|
|
1572
1605
|
})}\n`);
|
|
1573
1606
|
};
|
|
1607
|
+
const requestRuntimeCommand = async (targetRuntimeId, payload, timeoutMs = 3e4) => {
|
|
1608
|
+
if (targetRuntimeId === PROJECT_TARGET_RUNTIME_ID && projectTargetBackend) return projectTargetBackend.handleCommand(payload);
|
|
1609
|
+
const target = runtimeSockets.get(targetRuntimeId);
|
|
1610
|
+
if (!target || target.readyState !== WebSocket.OPEN) throw new Error(`Runtime ${targetRuntimeId} is not connected.`);
|
|
1611
|
+
const commandId = `hub:${randomUUID()}`;
|
|
1612
|
+
return await new Promise((resolve, reject) => {
|
|
1613
|
+
const timeout = setTimeout(() => {
|
|
1614
|
+
pendingHubCommands.delete(commandId);
|
|
1615
|
+
reject(/* @__PURE__ */ new Error(`Runtime command ${payload.kind} timed out.`));
|
|
1616
|
+
}, timeoutMs);
|
|
1617
|
+
pendingHubCommands.set(commandId, {
|
|
1618
|
+
resolve,
|
|
1619
|
+
reject,
|
|
1620
|
+
timeout
|
|
1621
|
+
});
|
|
1622
|
+
target.send(JSON.stringify({
|
|
1623
|
+
type: "command",
|
|
1624
|
+
commandId,
|
|
1625
|
+
targetRuntimeId,
|
|
1626
|
+
payload
|
|
1627
|
+
}));
|
|
1628
|
+
});
|
|
1629
|
+
};
|
|
1630
|
+
const createStorageAccessTicket = async (targetRuntimeId, id, purpose) => {
|
|
1631
|
+
const metadata = await requestRuntimeCommand(targetRuntimeId, {
|
|
1632
|
+
kind: "storage.readRange",
|
|
1633
|
+
id,
|
|
1634
|
+
offset: 0,
|
|
1635
|
+
length: 0
|
|
1636
|
+
});
|
|
1637
|
+
if (metadata.kind !== "storage.readRange.result") return {
|
|
1638
|
+
kind: "storage.access.create.result",
|
|
1639
|
+
error: "Runtime returned an unexpected storage access response."
|
|
1640
|
+
};
|
|
1641
|
+
if (metadata.error || !metadata.entry) return {
|
|
1642
|
+
kind: "storage.access.create.result",
|
|
1643
|
+
error: metadata.error ?? "Storage object could not be accessed."
|
|
1644
|
+
};
|
|
1645
|
+
const ticket = randomUUID();
|
|
1646
|
+
const expiresAt = Date.now() + STORAGE_ACCESS_TICKET_TTL_MS;
|
|
1647
|
+
storageAccessTickets.set(ticket, {
|
|
1648
|
+
id: ticket,
|
|
1649
|
+
runtimeId: targetRuntimeId,
|
|
1650
|
+
storageId: id,
|
|
1651
|
+
purpose,
|
|
1652
|
+
entry: metadata.entry,
|
|
1653
|
+
supportsRange: metadata.supportsRange,
|
|
1654
|
+
expiresAt
|
|
1655
|
+
});
|
|
1656
|
+
cleanupExpiredStorageTickets();
|
|
1657
|
+
return {
|
|
1658
|
+
kind: "storage.access.create.result",
|
|
1659
|
+
entry: metadata.entry,
|
|
1660
|
+
url: `http://127.0.0.1:${devtoolsPort}/storage/access/${ticket}`,
|
|
1661
|
+
expiresAt,
|
|
1662
|
+
supportsRange: metadata.supportsRange,
|
|
1663
|
+
maxPreviewBytes: STORAGE_ACCESS_MAX_PREVIEW_BYTES
|
|
1664
|
+
};
|
|
1665
|
+
};
|
|
1666
|
+
const cleanupExpiredStorageTickets = () => {
|
|
1667
|
+
const now = Date.now();
|
|
1668
|
+
for (const [ticket, access] of storageAccessTickets) if (access.expiresAt <= now) storageAccessTickets.delete(ticket);
|
|
1669
|
+
};
|
|
1670
|
+
const handleStorageAccessHttpRequest = async (request, response) => {
|
|
1671
|
+
setStorageCorsHeaders(response);
|
|
1672
|
+
if (request.method === "OPTIONS") {
|
|
1673
|
+
response.writeHead(204);
|
|
1674
|
+
response.end();
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
const requestUrl = new URL(request.url ?? "/", `http://127.0.0.1:${devtoolsPort}`);
|
|
1678
|
+
const match = /^\/storage\/access\/([^/]+)$/.exec(requestUrl.pathname);
|
|
1679
|
+
if (!match) {
|
|
1680
|
+
writeJsonResponse(response, 200, {
|
|
1681
|
+
ok: true,
|
|
1682
|
+
wsPort: devtoolsPort
|
|
1683
|
+
});
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
1687
|
+
writeTextResponse(response, 405, "Method not allowed.");
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
cleanupExpiredStorageTickets();
|
|
1691
|
+
const ticket = storageAccessTickets.get(match[1]);
|
|
1692
|
+
if (!ticket || ticket.expiresAt <= Date.now()) {
|
|
1693
|
+
writeTextResponse(response, 401, "Storage access ticket is invalid or expired.");
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
const range = parseStorageRangeHeader(request.headers.range, ticket.entry.size);
|
|
1697
|
+
if ("error" in range) {
|
|
1698
|
+
writeTextResponse(response, range.status, range.error);
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
const start = range.start;
|
|
1702
|
+
const end = range.end;
|
|
1703
|
+
const byteLength = end >= start ? end - start + 1 : 0;
|
|
1704
|
+
if (!ticket.supportsRange && ticket.entry.size > STORAGE_ACCESS_CHUNK_BYTES) {
|
|
1705
|
+
writeTextResponse(response, 409, "This storage backend does not support streaming large files.");
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
writeStorageAccessHeaders(response, ticket, {
|
|
1709
|
+
status: range.partial ? 206 : 200,
|
|
1710
|
+
start,
|
|
1711
|
+
end,
|
|
1712
|
+
byteLength
|
|
1713
|
+
});
|
|
1714
|
+
if (request.method === "HEAD") {
|
|
1715
|
+
response.end();
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
let offset = start;
|
|
1719
|
+
while (offset <= end) {
|
|
1720
|
+
const chunkLength = Math.min(STORAGE_ACCESS_CHUNK_BYTES, end - offset + 1);
|
|
1721
|
+
const chunk = await requestRuntimeCommand(ticket.runtimeId, {
|
|
1722
|
+
kind: "storage.readRange",
|
|
1723
|
+
id: ticket.storageId,
|
|
1724
|
+
offset,
|
|
1725
|
+
length: chunkLength
|
|
1726
|
+
}, 6e4);
|
|
1727
|
+
if (chunk.kind !== "storage.readRange.result") throw new Error("Runtime returned an unexpected storage chunk response.");
|
|
1728
|
+
if (chunk.error || !chunk.base64) throw new Error(chunk.error ?? "Storage chunk could not be read.");
|
|
1729
|
+
const bytes = Buffer.from(chunk.base64, "base64");
|
|
1730
|
+
if (bytes.byteLength === 0) break;
|
|
1731
|
+
await writeResponseChunk(response, bytes);
|
|
1732
|
+
offset += bytes.byteLength;
|
|
1733
|
+
}
|
|
1734
|
+
response.end();
|
|
1735
|
+
};
|
|
1574
1736
|
websocketServer.on("connection", (socket, request) => {
|
|
1575
1737
|
const isBrowserDashboardClient = isAllowedDashboardOrigin(request.headers.origin, dashboardPort);
|
|
1576
1738
|
const isAuthorizedDashboardClient = !isBrowserDashboardClient || isAuthorizedDashboardRequest({
|
|
@@ -1616,6 +1778,29 @@ async function startDevHub(options) {
|
|
|
1616
1778
|
if (!isAuthorizedDashboardClient) return;
|
|
1617
1779
|
const targetRuntimeId = message.targetRuntimeId;
|
|
1618
1780
|
if (!targetRuntimeId) return;
|
|
1781
|
+
if (message.payload.kind === "storage.access.create") {
|
|
1782
|
+
createStorageAccessTicket(targetRuntimeId, message.payload.id, message.payload.purpose).then((payload) => {
|
|
1783
|
+
if (socket.readyState !== WebSocket.OPEN) return;
|
|
1784
|
+
socket.send(JSON.stringify({
|
|
1785
|
+
type: "command.result",
|
|
1786
|
+
commandId: message.commandId,
|
|
1787
|
+
runtimeId: targetRuntimeId,
|
|
1788
|
+
payload
|
|
1789
|
+
}));
|
|
1790
|
+
}).catch((error) => {
|
|
1791
|
+
if (socket.readyState !== WebSocket.OPEN) return;
|
|
1792
|
+
socket.send(JSON.stringify({
|
|
1793
|
+
type: "command.result",
|
|
1794
|
+
commandId: message.commandId,
|
|
1795
|
+
runtimeId: targetRuntimeId,
|
|
1796
|
+
payload: {
|
|
1797
|
+
kind: "storage.access.create.result",
|
|
1798
|
+
error: formatError(error)
|
|
1799
|
+
}
|
|
1800
|
+
}));
|
|
1801
|
+
});
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1619
1804
|
if (targetRuntimeId === PROJECT_TARGET_RUNTIME_ID && projectTargetBackend) {
|
|
1620
1805
|
(async () => {
|
|
1621
1806
|
const payload = await projectTargetBackend.handleCommand(message.payload);
|
|
@@ -1719,6 +1904,15 @@ async function startDevHub(options) {
|
|
|
1719
1904
|
appendHubLog(message.event);
|
|
1720
1905
|
} else if (message.type === "event") appendHubLog(message.event);
|
|
1721
1906
|
if (message.type === "command.result" || message.type === "subscription.data" || message.type === "subscription.error") {
|
|
1907
|
+
if (message.type === "command.result") {
|
|
1908
|
+
const pending = pendingHubCommands.get(message.commandId);
|
|
1909
|
+
if (pending) {
|
|
1910
|
+
clearTimeout(pending.timeout);
|
|
1911
|
+
pendingHubCommands.delete(message.commandId);
|
|
1912
|
+
pending.resolve(message.payload);
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1722
1916
|
for (const client of dashboardSockets) if (client.readyState === WebSocket.OPEN) client.send(encoded);
|
|
1723
1917
|
return;
|
|
1724
1918
|
}
|
|
@@ -1787,19 +1981,18 @@ async function startDevHub(options) {
|
|
|
1787
1981
|
console.log(`Electron/Node runtimes: set devtoolsUrl to ws://localhost:${devtoolsPort}.`);
|
|
1788
1982
|
console.log(`Web/Next apps: connect the dashboard or worker bridge to ws://localhost:${devtoolsPort}.`);
|
|
1789
1983
|
console.log("Expo apps: use the same hub URL through LAN or adb reverse while developing.");
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
server: { port: dashboardPort }
|
|
1796
|
-
})).listen();
|
|
1984
|
+
let dashboardStaticServer;
|
|
1985
|
+
const dashboardStaticRoot = await resolvePackagedDashboardRoot();
|
|
1986
|
+
if (!dashboardStaticRoot) throw new Error("Syncore Dashboard build is missing. Expected the packaged dashboard at syncorejs/dist/_dashboard/index.html. Rebuild syncorejs before running `syncorejs dev`.");
|
|
1987
|
+
try {
|
|
1988
|
+
dashboardStaticServer = await startPackagedDashboardServer(dashboardStaticRoot, dashboardPort);
|
|
1797
1989
|
console.log(`Dashboard shell: ${sessionState.authenticatedDashboardUrl}`);
|
|
1798
1990
|
} catch (error) {
|
|
1799
|
-
|
|
1991
|
+
throw new Error(`Syncore Dashboard shell could not start: ${formatError(error)}`, { cause: error });
|
|
1800
1992
|
}
|
|
1801
1993
|
const close = () => {
|
|
1802
1994
|
projectTargetBackend?.dispose();
|
|
1995
|
+
dashboardStaticServer?.close();
|
|
1803
1996
|
websocketServer.close();
|
|
1804
1997
|
httpServer.close();
|
|
1805
1998
|
process.exit(0);
|
|
@@ -1808,6 +2001,155 @@ async function startDevHub(options) {
|
|
|
1808
2001
|
process.on("SIGTERM", close);
|
|
1809
2002
|
return sessionState;
|
|
1810
2003
|
}
|
|
2004
|
+
async function resolvePackagedDashboardRoot() {
|
|
2005
|
+
const candidates = [
|
|
2006
|
+
path.resolve(CORE_PACKAGE_ROOT, "..", "_dashboard"),
|
|
2007
|
+
path.resolve(CORE_PACKAGE_ROOT, "_dashboard"),
|
|
2008
|
+
path.resolve(CORE_PACKAGE_ROOT, "..", "syncore", "dist", "_dashboard")
|
|
2009
|
+
];
|
|
2010
|
+
for (const candidate of candidates) if (await fileExists(path.join(candidate, "index.html"))) return candidate;
|
|
2011
|
+
return null;
|
|
2012
|
+
}
|
|
2013
|
+
async function startPackagedDashboardServer(root, port) {
|
|
2014
|
+
const server = createServer((request, response) => {
|
|
2015
|
+
serveDashboardAsset(root, request, response).catch((error) => {
|
|
2016
|
+
if (!response.headersSent) writeTextResponse(response, 500, formatError(error));
|
|
2017
|
+
else response.destroy(error instanceof Error ? error : void 0);
|
|
2018
|
+
});
|
|
2019
|
+
});
|
|
2020
|
+
await new Promise((resolve, reject) => {
|
|
2021
|
+
server.once("error", reject);
|
|
2022
|
+
server.listen(port, "127.0.0.1", () => {
|
|
2023
|
+
server.off("error", reject);
|
|
2024
|
+
resolve();
|
|
2025
|
+
});
|
|
2026
|
+
});
|
|
2027
|
+
return server;
|
|
2028
|
+
}
|
|
2029
|
+
async function serveDashboardAsset(root, request, response) {
|
|
2030
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
2031
|
+
writeTextResponse(response, 405, "Method not allowed.");
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
2035
|
+
const requestedPath = decodeURIComponent(requestUrl.pathname);
|
|
2036
|
+
const relativePath = requestedPath === "/" ? "index.html" : requestedPath.slice(1);
|
|
2037
|
+
let assetPath = path.resolve(root, relativePath);
|
|
2038
|
+
if (!isPathInside(root, assetPath)) {
|
|
2039
|
+
writeTextResponse(response, 403, "Forbidden.");
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
try {
|
|
2043
|
+
if ((await stat(assetPath)).isDirectory()) assetPath = path.join(assetPath, "index.html");
|
|
2044
|
+
} catch {
|
|
2045
|
+
assetPath = path.join(root, "index.html");
|
|
2046
|
+
}
|
|
2047
|
+
if (!isPathInside(root, assetPath)) {
|
|
2048
|
+
writeTextResponse(response, 403, "Forbidden.");
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
const body = await readFile(assetPath);
|
|
2052
|
+
response.writeHead(200, {
|
|
2053
|
+
"Content-Type": getDashboardAssetContentType(assetPath),
|
|
2054
|
+
"Content-Length": body.byteLength
|
|
2055
|
+
});
|
|
2056
|
+
if (request.method === "HEAD") {
|
|
2057
|
+
response.end();
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
response.end(body);
|
|
2061
|
+
}
|
|
2062
|
+
function isPathInside(root, candidate) {
|
|
2063
|
+
const relative = path.relative(root, candidate);
|
|
2064
|
+
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
2065
|
+
}
|
|
2066
|
+
function getDashboardAssetContentType(filePath) {
|
|
2067
|
+
switch (path.extname(filePath)) {
|
|
2068
|
+
case ".html": return "text/html; charset=utf-8";
|
|
2069
|
+
case ".js": return "text/javascript; charset=utf-8";
|
|
2070
|
+
case ".css": return "text/css; charset=utf-8";
|
|
2071
|
+
case ".json": return "application/json; charset=utf-8";
|
|
2072
|
+
case ".svg": return "image/svg+xml";
|
|
2073
|
+
case ".png": return "image/png";
|
|
2074
|
+
case ".ico": return "image/x-icon";
|
|
2075
|
+
case ".woff2": return "font/woff2";
|
|
2076
|
+
default: return "application/octet-stream";
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
function setStorageCorsHeaders(response) {
|
|
2080
|
+
response.setHeader("Access-Control-Allow-Origin", "*");
|
|
2081
|
+
response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
|
|
2082
|
+
response.setHeader("Access-Control-Allow-Headers", "Range");
|
|
2083
|
+
response.setHeader("Access-Control-Expose-Headers", "Accept-Ranges, Content-Disposition, Content-Length, Content-Range, Content-Type");
|
|
2084
|
+
}
|
|
2085
|
+
function writeJsonResponse(response, status, payload) {
|
|
2086
|
+
response.writeHead(status, { "content-type": "application/json" });
|
|
2087
|
+
response.end(JSON.stringify(payload));
|
|
2088
|
+
}
|
|
2089
|
+
function writeTextResponse(response, status, message) {
|
|
2090
|
+
response.writeHead(status, { "content-type": "text/plain; charset=utf-8" });
|
|
2091
|
+
response.end(message);
|
|
2092
|
+
}
|
|
2093
|
+
function parseStorageRangeHeader(header, size) {
|
|
2094
|
+
if (size === 0) {
|
|
2095
|
+
if (!header) return {
|
|
2096
|
+
start: 0,
|
|
2097
|
+
end: -1,
|
|
2098
|
+
partial: false
|
|
2099
|
+
};
|
|
2100
|
+
return {
|
|
2101
|
+
error: "Requested range is not satisfiable.",
|
|
2102
|
+
status: 416
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
if (!header) return {
|
|
2106
|
+
start: 0,
|
|
2107
|
+
end: size - 1,
|
|
2108
|
+
partial: false
|
|
2109
|
+
};
|
|
2110
|
+
if (header.includes(",")) return {
|
|
2111
|
+
error: "Multi-range requests are not supported.",
|
|
2112
|
+
status: 416
|
|
2113
|
+
};
|
|
2114
|
+
const match = /^bytes=(\d+)-(\d*)$/.exec(header);
|
|
2115
|
+
if (!match) return {
|
|
2116
|
+
error: "Only simple byte ranges are supported.",
|
|
2117
|
+
status: 416
|
|
2118
|
+
};
|
|
2119
|
+
const start = Number(match[1]);
|
|
2120
|
+
const end = match[2] ? Number(match[2]) : size - 1;
|
|
2121
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start || start >= size) return {
|
|
2122
|
+
error: "Requested range is not satisfiable.",
|
|
2123
|
+
status: 416
|
|
2124
|
+
};
|
|
2125
|
+
return {
|
|
2126
|
+
start,
|
|
2127
|
+
end: Math.min(end, size - 1),
|
|
2128
|
+
partial: true
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
function writeStorageAccessHeaders(response, ticket, range) {
|
|
2132
|
+
const headers = {
|
|
2133
|
+
"content-type": ticket.entry.contentType ?? "application/octet-stream",
|
|
2134
|
+
"content-length": range.byteLength,
|
|
2135
|
+
"accept-ranges": ticket.supportsRange ? "bytes" : "none",
|
|
2136
|
+
"content-disposition": renderStorageContentDisposition(ticket)
|
|
2137
|
+
};
|
|
2138
|
+
if (range.status === 206) headers["content-range"] = `bytes ${range.start}-${range.end}/${ticket.entry.size}`;
|
|
2139
|
+
response.writeHead(range.status, headers);
|
|
2140
|
+
}
|
|
2141
|
+
function renderStorageContentDisposition(ticket) {
|
|
2142
|
+
return `${ticket.purpose === "download" ? "attachment" : "inline"}; filename="${sanitizeHeaderFilename(ticket.entry.fileName ?? ticket.entry.id)}"; filename*=UTF-8''${encodeURIComponent(ticket.entry.fileName ?? `${ticket.entry.id}.bin`)}`;
|
|
2143
|
+
}
|
|
2144
|
+
function sanitizeHeaderFilename(value) {
|
|
2145
|
+
return value.replaceAll(/["\\\r\n]/g, "_");
|
|
2146
|
+
}
|
|
2147
|
+
async function writeResponseChunk(response, chunk) {
|
|
2148
|
+
if (response.write(chunk)) return;
|
|
2149
|
+
await new Promise((resolve) => {
|
|
2150
|
+
response.once("drain", resolve);
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
1811
2153
|
async function writeDevtoolsSessionState(cwd, state) {
|
|
1812
2154
|
const sessionPath = path.join(cwd, DEVTOOLS_SESSION_FILE);
|
|
1813
2155
|
await mkdir(path.dirname(sessionPath), { recursive: true });
|