@artenx/dshb 0.1.18 → 0.1.20
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/lib/auth/decorate-webserver.d.ts +2 -1
- package/lib/auth/decorate-webserver.js +21 -10
- package/lib/auth/decorate-webserver.js.map +1 -1
- package/lib/auth/gate.d.ts +3 -1
- package/lib/auth/gate.js +18 -1
- package/lib/auth/gate.js.map +1 -1
- package/lib/auth/index.js +11 -3
- package/lib/auth/index.js.map +1 -1
- package/lib/auth/webserver.d.ts +3 -1
- package/lib/auth/webserver.js +10 -1
- package/lib/auth/webserver.js.map +1 -1
- package/lib/client.js +2157 -567
- package/lib/client.js.map +1 -1
- package/lib/core/index.js +72 -4
- package/lib/core/index.js.map +1 -1
- package/lib/core/node-registry.d.ts +2 -108
- package/lib/core/node-registry.js +43 -2
- package/lib/core/node-registry.js.map +1 -1
- package/lib/core/routes.d.ts +4 -3
- package/lib/core/routes.js +67 -23
- package/lib/core/routes.js.map +1 -1
- package/lib/core/sync/coordinator.d.ts +48 -0
- package/lib/core/sync/coordinator.js +247 -0
- package/lib/core/sync/coordinator.js.map +1 -0
- package/lib/core/sync/host-key.d.ts +2 -0
- package/lib/core/sync/host-key.js +71 -0
- package/lib/core/sync/host-key.js.map +1 -0
- package/lib/core/sync/internal-routes.d.ts +9 -0
- package/lib/core/sync/internal-routes.js +236 -0
- package/lib/core/sync/internal-routes.js.map +1 -0
- package/lib/core/sync/model.d.ts +2 -0
- package/lib/core/sync/model.js +106 -0
- package/lib/core/sync/model.js.map +1 -0
- package/lib/core/sync/node-sync.d.ts +2 -0
- package/lib/core/sync/node-sync.js +660 -0
- package/lib/core/sync/node-sync.js.map +1 -0
- package/lib/core/sync/offline-archive.d.ts +55 -0
- package/lib/core/sync/offline-archive.js +673 -0
- package/lib/core/sync/offline-archive.js.map +1 -0
- package/lib/core/sync/offline-journal.d.ts +22 -0
- package/lib/core/sync/offline-journal.js +74 -0
- package/lib/core/sync/offline-journal.js.map +1 -0
- package/lib/core/sync/offline-routes.d.ts +7 -0
- package/lib/core/sync/offline-routes.js +192 -0
- package/lib/core/sync/offline-routes.js.map +1 -0
- package/lib/core/sync/peer-manager.d.ts +56 -0
- package/lib/core/sync/peer-manager.js +258 -0
- package/lib/core/sync/peer-manager.js.map +1 -0
- package/lib/core/sync/protocol.d.ts +2 -0
- package/lib/core/sync/protocol.js +124 -0
- package/lib/core/sync/protocol.js.map +1 -0
- package/lib/core/sync/remote-helper.d.ts +5 -0
- package/lib/core/sync/remote-helper.js +160 -0
- package/lib/core/sync/remote-helper.js.map +1 -0
- package/lib/core/sync/revision.d.ts +2 -0
- package/lib/core/sync/revision.js +77 -0
- package/lib/core/sync/revision.js.map +1 -0
- package/lib/core/sync/routes.d.ts +9 -0
- package/lib/core/sync/routes.js +339 -0
- package/lib/core/sync/routes.js.map +1 -0
- package/lib/core/sync/scheduler.d.ts +14 -0
- package/lib/core/sync/scheduler.js +81 -0
- package/lib/core/sync/scheduler.js.map +1 -0
- package/lib/core/sync/session-journal.d.ts +2 -0
- package/lib/core/sync/session-journal.js +191 -0
- package/lib/core/sync/session-journal.js.map +1 -0
- package/lib/core/sync/session-routes.d.ts +7 -0
- package/lib/core/sync/session-routes.js +67 -0
- package/lib/core/sync/session-routes.js.map +1 -0
- package/lib/core/sync/session-sync.d.ts +2 -0
- package/lib/core/sync/session-sync.js +2 -0
- package/lib/core/sync/ssh-transport.d.ts +2 -0
- package/lib/core/sync/ssh-transport.js +408 -0
- package/lib/core/sync/ssh-transport.js.map +1 -0
- package/lib/core/sync/stores.d.ts +3 -0
- package/lib/core/sync/stores.js +385 -0
- package/lib/core/sync/stores.js.map +1 -0
- package/lib/core/test.d.ts +1 -1
- package/lib/core/workspace-bindings.d.ts +2 -28
- package/lib/core/workspace-bindings.js +14 -14
- package/lib/core/workspace-bindings.js.map +1 -1
- package/lib/core/workspace-instruction-context.d.ts +3 -2260
- package/lib/core/workspace-instruction-context.js +104 -2
- package/lib/core/workspace-instruction-context.js.map +1 -0
- package/lib/core/workspace-instruction-routes.d.ts +1 -1
- package/lib/core/workspace-instructions.d.ts +2 -68
- package/lib/core/workspace-instructions.js +13 -0
- package/lib/core/workspace-instructions.js.map +1 -1
- package/lib/core/workspaces.d.ts +4 -3
- package/lib/core/workspaces.js +17 -1
- package/lib/core/workspaces.js.map +1 -1
- package/lib/host-key-C5afhAUd.d.ts +30 -0
- package/lib/index-VF8bcBeX.d.ts +2293 -0
- package/lib/{workspace-instruction-context-DytAmaPY.js → lib-gdtU_WKT.js} +159 -102
- package/lib/lib-gdtU_WKT.js.map +1 -0
- package/lib/model-BP7Gw1yk.d.ts +104 -0
- package/lib/node-registry-BTnnM5-k.d.ts +112 -0
- package/lib/node-sync-3Z9QLz2n.d.ts +89 -0
- package/lib/protocol-CV7LGE44.d.ts +35 -0
- package/lib/revision-C9sOlDZ6.d.ts +25 -0
- package/lib/router/router-fs.d.ts +12 -3
- package/lib/router/router-fs.js.map +1 -1
- package/lib/session-journal-pgPqcWn9.d.ts +40 -0
- package/lib/session-sync-EdfwUON5.js +2177 -0
- package/lib/session-sync-EdfwUON5.js.map +1 -0
- package/lib/session-sync-g6qKZzjV.d.ts +1252 -0
- package/lib/ssh-transport-BcIiLxsX.d.ts +55 -0
- package/lib/stores-CKyEt0yk.d.ts +31 -0
- package/lib/workspace-bindings-CsB0TIJB.d.ts +28 -0
- package/lib/workspace-instructions-DirYYiku.d.ts +69 -0
- package/package.json +20 -1
- package/lib/workspace-instruction-context-DytAmaPY.js.map +0 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
//#region src/core/sync/revision.ts
|
|
2
|
+
function assertTimestamp(value, label) {
|
|
3
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${label} must be a non-negative safe integer`);
|
|
4
|
+
}
|
|
5
|
+
function assertWriterId(value) {
|
|
6
|
+
if (!value || /[\u0000-\u001f\u007f]/.test(value)) throw new Error("writerInstanceId is invalid");
|
|
7
|
+
}
|
|
8
|
+
function compareSyncRevision(a, b) {
|
|
9
|
+
if (a.physicalTimeMs !== b.physicalTimeMs) return a.physicalTimeMs < b.physicalTimeMs ? -1 : 1;
|
|
10
|
+
if (a.logicalCounter !== b.logicalCounter) return a.logicalCounter < b.logicalCounter ? -1 : 1;
|
|
11
|
+
if (a.writerInstanceId === b.writerInstanceId) return 0;
|
|
12
|
+
return a.writerInstanceId < b.writerInstanceId ? -1 : 1;
|
|
13
|
+
}
|
|
14
|
+
var HybridLogicalClock = class {
|
|
15
|
+
writerInstanceId;
|
|
16
|
+
physicalTimeMs;
|
|
17
|
+
logicalCounter;
|
|
18
|
+
constructor(writerInstanceId, snapshot = {
|
|
19
|
+
physicalTimeMs: 0,
|
|
20
|
+
logicalCounter: 0
|
|
21
|
+
}) {
|
|
22
|
+
this.writerInstanceId = writerInstanceId;
|
|
23
|
+
assertWriterId(writerInstanceId);
|
|
24
|
+
assertTimestamp(snapshot.physicalTimeMs, "physicalTimeMs");
|
|
25
|
+
assertTimestamp(snapshot.logicalCounter, "logicalCounter");
|
|
26
|
+
this.physicalTimeMs = snapshot.physicalTimeMs;
|
|
27
|
+
this.logicalCounter = snapshot.logicalCounter;
|
|
28
|
+
}
|
|
29
|
+
tick(now = Date.now()) {
|
|
30
|
+
assertTimestamp(now, "now");
|
|
31
|
+
if (now > this.physicalTimeMs) {
|
|
32
|
+
this.physicalTimeMs = now;
|
|
33
|
+
this.logicalCounter = 0;
|
|
34
|
+
} else this.incrementCounter();
|
|
35
|
+
return this.current();
|
|
36
|
+
}
|
|
37
|
+
observe(remote, now = Date.now()) {
|
|
38
|
+
assertTimestamp(now, "now");
|
|
39
|
+
assertTimestamp(remote.physicalTimeMs, "remote.physicalTimeMs");
|
|
40
|
+
assertTimestamp(remote.logicalCounter, "remote.logicalCounter");
|
|
41
|
+
assertWriterId(remote.writerInstanceId);
|
|
42
|
+
const localTime = this.physicalTimeMs;
|
|
43
|
+
const maxTime = Math.max(now, localTime, remote.physicalTimeMs);
|
|
44
|
+
let logicalCounter;
|
|
45
|
+
if (maxTime === localTime && maxTime === remote.physicalTimeMs) logicalCounter = Math.max(this.logicalCounter, remote.logicalCounter) + 1;
|
|
46
|
+
else if (maxTime === localTime) {
|
|
47
|
+
if (this.logicalCounter === Number.MAX_SAFE_INTEGER) throw new Error("HLC logical counter exhausted");
|
|
48
|
+
logicalCounter = this.logicalCounter + 1;
|
|
49
|
+
} else if (maxTime === remote.physicalTimeMs) logicalCounter = remote.logicalCounter + 1;
|
|
50
|
+
else logicalCounter = 0;
|
|
51
|
+
assertTimestamp(logicalCounter, "logicalCounter");
|
|
52
|
+
this.physicalTimeMs = maxTime;
|
|
53
|
+
this.logicalCounter = logicalCounter;
|
|
54
|
+
return this.current();
|
|
55
|
+
}
|
|
56
|
+
snapshot() {
|
|
57
|
+
return {
|
|
58
|
+
physicalTimeMs: this.physicalTimeMs,
|
|
59
|
+
logicalCounter: this.logicalCounter
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
incrementCounter() {
|
|
63
|
+
if (this.logicalCounter === Number.MAX_SAFE_INTEGER) throw new Error("HLC logical counter exhausted");
|
|
64
|
+
this.logicalCounter += 1;
|
|
65
|
+
}
|
|
66
|
+
current() {
|
|
67
|
+
return {
|
|
68
|
+
physicalTimeMs: this.physicalTimeMs,
|
|
69
|
+
logicalCounter: this.logicalCounter,
|
|
70
|
+
writerInstanceId: this.writerInstanceId
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
export { HybridLogicalClock, compareSyncRevision };
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=revision.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revision.js","names":[],"sources":["../../../src/core/sync/revision.ts"],"sourcesContent":["export interface SyncRevision {\n physicalTimeMs: number\n logicalCounter: number\n writerInstanceId: string\n}\n\nexport interface HlcSnapshot {\n physicalTimeMs: number\n logicalCounter: number\n}\n\nfunction assertTimestamp(value: number, label: string): void {\n if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${label} must be a non-negative safe integer`)\n}\n\nfunction assertWriterId(value: string): void {\n if (!value || /[\\u0000-\\u001f\\u007f]/.test(value)) throw new Error('writerInstanceId is invalid')\n}\n\nexport function compareSyncRevision(a: SyncRevision, b: SyncRevision): number {\n if (a.physicalTimeMs !== b.physicalTimeMs) return a.physicalTimeMs < b.physicalTimeMs ? -1 : 1\n if (a.logicalCounter !== b.logicalCounter) return a.logicalCounter < b.logicalCounter ? -1 : 1\n if (a.writerInstanceId === b.writerInstanceId) return 0\n return a.writerInstanceId < b.writerInstanceId ? -1 : 1\n}\n\nexport class HybridLogicalClock {\n private physicalTimeMs: number\n private logicalCounter: number\n\n constructor(\n private readonly writerInstanceId: string,\n snapshot: HlcSnapshot = { physicalTimeMs: 0, logicalCounter: 0 },\n ) {\n assertWriterId(writerInstanceId)\n assertTimestamp(snapshot.physicalTimeMs, 'physicalTimeMs')\n assertTimestamp(snapshot.logicalCounter, 'logicalCounter')\n this.physicalTimeMs = snapshot.physicalTimeMs\n this.logicalCounter = snapshot.logicalCounter\n }\n\n tick(now = Date.now()): SyncRevision {\n assertTimestamp(now, 'now')\n if (now > this.physicalTimeMs) {\n this.physicalTimeMs = now\n this.logicalCounter = 0\n } else {\n this.incrementCounter()\n }\n return this.current()\n }\n\n observe(remote: SyncRevision, now = Date.now()): SyncRevision {\n assertTimestamp(now, 'now')\n assertTimestamp(remote.physicalTimeMs, 'remote.physicalTimeMs')\n assertTimestamp(remote.logicalCounter, 'remote.logicalCounter')\n assertWriterId(remote.writerInstanceId)\n\n const localTime = this.physicalTimeMs\n const maxTime = Math.max(now, localTime, remote.physicalTimeMs)\n let logicalCounter: number\n if (maxTime === localTime && maxTime === remote.physicalTimeMs) {\n logicalCounter = Math.max(this.logicalCounter, remote.logicalCounter) + 1\n } else if (maxTime === localTime) {\n if (this.logicalCounter === Number.MAX_SAFE_INTEGER) throw new Error('HLC logical counter exhausted')\n logicalCounter = this.logicalCounter + 1\n } else if (maxTime === remote.physicalTimeMs) {\n logicalCounter = remote.logicalCounter + 1\n } else {\n logicalCounter = 0\n }\n assertTimestamp(logicalCounter, 'logicalCounter')\n this.physicalTimeMs = maxTime\n this.logicalCounter = logicalCounter\n return this.current()\n }\n\n snapshot(): HlcSnapshot {\n return { physicalTimeMs: this.physicalTimeMs, logicalCounter: this.logicalCounter }\n }\n\n private incrementCounter(): void {\n if (this.logicalCounter === Number.MAX_SAFE_INTEGER) throw new Error('HLC logical counter exhausted')\n this.logicalCounter += 1\n }\n\n private current(): SyncRevision {\n return {\n physicalTimeMs: this.physicalTimeMs,\n logicalCounter: this.logicalCounter,\n writerInstanceId: this.writerInstanceId,\n }\n }\n}\n"],"mappings":";AAWA,SAAS,gBAAgB,OAAe,OAAqB;CAC3D,IAAI,CAAC,OAAO,cAAc,KAAK,KAAK,QAAQ,GAAG,MAAM,IAAI,MAAM,GAAG,MAAM,qCAAqC;AAC/G;AAEA,SAAS,eAAe,OAAqB;CAC3C,IAAI,CAAC,SAAS,wBAAwB,KAAK,KAAK,GAAG,MAAM,IAAI,MAAM,6BAA6B;AAClG;AAEA,SAAgB,oBAAoB,GAAiB,GAAyB;CAC5E,IAAI,EAAE,mBAAmB,EAAE,gBAAgB,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,KAAK;CAC7F,IAAI,EAAE,mBAAmB,EAAE,gBAAgB,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,KAAK;CAC7F,IAAI,EAAE,qBAAqB,EAAE,kBAAkB,OAAO;CACtD,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,KAAK;AACxD;AAEA,IAAa,qBAAb,MAAgC;CAKX;CAJnB;CACA;CAEA,YACE,kBACA,WAAwB;EAAE,gBAAgB;EAAG,gBAAgB;CAAE,GAC/D;EAFiB,KAAA,mBAAA;EAGjB,eAAe,gBAAgB;EAC/B,gBAAgB,SAAS,gBAAgB,gBAAgB;EACzD,gBAAgB,SAAS,gBAAgB,gBAAgB;EACzD,KAAK,iBAAiB,SAAS;EAC/B,KAAK,iBAAiB,SAAS;CACjC;CAEA,KAAK,MAAM,KAAK,IAAI,GAAiB;EACnC,gBAAgB,KAAK,KAAK;EAC1B,IAAI,MAAM,KAAK,gBAAgB;GAC7B,KAAK,iBAAiB;GACtB,KAAK,iBAAiB;EACxB,OACE,KAAK,iBAAiB;EAExB,OAAO,KAAK,QAAQ;CACtB;CAEA,QAAQ,QAAsB,MAAM,KAAK,IAAI,GAAiB;EAC5D,gBAAgB,KAAK,KAAK;EAC1B,gBAAgB,OAAO,gBAAgB,uBAAuB;EAC9D,gBAAgB,OAAO,gBAAgB,uBAAuB;EAC9D,eAAe,OAAO,gBAAgB;EAEtC,MAAM,YAAY,KAAK;EACvB,MAAM,UAAU,KAAK,IAAI,KAAK,WAAW,OAAO,cAAc;EAC9D,IAAI;EACJ,IAAI,YAAY,aAAa,YAAY,OAAO,gBAC9C,iBAAiB,KAAK,IAAI,KAAK,gBAAgB,OAAO,cAAc,IAAI;OACnE,IAAI,YAAY,WAAW;GAChC,IAAI,KAAK,mBAAmB,OAAO,kBAAkB,MAAM,IAAI,MAAM,+BAA+B;GACpG,iBAAiB,KAAK,iBAAiB;EACzC,OAAO,IAAI,YAAY,OAAO,gBAC5B,iBAAiB,OAAO,iBAAiB;OAEzC,iBAAiB;EAEnB,gBAAgB,gBAAgB,gBAAgB;EAChD,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,OAAO,KAAK,QAAQ;CACtB;CAEA,WAAwB;EACtB,OAAO;GAAE,gBAAgB,KAAK;GAAgB,gBAAgB,KAAK;EAAe;CACpF;CAEA,mBAAiC;EAC/B,IAAI,KAAK,mBAAmB,OAAO,kBAAkB,MAAM,IAAI,MAAM,+BAA+B;EACpG,KAAK,kBAAkB;CACzB;CAEA,UAAgC;EAC9B,OAAO;GACL,gBAAgB,KAAK;GACrB,gBAAgB,KAAK;GACrB,kBAAkB,KAAK;EACzB;CACF;AACF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SyncPeerManager } from "./peer-manager.js";
|
|
2
|
+
import { a as SyncSshTransport } from "../../ssh-transport-BcIiLxsX.js";
|
|
3
|
+
import { SyncCoordinator } from "./coordinator.js";
|
|
4
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
5
|
+
//#region src/core/sync/routes.d.ts
|
|
6
|
+
declare function registerSyncPeerRoutes(ctx: Context, manager: SyncPeerManager, transport?: SyncSshTransport, coordinator?: SyncCoordinator): void;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { registerSyncPeerRoutes };
|
|
9
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { SyncPeerInputError, SyncPeerNotFoundError } from "./peer-manager.js";
|
|
2
|
+
import { SyncHostKeyChallengeError, SyncHostKeyChallengeExpiredError } from "./host-key.js";
|
|
3
|
+
import { SyncSshProbeError } from "./ssh-transport.js";
|
|
4
|
+
import { SyncCoordinatorError } from "./coordinator.js";
|
|
5
|
+
//#region src/core/sync/routes.ts
|
|
6
|
+
const MAX_BODY_BYTES = 131072;
|
|
7
|
+
function sendJson(res, status, body) {
|
|
8
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
9
|
+
res.end(JSON.stringify(body));
|
|
10
|
+
}
|
|
11
|
+
function readBody(req) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
if (req.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") {
|
|
14
|
+
resolve(null);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const chunks = [];
|
|
18
|
+
let size = 0;
|
|
19
|
+
let settled = false;
|
|
20
|
+
const fail = () => {
|
|
21
|
+
if (settled) return;
|
|
22
|
+
settled = true;
|
|
23
|
+
resolve(null);
|
|
24
|
+
};
|
|
25
|
+
req.on("data", (chunk) => {
|
|
26
|
+
if (settled) return;
|
|
27
|
+
size += chunk.length;
|
|
28
|
+
if (size > MAX_BODY_BYTES) {
|
|
29
|
+
fail();
|
|
30
|
+
req.destroy();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
chunks.push(chunk);
|
|
34
|
+
});
|
|
35
|
+
req.on("end", () => {
|
|
36
|
+
if (settled) return;
|
|
37
|
+
try {
|
|
38
|
+
const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
39
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
40
|
+
fail();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
settled = true;
|
|
44
|
+
resolve(value);
|
|
45
|
+
} catch {
|
|
46
|
+
fail();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
req.on("error", fail);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function pathSegments(req) {
|
|
53
|
+
return new URL(req.url ?? "/", "http://x").pathname.split("/").filter(Boolean);
|
|
54
|
+
}
|
|
55
|
+
function splitBody(body) {
|
|
56
|
+
const { secrets, ...fields } = body;
|
|
57
|
+
return {
|
|
58
|
+
fields,
|
|
59
|
+
secrets
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function sendFailure(res, error) {
|
|
63
|
+
if (error instanceof SyncPeerInputError) {
|
|
64
|
+
sendJson(res, 400, {
|
|
65
|
+
ok: false,
|
|
66
|
+
error: error.message
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (error instanceof SyncPeerNotFoundError) {
|
|
71
|
+
sendJson(res, 404, {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: "sync peer not found"
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
sendJson(res, 500, {
|
|
78
|
+
ok: false,
|
|
79
|
+
error: "sync peer operation failed"
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function sendProbeFailure(res, error) {
|
|
83
|
+
if (error instanceof SyncHostKeyChallengeError) {
|
|
84
|
+
sendJson(res, 409, {
|
|
85
|
+
ok: false,
|
|
86
|
+
code: `host-key-${error.decision}`,
|
|
87
|
+
fingerprint: error.fingerprint,
|
|
88
|
+
...error.expected ? { expected: error.expected } : {}
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (error instanceof SyncHostKeyChallengeExpiredError) {
|
|
93
|
+
sendJson(res, 409, {
|
|
94
|
+
ok: false,
|
|
95
|
+
code: "host-key-challenge-expired",
|
|
96
|
+
error: error.message
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (error instanceof SyncPeerNotFoundError) {
|
|
101
|
+
sendJson(res, 404, {
|
|
102
|
+
ok: false,
|
|
103
|
+
error: "sync peer not found"
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (error instanceof SyncSshProbeError) {
|
|
108
|
+
sendJson(res, error.category === "auth" ? 502 : error.category === "protocol" || error.category === "hostkey" ? 409 : 503, {
|
|
109
|
+
ok: false,
|
|
110
|
+
code: `ssh-${error.category}`,
|
|
111
|
+
error: error.message
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
sendJson(res, 500, {
|
|
116
|
+
ok: false,
|
|
117
|
+
error: "sync peer probe failed"
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function sendRunFailure(res, error) {
|
|
121
|
+
if (error instanceof SyncCoordinatorError) {
|
|
122
|
+
sendJson(res, error.code === "peer-not-found" ? 404 : error.code === "peer-disabled" || error.code === "sessions-unavailable" || error.code === "revision-conflict" ? 409 : error.code === "session-bundle-too-large" || error.code === "session-too-large" ? 413 : error.code === "session-workspace-unmapped" || error.code === "session-format" || error.code === "session-revision-conflict" || error.code === "session-same-instance" ? 409 : error.code.startsWith("ssh-") ? 502 : 500, {
|
|
123
|
+
ok: false,
|
|
124
|
+
code: error.code,
|
|
125
|
+
error: error.message
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
sendJson(res, 500, {
|
|
130
|
+
ok: false,
|
|
131
|
+
error: "sync run failed"
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function registerSyncPeerRoutes(ctx, manager, transport, coordinator) {
|
|
135
|
+
ctx.effect(() => ctx.webServer.register({
|
|
136
|
+
kind: "prefix",
|
|
137
|
+
path: "/api/dshb/sync/peers",
|
|
138
|
+
handler: async (req, res) => {
|
|
139
|
+
const segments = pathSegments(req);
|
|
140
|
+
const peerId = segments[4];
|
|
141
|
+
const action = segments[5];
|
|
142
|
+
if (segments.length > 6) {
|
|
143
|
+
sendJson(res, 404, {
|
|
144
|
+
ok: false,
|
|
145
|
+
error: "not found"
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (req.method === "GET" && peerId === void 0) {
|
|
150
|
+
try {
|
|
151
|
+
sendJson(res, 200, {
|
|
152
|
+
ok: true,
|
|
153
|
+
peers: await manager.list()
|
|
154
|
+
});
|
|
155
|
+
} catch {
|
|
156
|
+
sendJson(res, 500, {
|
|
157
|
+
ok: false,
|
|
158
|
+
error: "sync peer list unavailable"
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (req.method === "POST" && peerId === void 0) {
|
|
164
|
+
const body = await readBody(req);
|
|
165
|
+
if (!body) {
|
|
166
|
+
sendJson(res, 400, {
|
|
167
|
+
ok: false,
|
|
168
|
+
error: "request body must be JSON"
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const { fields, secrets } = splitBody(body);
|
|
173
|
+
try {
|
|
174
|
+
sendJson(res, 201, {
|
|
175
|
+
ok: true,
|
|
176
|
+
peer: await manager.create(fields, secrets)
|
|
177
|
+
});
|
|
178
|
+
} catch (error) {
|
|
179
|
+
sendFailure(res, error);
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!peerId) {
|
|
184
|
+
sendJson(res, 405, {
|
|
185
|
+
ok: false,
|
|
186
|
+
error: "method not allowed"
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (action !== void 0) {
|
|
191
|
+
if (segments.length !== 6 || req.method !== "POST") {
|
|
192
|
+
sendJson(res, 405, {
|
|
193
|
+
ok: false,
|
|
194
|
+
error: "method not allowed"
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (!transport) {
|
|
199
|
+
sendJson(res, 503, {
|
|
200
|
+
ok: false,
|
|
201
|
+
error: "SSH sync transport unavailable"
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (action === "probe") {
|
|
206
|
+
try {
|
|
207
|
+
sendJson(res, 200, {
|
|
208
|
+
ok: true,
|
|
209
|
+
handshake: await transport.probe(peerId)
|
|
210
|
+
});
|
|
211
|
+
} catch (error) {
|
|
212
|
+
sendProbeFailure(res, error);
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (action === "trust-host-key") {
|
|
217
|
+
try {
|
|
218
|
+
await transport.trustObservedHostKey(peerId);
|
|
219
|
+
sendJson(res, 200, {
|
|
220
|
+
ok: true,
|
|
221
|
+
peer: await manager.get(peerId)
|
|
222
|
+
});
|
|
223
|
+
} catch (error) {
|
|
224
|
+
sendProbeFailure(res, error);
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (action === "run") {
|
|
229
|
+
if (!coordinator) {
|
|
230
|
+
sendJson(res, 503, {
|
|
231
|
+
ok: false,
|
|
232
|
+
error: "sync coordinator unavailable"
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const peer = await manager.get(peerId);
|
|
238
|
+
if (!peer) {
|
|
239
|
+
sendJson(res, 404, {
|
|
240
|
+
ok: false,
|
|
241
|
+
error: "sync peer not found"
|
|
242
|
+
});
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (!peer.enabled) {
|
|
246
|
+
sendJson(res, 409, {
|
|
247
|
+
ok: false,
|
|
248
|
+
code: "peer-disabled",
|
|
249
|
+
error: "此同步节点已停用"
|
|
250
|
+
});
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
coordinator.run(peerId).catch(() => void 0);
|
|
254
|
+
sendJson(res, 202, {
|
|
255
|
+
ok: true,
|
|
256
|
+
accepted: true
|
|
257
|
+
});
|
|
258
|
+
} catch (error) {
|
|
259
|
+
sendRunFailure(res, error);
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
sendJson(res, 404, {
|
|
264
|
+
ok: false,
|
|
265
|
+
error: "not found"
|
|
266
|
+
});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (req.method === "GET") {
|
|
270
|
+
try {
|
|
271
|
+
const peer = await manager.get(peerId);
|
|
272
|
+
if (!peer) {
|
|
273
|
+
sendJson(res, 404, {
|
|
274
|
+
ok: false,
|
|
275
|
+
error: "sync peer not found"
|
|
276
|
+
});
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
sendJson(res, 200, {
|
|
280
|
+
ok: true,
|
|
281
|
+
peer
|
|
282
|
+
});
|
|
283
|
+
} catch {
|
|
284
|
+
sendJson(res, 500, {
|
|
285
|
+
ok: false,
|
|
286
|
+
error: "sync peer lookup failed"
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (req.method === "PATCH") {
|
|
292
|
+
const body = await readBody(req);
|
|
293
|
+
if (!body) {
|
|
294
|
+
sendJson(res, 400, {
|
|
295
|
+
ok: false,
|
|
296
|
+
error: "request body must be JSON"
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const { fields, secrets } = splitBody(body);
|
|
301
|
+
try {
|
|
302
|
+
sendJson(res, 200, {
|
|
303
|
+
ok: true,
|
|
304
|
+
peer: await manager.update(peerId, fields, secrets)
|
|
305
|
+
});
|
|
306
|
+
} catch (error) {
|
|
307
|
+
sendFailure(res, error);
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (req.method === "DELETE") {
|
|
312
|
+
try {
|
|
313
|
+
if (!await manager.remove(peerId)) {
|
|
314
|
+
sendJson(res, 404, {
|
|
315
|
+
ok: false,
|
|
316
|
+
error: "sync peer not found"
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
sendJson(res, 200, { ok: true });
|
|
321
|
+
} catch {
|
|
322
|
+
sendJson(res, 500, {
|
|
323
|
+
ok: false,
|
|
324
|
+
error: "sync peer removal failed"
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
sendJson(res, 405, {
|
|
330
|
+
ok: false,
|
|
331
|
+
error: "method not allowed"
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
//#endregion
|
|
337
|
+
export { registerSyncPeerRoutes };
|
|
338
|
+
|
|
339
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","names":[],"sources":["../../../src/core/sync/routes.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport { SyncHostKeyChallengeError, SyncHostKeyChallengeExpiredError } from './host-key.js'\nimport { SyncPeerInputError, SyncPeerManager, SyncPeerNotFoundError } from './peer-manager.js'\nimport { SyncSshProbeError, SyncSshTransport } from './ssh-transport.js'\nimport { SyncCoordinator, SyncCoordinatorError } from './coordinator.js'\n\nconst MAX_BODY_BYTES = 128 * 1024\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { 'content-type': 'application/json' })\n res.end(JSON.stringify(body))\n}\n\nfunction readBody(req: IncomingMessage): Promise<Record<string, unknown> | null> {\n return new Promise((resolve) => {\n const contentType = req.headers['content-type']?.split(';', 1)[0]?.trim().toLowerCase()\n if (contentType !== 'application/json') {\n resolve(null)\n return\n }\n\n const chunks: Buffer[] = []\n let size = 0\n let settled = false\n const fail = (): void => {\n if (settled) return\n settled = true\n resolve(null)\n }\n req.on('data', (chunk: Buffer) => {\n if (settled) return\n size += chunk.length\n if (size > MAX_BODY_BYTES) {\n fail()\n req.destroy()\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => {\n if (settled) return\n try {\n const value: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8'))\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n fail()\n return\n }\n settled = true\n resolve(value as Record<string, unknown>)\n } catch {\n fail()\n }\n })\n req.on('error', fail)\n })\n}\n\nfunction pathSegments(req: IncomingMessage): string[] {\n return new URL(req.url ?? '/', 'http://x').pathname.split('/').filter(Boolean)\n}\n\nfunction splitBody(body: Record<string, unknown>): { fields: Record<string, unknown>; secrets: unknown } {\n const { secrets, ...fields } = body\n return { fields, secrets }\n}\n\nfunction sendFailure(res: ServerResponse, error: unknown): void {\n if (error instanceof SyncPeerInputError) {\n sendJson(res, 400, { ok: false, error: error.message })\n return\n }\n if (error instanceof SyncPeerNotFoundError) {\n sendJson(res, 404, { ok: false, error: 'sync peer not found' })\n return\n }\n sendJson(res, 500, { ok: false, error: 'sync peer operation failed' })\n}\n\nfunction sendProbeFailure(res: ServerResponse, error: unknown): void {\n if (error instanceof SyncHostKeyChallengeError) {\n sendJson(res, 409, {\n ok: false,\n code: `host-key-${error.decision}`,\n fingerprint: error.fingerprint,\n ...(error.expected ? { expected: error.expected } : {}),\n })\n return\n }\n if (error instanceof SyncHostKeyChallengeExpiredError) {\n sendJson(res, 409, { ok: false, code: 'host-key-challenge-expired', error: error.message })\n return\n }\n if (error instanceof SyncPeerNotFoundError) {\n sendJson(res, 404, { ok: false, error: 'sync peer not found' })\n return\n }\n if (error instanceof SyncSshProbeError) {\n const status = error.category === 'auth' ? 502 : error.category === 'protocol' || error.category === 'hostkey' ? 409 : 503\n sendJson(res, status, { ok: false, code: `ssh-${error.category}`, error: error.message })\n return\n }\n sendJson(res, 500, { ok: false, error: 'sync peer probe failed' })\n}\n\nfunction sendRunFailure(res: ServerResponse, error: unknown): void {\n if (error instanceof SyncCoordinatorError) {\n const status = error.code === 'peer-not-found' ? 404\n : error.code === 'peer-disabled' || error.code === 'sessions-unavailable' || error.code === 'revision-conflict' ? 409\n : error.code === 'session-bundle-too-large' || error.code === 'session-too-large' ? 413\n : error.code === 'session-workspace-unmapped' || error.code === 'session-format' || error.code === 'session-revision-conflict' || error.code === 'session-same-instance' ? 409\n : error.code.startsWith('ssh-') ? 502 : 500\n sendJson(res, status, { ok: false, code: error.code, error: error.message })\n return\n }\n sendJson(res, 500, { ok: false, error: 'sync run failed' })\n}\n\nexport function registerSyncPeerRoutes(\n ctx: Context,\n manager: SyncPeerManager,\n transport?: SyncSshTransport,\n coordinator?: SyncCoordinator,\n): void {\n ctx.effect(() =>\n ctx.webServer.register({\n kind: 'prefix',\n path: '/api/dshb/sync/peers',\n handler: async (req, res) => {\n const segments = pathSegments(req)\n const peerId = segments[4]\n const action = segments[5]\n if (segments.length > 6) {\n sendJson(res, 404, { ok: false, error: 'not found' })\n return\n }\n\n if (req.method === 'GET' && peerId === undefined) {\n try {\n sendJson(res, 200, { ok: true, peers: await manager.list() })\n } catch {\n sendJson(res, 500, { ok: false, error: 'sync peer list unavailable' })\n }\n return\n }\n\n if (req.method === 'POST' && peerId === undefined) {\n const body = await readBody(req)\n if (!body) {\n sendJson(res, 400, { ok: false, error: 'request body must be JSON' })\n return\n }\n const { fields, secrets } = splitBody(body)\n try {\n sendJson(res, 201, { ok: true, peer: await manager.create(fields, secrets) })\n } catch (error) {\n sendFailure(res, error)\n }\n return\n }\n\n if (!peerId) {\n sendJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n\n if (action !== undefined) {\n if (segments.length !== 6 || req.method !== 'POST') {\n sendJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n if (!transport) {\n sendJson(res, 503, { ok: false, error: 'SSH sync transport unavailable' })\n return\n }\n if (action === 'probe') {\n try {\n const handshake = await transport.probe(peerId)\n sendJson(res, 200, { ok: true, handshake })\n } catch (error) {\n sendProbeFailure(res, error)\n }\n return\n }\n if (action === 'trust-host-key') {\n try {\n await transport.trustObservedHostKey(peerId)\n const peer = await manager.get(peerId)\n sendJson(res, 200, { ok: true, peer })\n } catch (error) {\n sendProbeFailure(res, error)\n }\n return\n }\n if (action === 'run') {\n if (!coordinator) {\n sendJson(res, 503, { ok: false, error: 'sync coordinator unavailable' })\n return\n }\n try {\n const peer = await manager.get(peerId)\n if (!peer) {\n sendJson(res, 404, { ok: false, error: 'sync peer not found' })\n return\n }\n if (!peer.enabled) {\n sendJson(res, 409, { ok: false, code: 'peer-disabled', error: '此同步节点已停用' })\n return\n }\n void coordinator.run(peerId).catch(() => undefined)\n sendJson(res, 202, { ok: true, accepted: true })\n } catch (error) {\n sendRunFailure(res, error)\n }\n return\n }\n sendJson(res, 404, { ok: false, error: 'not found' })\n return\n }\n\n if (req.method === 'GET') {\n try {\n const peer = await manager.get(peerId)\n if (!peer) {\n sendJson(res, 404, { ok: false, error: 'sync peer not found' })\n return\n }\n sendJson(res, 200, { ok: true, peer })\n } catch {\n sendJson(res, 500, { ok: false, error: 'sync peer lookup failed' })\n }\n return\n }\n\n if (req.method === 'PATCH') {\n const body = await readBody(req)\n if (!body) {\n sendJson(res, 400, { ok: false, error: 'request body must be JSON' })\n return\n }\n const { fields, secrets } = splitBody(body)\n try {\n sendJson(res, 200, { ok: true, peer: await manager.update(peerId, fields, secrets) })\n } catch (error) {\n sendFailure(res, error)\n }\n return\n }\n\n if (req.method === 'DELETE') {\n try {\n if (!(await manager.remove(peerId))) {\n sendJson(res, 404, { ok: false, error: 'sync peer not found' })\n return\n }\n sendJson(res, 200, { ok: true })\n } catch {\n sendJson(res, 500, { ok: false, error: 'sync peer removal failed' })\n }\n return\n }\n\n sendJson(res, 405, { ok: false, error: 'method not allowed' })\n },\n }),\n )\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SyncPeerManager } from "./peer-manager.js";
|
|
2
|
+
import { SyncCoordinator } from "./coordinator.js";
|
|
3
|
+
//#region src/core/sync/scheduler.d.ts
|
|
4
|
+
declare const SYNC_SCHEDULE_INTERVAL_MS: number;
|
|
5
|
+
declare class SyncScheduler {
|
|
6
|
+
private readonly peers;
|
|
7
|
+
private readonly coordinator;
|
|
8
|
+
private readonly intervalMs;
|
|
9
|
+
constructor(peers: SyncPeerManager, coordinator: SyncCoordinator, intervalMs?: number);
|
|
10
|
+
start(): () => void;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { SYNC_SCHEDULE_INTERVAL_MS, SyncScheduler };
|
|
14
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//#region src/core/sync/scheduler.ts
|
|
2
|
+
const SYNC_SCHEDULE_INTERVAL_MS = 3e5;
|
|
3
|
+
var SyncScheduler = class {
|
|
4
|
+
peers;
|
|
5
|
+
coordinator;
|
|
6
|
+
intervalMs;
|
|
7
|
+
constructor(peers, coordinator, intervalMs = SYNC_SCHEDULE_INTERVAL_MS) {
|
|
8
|
+
this.peers = peers;
|
|
9
|
+
this.coordinator = coordinator;
|
|
10
|
+
this.intervalMs = intervalMs;
|
|
11
|
+
}
|
|
12
|
+
start() {
|
|
13
|
+
const timers = /* @__PURE__ */ new Map();
|
|
14
|
+
let stopped = false;
|
|
15
|
+
let generation = 0;
|
|
16
|
+
let refreshNeeded = false;
|
|
17
|
+
let refreshing;
|
|
18
|
+
const clearTimer = (peerId) => {
|
|
19
|
+
const timer = timers.get(peerId);
|
|
20
|
+
if (!timer) return;
|
|
21
|
+
clearInterval(timer);
|
|
22
|
+
timers.delete(peerId);
|
|
23
|
+
};
|
|
24
|
+
const configure = (peer) => {
|
|
25
|
+
if (!peer.enabled || peer.schedule !== "every-5m") {
|
|
26
|
+
clearTimer(peer.id);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (timers.has(peer.id)) return;
|
|
30
|
+
const timer = setInterval(() => {
|
|
31
|
+
this.coordinator.run(peer.id).catch(() => {});
|
|
32
|
+
}, this.intervalMs);
|
|
33
|
+
timers.set(peer.id, timer);
|
|
34
|
+
};
|
|
35
|
+
const refresh = () => {
|
|
36
|
+
if (stopped) return;
|
|
37
|
+
refreshNeeded = true;
|
|
38
|
+
if (refreshing) return;
|
|
39
|
+
refreshing = (async () => {
|
|
40
|
+
while (refreshNeeded && !stopped) {
|
|
41
|
+
refreshNeeded = false;
|
|
42
|
+
const observedGeneration = generation;
|
|
43
|
+
let peers;
|
|
44
|
+
try {
|
|
45
|
+
peers = await this.peers.list();
|
|
46
|
+
} catch {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
if (observedGeneration !== generation) {
|
|
50
|
+
refreshNeeded = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const liveIds = new Set(peers.map((peer) => peer.id));
|
|
54
|
+
for (const peerId of timers.keys()) if (!liveIds.has(peerId)) clearTimer(peerId);
|
|
55
|
+
for (const peer of peers) configure(peer);
|
|
56
|
+
}
|
|
57
|
+
})().finally(() => {
|
|
58
|
+
refreshing = void 0;
|
|
59
|
+
if (refreshNeeded && !stopped) refresh();
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
const unsubscribe = this.peers.subscribe((peerId, peer) => {
|
|
63
|
+
generation += 1;
|
|
64
|
+
if (peer) configure(peer);
|
|
65
|
+
else clearTimer(peerId);
|
|
66
|
+
refresh();
|
|
67
|
+
});
|
|
68
|
+
refresh();
|
|
69
|
+
return () => {
|
|
70
|
+
if (stopped) return;
|
|
71
|
+
stopped = true;
|
|
72
|
+
unsubscribe();
|
|
73
|
+
for (const timer of timers.values()) clearInterval(timer);
|
|
74
|
+
timers.clear();
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
//#endregion
|
|
79
|
+
export { SYNC_SCHEDULE_INTERVAL_MS, SyncScheduler };
|
|
80
|
+
|
|
81
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.js","names":[],"sources":["../../../src/core/sync/scheduler.ts"],"sourcesContent":["import { SyncCoordinator } from './coordinator.js'\nimport { SyncPeerManager, type SyncPeerView } from './peer-manager.js'\nimport type { SyncPeer } from './model.js'\n\nexport const SYNC_SCHEDULE_INTERVAL_MS = 5 * 60 * 1000\n\nexport class SyncScheduler {\n constructor(\n private readonly peers: SyncPeerManager,\n private readonly coordinator: SyncCoordinator,\n private readonly intervalMs = SYNC_SCHEDULE_INTERVAL_MS,\n ) {}\n\n start(): () => void {\n const timers = new Map<string, ReturnType<typeof setInterval>>()\n let stopped = false\n let generation = 0\n let refreshNeeded = false\n let refreshing: Promise<void> | undefined\n\n const clearTimer = (peerId: string): void => {\n const timer = timers.get(peerId)\n if (!timer) return\n clearInterval(timer)\n timers.delete(peerId)\n }\n\n const configure = (peer: Pick<SyncPeer, 'id' | 'enabled' | 'schedule'>): void => {\n if (!peer.enabled || peer.schedule !== 'every-5m') {\n clearTimer(peer.id)\n return\n }\n if (timers.has(peer.id)) return\n const timer = setInterval(() => {\n void this.coordinator.run(peer.id).catch(() => {})\n }, this.intervalMs)\n timers.set(peer.id, timer)\n }\n\n const refresh = (): void => {\n if (stopped) return\n refreshNeeded = true\n if (refreshing) return\n refreshing = (async () => {\n while (refreshNeeded && !stopped) {\n refreshNeeded = false\n const observedGeneration = generation\n let peers: SyncPeerView[]\n try {\n peers = await this.peers.list()\n } catch {\n break\n }\n if (observedGeneration !== generation) {\n refreshNeeded = true\n continue\n }\n const liveIds = new Set(peers.map((peer) => peer.id))\n for (const peerId of timers.keys()) {\n if (!liveIds.has(peerId)) clearTimer(peerId)\n }\n for (const peer of peers) configure(peer)\n }\n })().finally(() => {\n refreshing = undefined\n if (refreshNeeded && !stopped) refresh()\n })\n }\n\n const unsubscribe = this.peers.subscribe((peerId, peer) => {\n generation += 1\n if (peer) configure(peer)\n else clearTimer(peerId)\n refresh()\n })\n refresh()\n\n return () => {\n if (stopped) return\n stopped = true\n unsubscribe()\n for (const timer of timers.values()) clearInterval(timer)\n timers.clear()\n }\n }\n}\n"],"mappings":";AAIA,MAAa,4BAA4B;AAEzC,IAAa,gBAAb,MAA2B;CAEN;CACA;CACA;CAHnB,YACE,OACA,aACA,aAA8B,2BAC9B;EAHiB,KAAA,QAAA;EACA,KAAA,cAAA;EACA,KAAA,aAAA;CAChB;CAEH,QAAoB;EAClB,MAAM,yBAAS,IAAI,IAA4C;EAC/D,IAAI,UAAU;EACd,IAAI,aAAa;EACjB,IAAI,gBAAgB;EACpB,IAAI;EAEJ,MAAM,cAAc,WAAyB;GAC3C,MAAM,QAAQ,OAAO,IAAI,MAAM;GAC/B,IAAI,CAAC,OAAO;GACZ,cAAc,KAAK;GACnB,OAAO,OAAO,MAAM;EACtB;EAEA,MAAM,aAAa,SAA8D;GAC/E,IAAI,CAAC,KAAK,WAAW,KAAK,aAAa,YAAY;IACjD,WAAW,KAAK,EAAE;IAClB;GACF;GACA,IAAI,OAAO,IAAI,KAAK,EAAE,GAAG;GACzB,MAAM,QAAQ,kBAAkB;IAC9B,KAAU,YAAY,IAAI,KAAK,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC;GACnD,GAAG,KAAK,UAAU;GAClB,OAAO,IAAI,KAAK,IAAI,KAAK;EAC3B;EAEA,MAAM,gBAAsB;GAC1B,IAAI,SAAS;GACb,gBAAgB;GAChB,IAAI,YAAY;GAChB,cAAc,YAAY;IACxB,OAAO,iBAAiB,CAAC,SAAS;KAChC,gBAAgB;KAChB,MAAM,qBAAqB;KAC3B,IAAI;KACJ,IAAI;MACF,QAAQ,MAAM,KAAK,MAAM,KAAK;KAChC,QAAQ;MACN;KACF;KACA,IAAI,uBAAuB,YAAY;MACrC,gBAAgB;MAChB;KACF;KACA,MAAM,UAAU,IAAI,IAAI,MAAM,KAAK,SAAS,KAAK,EAAE,CAAC;KACpD,KAAK,MAAM,UAAU,OAAO,KAAK,GAC/B,IAAI,CAAC,QAAQ,IAAI,MAAM,GAAG,WAAW,MAAM;KAE7C,KAAK,MAAM,QAAQ,OAAO,UAAU,IAAI;IAC1C;GACF,EAAA,CAAG,CAAC,CAAC,cAAc;IACjB,aAAa,KAAA;IACb,IAAI,iBAAiB,CAAC,SAAS,QAAQ;GACzC,CAAC;EACH;EAEA,MAAM,cAAc,KAAK,MAAM,WAAW,QAAQ,SAAS;GACzD,cAAc;GACd,IAAI,MAAM,UAAU,IAAI;QACnB,WAAW,MAAM;GACtB,QAAQ;EACV,CAAC;EACD,QAAQ;EAER,aAAa;GACX,IAAI,SAAS;GACb,UAAU;GACV,YAAY;GACZ,KAAK,MAAM,SAAS,OAAO,OAAO,GAAG,cAAc,KAAK;GACxD,OAAO,MAAM;EACf;CACF;AACF"}
|