@artenx/dshb 0.1.30 → 0.1.31
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/core/index.js +1 -1
- package/lib/core/sync/coordinator.d.ts +2 -2
- package/lib/core/sync/coordinator.js +90 -45
- package/lib/core/sync/coordinator.js.map +1 -1
- package/lib/core/sync/internal-routes.d.ts +1 -1
- package/lib/core/sync/internal-routes.js +3 -3
- package/lib/core/sync/internal-routes.js.map +1 -1
- package/lib/core/sync/offline-archive.d.ts +1 -1
- package/lib/core/sync/offline-archive.js +1 -1
- package/lib/core/sync/offline-routes.js +1 -1
- package/lib/core/sync/protocol.js +1 -1
- package/lib/core/sync/protocol.js.map +1 -1
- package/lib/core/sync/routes.d.ts +1 -1
- package/lib/core/sync/session-routes.d.ts +1 -1
- package/lib/core/sync/session-routes.js +1 -1
- package/lib/core/sync/session-sync.d.ts +2 -2
- package/lib/core/sync/session-sync.js +2 -2
- package/lib/core/sync/ssh-transport.d.ts +1 -1
- package/lib/core/sync/ssh-transport.js +4 -4
- package/lib/core/sync/ssh-transport.js.map +1 -1
- package/lib/{session-sync-C0uoizyB.js → session-sync-BQt3HXkp.js} +100 -33
- package/lib/session-sync-BQt3HXkp.js.map +1 -0
- package/lib/{session-sync-C2j2rk9K.d.ts → session-sync-Zfo1Q-qT.d.ts} +12 -2
- package/lib/{ssh-transport-uK9Ep0yp.d.ts → ssh-transport-Cfb_nwkY.d.ts} +2 -2
- package/package.json +1 -1
- package/lib/session-sync-C0uoizyB.js.map +0 -1
package/lib/core/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { registerWorkspaceInstructionRoutes } from "./workspace-instruction-rout
|
|
|
11
11
|
import { WorkspaceBindingsStore } from "./workspace-bindings.js";
|
|
12
12
|
import { registerWorkspaceInstructionInjector } from "./workspace-instruction-context.js";
|
|
13
13
|
import { SyncPeerManager } from "./sync/peer-manager.js";
|
|
14
|
-
import {
|
|
14
|
+
import { r as SyncSessionService } from "../session-sync-BQt3HXkp.js";
|
|
15
15
|
import { registerSyncInternalRoutes } from "./sync/internal-routes.js";
|
|
16
16
|
import { SyncSshTransport } from "./sync/ssh-transport.js";
|
|
17
17
|
import { SyncCoordinator } from "./sync/coordinator.js";
|
|
@@ -2,8 +2,8 @@ import { a as SyncPeer } from "../../model-BP7Gw1yk.js";
|
|
|
2
2
|
import { n as SyncStateStore } from "../../stores-CKyEt0yk.js";
|
|
3
3
|
import { n as SyncNodeEntry, s as SyncNodeService } from "../../node-sync-3Z9QLz2n.js";
|
|
4
4
|
import { SyncPeerManager } from "./peer-manager.js";
|
|
5
|
-
import {
|
|
6
|
-
import { s as SyncSshTransport } from "../../ssh-transport-
|
|
5
|
+
import { p as SyncSessionService } from "../../session-sync-Zfo1Q-qT.js";
|
|
6
|
+
import { s as SyncSshTransport } from "../../ssh-transport-Cfb_nwkY.js";
|
|
7
7
|
//#region src/core/sync/coordinator.d.ts
|
|
8
8
|
interface NodeTransferPlan {
|
|
9
9
|
toLocal: SyncNodeEntry[];
|
|
@@ -2,7 +2,7 @@ import { compareSyncRevision } from "./revision.js";
|
|
|
2
2
|
import { syncRecordKey } from "./model.js";
|
|
3
3
|
import { SyncNodeManifestError } from "./node-sync.js";
|
|
4
4
|
import { SyncPeerNotFoundError } from "./peer-manager.js";
|
|
5
|
-
import { a as
|
|
5
|
+
import { a as makeSyncSessionBundle, c as sessionEventListDigest, d as validateSyncSessionManifest, l as syncSessionEntryKey, n as SyncSessionError, s as planSessionTransfers, u as validateSyncSessionBundle } from "../../session-sync-BQt3HXkp.js";
|
|
6
6
|
import { SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY, SYNC_SESSION_EVENT_TAIL_CAPABILITY } from "./protocol.js";
|
|
7
7
|
import { SyncHostKeyChallengeError } from "./host-key.js";
|
|
8
8
|
import { SyncSessionDeltaPrefixError, SyncSshProbeError } from "./ssh-transport.js";
|
|
@@ -16,6 +16,8 @@ const EMPTY_APPLY_RESULT = {
|
|
|
16
16
|
skipped: 0
|
|
17
17
|
};
|
|
18
18
|
const TRANSPORT_RETRY_DELAY_MS = 250;
|
|
19
|
+
const SESSION_CHUNK_TARGET_BYTES = 12582912;
|
|
20
|
+
const SESSION_CHUNK_MAX_KEYS = 8;
|
|
19
21
|
var SyncCoordinatorError = class extends Error {
|
|
20
22
|
code;
|
|
21
23
|
constructor(code, message) {
|
|
@@ -111,6 +113,16 @@ function emptySessionResult() {
|
|
|
111
113
|
function sessionResultCount(result) {
|
|
112
114
|
return result.created + result.updated + result.deleted;
|
|
113
115
|
}
|
|
116
|
+
function addSessionResults(target, delta) {
|
|
117
|
+
return {
|
|
118
|
+
created: target.created + delta.created,
|
|
119
|
+
updated: target.updated + delta.updated,
|
|
120
|
+
archived: target.archived + delta.archived,
|
|
121
|
+
deleted: target.deleted + delta.deleted,
|
|
122
|
+
skipped: target.skipped + delta.skipped,
|
|
123
|
+
unmapped: target.unmapped + delta.unmapped
|
|
124
|
+
};
|
|
125
|
+
}
|
|
114
126
|
function mergeSessionSkipped(...reports) {
|
|
115
127
|
const merged = /* @__PURE__ */ new Map();
|
|
116
128
|
for (const report of reports) for (const entry of report) merged.set(JSON.stringify([entry.sessionId, entry.reason]), entry);
|
|
@@ -223,39 +235,95 @@ var SyncCoordinator = class {
|
|
|
223
235
|
if (peer.syncSessions) {
|
|
224
236
|
const sessionService = this.sessions;
|
|
225
237
|
let plan;
|
|
226
|
-
let localBundleToApply;
|
|
227
|
-
let remoteBundleToApply;
|
|
228
238
|
let remoteSkipped = 0;
|
|
229
239
|
let remoteSessionDigest = "";
|
|
230
240
|
let localSessionDigest = "";
|
|
231
241
|
let localInstanceId = "";
|
|
232
|
-
let
|
|
242
|
+
let sessionLocalResult = emptySessionResult();
|
|
243
|
+
let sessionRemoteResult = emptySessionResult();
|
|
233
244
|
if (remoteNodeManifest.capabilities?.includes("session-manifest-v1")) {
|
|
234
245
|
const remoteManifest = validateSyncSessionManifest(await this.retryTransport(() => this.transport.fetchSessionManifest(peer.id)));
|
|
235
246
|
if (remoteManifest.instanceId !== remoteNodeManifest.instanceId) throw new SyncCoordinatorError("protocol-invalid", "节点与会话 manifest 的实例身份不一致");
|
|
236
247
|
const localManifest = await sessionService.exportManifest();
|
|
237
|
-
modernManifests = {
|
|
238
|
-
remote: remoteManifest,
|
|
239
|
-
local: localManifest
|
|
240
|
-
};
|
|
241
248
|
plan = planSessionTransfers(peer.direction, localManifest.entries, remoteManifest.entries);
|
|
242
|
-
remoteSkipped = remoteManifest.skipped.length;
|
|
243
249
|
remoteSessionDigest = remoteManifest.digest;
|
|
244
250
|
localSessionDigest = localManifest.digest;
|
|
245
251
|
localInstanceId = localManifest.instanceId;
|
|
246
252
|
const useEventDeltas = remoteNodeManifest.capabilities.includes(SYNC_SESSION_EVENT_TAIL_CAPABILITY);
|
|
247
253
|
const useSharedObjects = remoteNodeManifest.capabilities.includes(SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY);
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
254
|
+
const remoteSkippedReports = [remoteManifest.skipped];
|
|
255
|
+
if (plan.toLocal.length > 0) {
|
|
256
|
+
let perKeyBytes = 0;
|
|
257
|
+
let firstChunk = true;
|
|
258
|
+
let cursor = 0;
|
|
259
|
+
const keys = plan.toLocal.map(syncSessionEntryKey);
|
|
260
|
+
while (cursor < keys.length) {
|
|
261
|
+
const take = perKeyBytes > 0 ? Math.max(1, Math.min(SESSION_CHUNK_MAX_KEYS, Math.floor(SESSION_CHUNK_TARGET_BYTES / perKeyBytes))) : SESSION_CHUNK_MAX_KEYS;
|
|
262
|
+
const chunkKeys = keys.slice(cursor, cursor + take);
|
|
263
|
+
cursor += chunkKeys.length;
|
|
264
|
+
const chunkKeySet = new Set(chunkKeys);
|
|
265
|
+
const chunkManifest = plan.toLocal.filter((entry) => chunkKeySet.has(syncSessionEntryKey(entry)));
|
|
266
|
+
const prefixes = useEventDeltas ? sessionEventPrefixesFor(chunkManifest, localManifest.entries) : [];
|
|
267
|
+
let fetched = validateSyncSessionBundle(await this.retryTransport(() => this.transport.fetchSessionEntries(peer.id, chunkKeys, remoteManifest.digest, prefixes)));
|
|
268
|
+
if (fetched.instanceId !== remoteManifest.instanceId) throw new SyncCoordinatorError("protocol-invalid", "远端会话内容与 manifest 实例不一致");
|
|
269
|
+
assertSelectedSessionEntries(chunkManifest, fetched.entries, "远端");
|
|
270
|
+
remoteSkippedReports.push(fetched.skipped);
|
|
271
|
+
const applySkipped = firstChunk ? mergeSessionSkipped(remoteManifest.skipped, fetched.skipped) : fetched.skipped;
|
|
272
|
+
try {
|
|
273
|
+
const applied = await sessionService.applyBundle(makeSyncSessionBundle(remoteManifest.instanceId, fetched.entries, applySkipped), peer.name, peer.id);
|
|
274
|
+
sessionLocalResult = addSessionResults(sessionLocalResult, applied);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (!(error instanceof SyncSessionError && error.code === "session-delta-prefix")) throw error;
|
|
277
|
+
const fullRemoteEntries = validateSyncSessionBundle(await this.retryTransport(() => this.transport.fetchSessionEntries(peer.id, chunkKeys, remoteManifest.digest)));
|
|
278
|
+
if (fullRemoteEntries.instanceId !== remoteManifest.instanceId) throw new SyncCoordinatorError("protocol-invalid", "远端会话内容与 manifest 实例不一致");
|
|
279
|
+
assertSelectedSessionEntries(chunkManifest, fullRemoteEntries.entries, "远端");
|
|
280
|
+
remoteSkippedReports.push(fullRemoteEntries.skipped);
|
|
281
|
+
const applied = await sessionService.applyBundle(makeSyncSessionBundle(remoteManifest.instanceId, fullRemoteEntries.entries, firstChunk ? mergeSessionSkipped(remoteManifest.skipped, fullRemoteEntries.skipped) : fullRemoteEntries.skipped), peer.name, peer.id);
|
|
282
|
+
sessionLocalResult = addSessionResults(sessionLocalResult, applied);
|
|
283
|
+
fetched = fullRemoteEntries;
|
|
284
|
+
}
|
|
285
|
+
const chunkBytes = Buffer.byteLength(JSON.stringify(fetched), "utf8");
|
|
286
|
+
if (chunkKeys.length > 0) perKeyBytes = Math.max(perKeyBytes, Math.ceil(chunkBytes / chunkKeys.length));
|
|
287
|
+
firstChunk = false;
|
|
288
|
+
}
|
|
289
|
+
} else if (remoteManifest.skipped.length > 0) {
|
|
290
|
+
const applied = await sessionService.applyBundle(makeSyncSessionBundle(remoteManifest.instanceId, [], remoteManifest.skipped), peer.name, peer.id);
|
|
291
|
+
sessionLocalResult = addSessionResults(sessionLocalResult, applied);
|
|
292
|
+
}
|
|
293
|
+
remoteSkipped = mergeSessionSkipped(...remoteSkippedReports).length;
|
|
294
|
+
if (plan.toRemote.length > 0) {
|
|
295
|
+
const keys = plan.toRemote.map(syncSessionEntryKey);
|
|
296
|
+
let perKeyBytes = 0;
|
|
297
|
+
let firstChunk = true;
|
|
298
|
+
let cursor = 0;
|
|
299
|
+
while (cursor < keys.length) {
|
|
300
|
+
const take = perKeyBytes > 0 ? Math.max(1, Math.min(SESSION_CHUNK_MAX_KEYS, Math.floor(SESSION_CHUNK_TARGET_BYTES / perKeyBytes))) : SESSION_CHUNK_MAX_KEYS;
|
|
301
|
+
const chunkKeys = keys.slice(cursor, cursor + take);
|
|
302
|
+
cursor += chunkKeys.length;
|
|
303
|
+
const chunkKeySet = new Set(chunkKeys);
|
|
304
|
+
const chunkManifest = plan.toRemote.filter((entry) => chunkKeySet.has(syncSessionEntryKey(entry)));
|
|
305
|
+
const prefixes = useEventDeltas ? sessionEventPrefixesFor(chunkManifest, remoteManifest.entries) : [];
|
|
306
|
+
const exported = await sessionService.exportEntries(chunkKeys, localManifest.digest, prefixes, { sharedObjects: useSharedObjects });
|
|
307
|
+
assertSelectedSessionEntries(chunkManifest, exported.entries, "本地");
|
|
308
|
+
const wireSkipped = firstChunk ? mergeSessionSkipped(localManifest.skipped, exported.skipped) : exported.skipped;
|
|
309
|
+
let wireBundle = exported.objects !== void 0 ? makeSyncSessionBundle(exported.instanceId, exported.entries, wireSkipped, exported.objects) : makeSyncSessionBundle(exported.instanceId, exported.entries, wireSkipped);
|
|
310
|
+
try {
|
|
311
|
+
const applied = await this.retryTransport(() => this.transport.applySessionBundle(peer.id, wireBundle, localInstanceId));
|
|
312
|
+
sessionRemoteResult = addSessionResults(sessionRemoteResult, applied);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (!(error instanceof SyncSessionDeltaPrefixError)) throw error;
|
|
315
|
+
const fullLocalEntries = await sessionService.exportEntries(chunkKeys, localManifest.digest, [], { sharedObjects: wireBundle.objects !== void 0 });
|
|
316
|
+
assertSelectedSessionEntries(chunkManifest, fullLocalEntries.entries, "本地");
|
|
317
|
+
const skippedReport = firstChunk ? mergeSessionSkipped(localManifest.skipped, fullLocalEntries.skipped) : fullLocalEntries.skipped;
|
|
318
|
+
wireBundle = fullLocalEntries.objects !== void 0 ? makeSyncSessionBundle(fullLocalEntries.instanceId, fullLocalEntries.entries, skippedReport, fullLocalEntries.objects) : makeSyncSessionBundle(fullLocalEntries.instanceId, fullLocalEntries.entries, skippedReport);
|
|
319
|
+
const applied = await this.retryTransport(() => this.transport.applySessionBundle(peer.id, wireBundle, localInstanceId));
|
|
320
|
+
sessionRemoteResult = addSessionResults(sessionRemoteResult, applied);
|
|
321
|
+
}
|
|
322
|
+
const chunkBytes = Buffer.byteLength(JSON.stringify(wireBundle), "utf8");
|
|
323
|
+
if (chunkKeys.length > 0) perKeyBytes = Math.max(perKeyBytes, Math.ceil(chunkBytes / chunkKeys.length));
|
|
324
|
+
firstChunk = false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
259
327
|
} else {
|
|
260
328
|
const remoteBundle = validateSyncSessionBundle(await this.retryTransport(() => this.transport.fetchSessionBundle(peer.id)));
|
|
261
329
|
if (remoteBundle.instanceId !== remoteNodeManifest.instanceId) throw new SyncCoordinatorError("protocol-invalid", "节点与会话 manifest 的实例身份不一致");
|
|
@@ -265,31 +333,8 @@ var SyncCoordinator = class {
|
|
|
265
333
|
remoteSessionDigest = remoteBundle.digest;
|
|
266
334
|
localSessionDigest = localBundle.digest;
|
|
267
335
|
localInstanceId = localBundle.instanceId;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
let sessionLocalResult = emptySessionResult();
|
|
272
|
-
if (plan.toLocal.length > 0 || remoteSkipped > 0) try {
|
|
273
|
-
sessionLocalResult = await sessionService.applyBundle(localBundleToApply, peer.name, peer.id);
|
|
274
|
-
} catch (error) {
|
|
275
|
-
if (!(error instanceof SyncSessionError && error.code === "session-delta-prefix") || !modernManifests) throw error;
|
|
276
|
-
const fullRemoteEntries = validateSyncSessionBundle(await this.retryTransport(() => this.transport.fetchSessionEntries(peer.id, plan.toLocal.map(syncSessionEntryKey), modernManifests.remote.digest)));
|
|
277
|
-
assertSelectedSessionEntries(plan.toLocal, fullRemoteEntries.entries, "远端");
|
|
278
|
-
const skippedReport = mergeSessionSkipped(modernManifests.remote.skipped, fullRemoteEntries.skipped);
|
|
279
|
-
remoteSkipped = skippedReport.length;
|
|
280
|
-
localBundleToApply = makeSyncSessionBundle(modernManifests.remote.instanceId, fullRemoteEntries.entries, skippedReport);
|
|
281
|
-
sessionLocalResult = await sessionService.applyBundle(localBundleToApply, peer.name, peer.id);
|
|
282
|
-
}
|
|
283
|
-
let sessionRemoteResult = emptySessionResult();
|
|
284
|
-
if (plan.toRemote.length > 0) try {
|
|
285
|
-
sessionRemoteResult = await this.retryTransport(() => this.transport.applySessionBundle(peer.id, remoteBundleToApply, localInstanceId));
|
|
286
|
-
} catch (error) {
|
|
287
|
-
if (!(error instanceof SyncSessionDeltaPrefixError) || !modernManifests) throw error;
|
|
288
|
-
const fullLocalEntries = await sessionService.exportEntries(plan.toRemote.map(syncSessionEntryKey), modernManifests.local.digest, [], { sharedObjects: remoteBundleToApply?.objects !== void 0 });
|
|
289
|
-
assertSelectedSessionEntries(plan.toRemote, fullLocalEntries.entries, "本地");
|
|
290
|
-
const skippedReport = mergeSessionSkipped(modernManifests.local.skipped, fullLocalEntries.skipped);
|
|
291
|
-
remoteBundleToApply = fullLocalEntries.objects !== void 0 ? makeSyncSessionBundle(fullLocalEntries.instanceId, fullLocalEntries.entries, skippedReport, fullLocalEntries.objects) : makeSyncSessionBundle(fullLocalEntries.instanceId, fullLocalEntries.entries, skippedReport);
|
|
292
|
-
sessionRemoteResult = await this.retryTransport(() => this.transport.applySessionBundle(peer.id, remoteBundleToApply, localInstanceId));
|
|
336
|
+
if (plan.toLocal.length > 0 || remoteSkipped > 0) sessionLocalResult = await sessionService.applyBundle(makeSyncSessionBundle(remoteBundle.instanceId, plan.toLocal, remoteBundle.skipped), peer.name, peer.id);
|
|
337
|
+
if (plan.toRemote.length > 0) sessionRemoteResult = await this.retryTransport(() => this.transport.applySessionBundle(peer.id, makeSyncSessionBundle(localBundle.instanceId, plan.toRemote, localBundle.skipped), localInstanceId));
|
|
293
338
|
}
|
|
294
339
|
importedSessions = sessionResultCount(sessionLocalResult);
|
|
295
340
|
exportedSessions = sessionResultCount(sessionRemoteResult);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coordinator.js","names":[],"sources":["../../../src/core/sync/coordinator.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport { SyncNodeManifestError, type SyncNodeApplyResult, type SyncNodeEntry, type SyncNodeService } from './node-sync.js'\nimport { SyncHostKeyChallengeError } from './host-key.js'\nimport { compareSyncRevision } from './revision.js'\nimport { syncRecordKey, type SyncPeer } from './model.js'\nimport { SyncPeerManager, SyncPeerNotFoundError } from './peer-manager.js'\nimport type { SyncStateStore } from './stores.js'\nimport { SyncSessionDeltaPrefixError, SyncSshProbeError, SyncSshTransport } from './ssh-transport.js'\nimport {\n makeSyncSessionBundle,\n planSessionTransfers,\n SyncSessionError,\n SyncSessionService,\n sessionEventListDigest,\n syncSessionEntryKey,\n validateSyncSessionBundle,\n validateSyncSessionManifest,\n} from './session-sync.js'\nimport type {\n SyncSessionApplyResult,\n SyncSessionBundle,\n SyncSessionEntry,\n SyncSessionEventPrefix,\n SyncSessionManifest,\n SyncSessionManifestEntry,\n} from './session-sync.js'\nimport { SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY, SYNC_SESSION_EVENT_TAIL_CAPABILITY, SYNC_SESSION_MANIFEST_CAPABILITY } from './protocol.js'\n\nconst EMPTY_APPLY_RESULT: SyncNodeApplyResult = { created: 0, updated: 0, deleted: 0, pendingRemoval: 0, skipped: 0 }\nconst TRANSPORT_RETRY_DELAY_MS = 250\n\nexport interface NodeTransferPlan {\n toLocal: SyncNodeEntry[]\n toRemote: SyncNodeEntry[]\n skipped: number\n}\n\nexport interface SyncRunSummary {\n peerId: string\n direction: SyncPeer['direction']\n startedAt: string\n finishedAt: string\n remoteInstanceId: string\n imported: number\n exported: number\n skipped: number\n pendingRemoval: number\n importedSessions: number\n exportedSessions: number\n skippedSessions: number\n}\n\nexport class SyncCoordinatorError extends Error {\n constructor(readonly code: string, message: string) {\n super(message)\n this.name = 'SyncCoordinatorError'\n }\n}\n\nfunction addCount(result: SyncNodeApplyResult): number {\n return result.created + result.updated + result.deleted\n}\n\nfunction manifestDigest(entries: readonly SyncNodeEntry[]): string {\n const versions = entries\n .map((entry) => ({\n key: syncRecordKey('node', entry.metadata),\n revision: entry.metadata.revision,\n contentDigest: entry.metadata.contentDigest,\n deleted: Boolean(entry.metadata.deleted),\n }))\n .sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0)\n return `sha256:${createHash('sha256').update(JSON.stringify(versions)).digest('hex')}`\n}\n\nexport function planNodeTransfers(\n direction: SyncPeer['direction'],\n localEntries: readonly SyncNodeEntry[],\n remoteEntries: readonly SyncNodeEntry[],\n): NodeTransferPlan {\n const toLocal: SyncNodeEntry[] = []\n const toRemote: SyncNodeEntry[] = []\n if (direction === 'pull') return { toLocal: [...remoteEntries], toRemote, skipped: 0 }\n\n const localByKey = new Map(localEntries.map((entry) => [syncRecordKey('node', entry.metadata), entry]))\n const remoteByKey = new Map(remoteEntries.map((entry) => [syncRecordKey('node', entry.metadata), entry]))\n const keys = new Set([...localByKey.keys(), ...remoteByKey.keys()])\n let skipped = 0\n\n for (const key of keys) {\n const local = localByKey.get(key)\n const remote = remoteByKey.get(key)\n if (!local) {\n toLocal.push(remote!)\n continue\n }\n if (!remote) {\n toRemote.push(local)\n continue\n }\n\n const comparison = compareSyncRevision(local.metadata.revision, remote.metadata.revision)\n if (comparison > 0) {\n toRemote.push(local)\n } else if (comparison < 0) {\n toLocal.push(remote)\n } else if (\n local.metadata.contentDigest === remote.metadata.contentDigest &&\n Boolean(local.metadata.deleted) === Boolean(remote.metadata.deleted)\n ) {\n skipped += 1\n } else {\n throw new SyncCoordinatorError('revision-conflict', 'Equal node revisions contain different content')\n }\n }\n return { toLocal, toRemote, skipped }\n}\n\nfunction mapFailure(error: unknown): SyncCoordinatorError {\n if (error instanceof SyncCoordinatorError) return error\n if (error instanceof SyncPeerNotFoundError) return new SyncCoordinatorError('peer-not-found', '同步节点已不存在')\n if (error instanceof SyncHostKeyChallengeError) {\n const code = error.decision === 'changed' ? 'host-key-changed' : 'host-key-untrusted'\n return new SyncCoordinatorError(code, error.message)\n }\n if (error instanceof SyncSshProbeError) {\n const messages: Record<string, string> = {\n auth: 'SSH 认证失败',\n network: 'SSH 节点不可达',\n timeout: 'SSH 连接超时',\n hostkey: 'SSH 主机密钥校验失败',\n helper: '远端同步 helper 不可用',\n protocol: '同步协议不兼容',\n }\n const baseMsg = messages[error.category] ?? 'SSH 同步失败'\n const detail = (error as Error & { detail?: string }).detail\n return new SyncCoordinatorError(`ssh-${error.category}`, detail ? `${baseMsg}:${detail}` : baseMsg)\n }\n if (error instanceof SyncNodeManifestError) return new SyncCoordinatorError('manifest-invalid', '节点 manifest 校验失败')\n if (error instanceof SyncSessionError) {\n return new SyncCoordinatorError(error.code.startsWith('session-') ? error.code : `session-${error.code}`, error.message)\n }\n return new SyncCoordinatorError('sync-failed', '同步作业失败')\n}\n\nfunction resultCount(result: SyncNodeApplyResult): { applied: number; skipped: number; pendingRemoval: number } {\n return {\n applied: addCount(result),\n skipped: result.skipped,\n pendingRemoval: result.pendingRemoval,\n }\n}\n\nfunction emptySessionResult(): SyncSessionApplyResult {\n return { created: 0, updated: 0, archived: 0, deleted: 0, skipped: 0, unmapped: 0 }\n}\n\nfunction sessionResultCount(result: SyncSessionApplyResult): number {\n return result.created + result.updated + result.deleted\n}\n\nfunction mergeSessionSkipped(...reports: Array<readonly { sessionId: string; reason: string }[]>): Array<{ sessionId: string; reason: string }> {\n const merged = new Map<string, { sessionId: string; reason: string }>()\n for (const report of reports) {\n for (const entry of report) merged.set(JSON.stringify([entry.sessionId, entry.reason]), entry)\n }\n return [...merged.values()]\n}\n\nfunction assertSelectedSessionEntries(\n expectedEntries: readonly SyncSessionManifestEntry[],\n selectedEntries: readonly SyncSessionEntry[],\n side: string,\n): void {\n const expected = new Map(expectedEntries.map((entry) => [syncSessionEntryKey(entry), entry]))\n const selected = new Map(selectedEntries.map((entry) => [syncSessionEntryKey(entry), entry]))\n if (selected.size !== selectedEntries.length || selected.size !== expected.size) {\n throw new SyncCoordinatorError('session-manifest-stale', `${side}会话内容与 manifest 清单不一致`)\n }\n for (const [key, expectedEntry] of expected) {\n const metadata = expectedEntry.metadata\n const currentEntry = selected.get(key)\n const current = currentEntry?.metadata\n const eventSummary = currentEntry?.eventDelta\n ? {\n eventCount: currentEntry.eventDelta.fullEventCount,\n eventDigest: currentEntry.eventDelta.fullEventDigest,\n inheritedEventCount: currentEntry.inheritedEventCount,\n }\n : currentEntry?.events\n ? {\n eventCount: currentEntry.events.length,\n eventDigest: sessionEventListDigest(currentEntry.events),\n inheritedEventCount: currentEntry.inheritedEventCount,\n }\n : undefined\n const hasExpectedEventSummary = expectedEntry.eventCount !== undefined || expectedEntry.eventDigest !== undefined ||\n expectedEntry.inheritedEventCount !== undefined\n const expectedWorkspace = metadata.workspaceOrigin\n const currentWorkspace = current?.workspaceOrigin\n const mismatches: string[] = []\n if (!current) mismatches.push('entry-missing')\n else {\n if (current.localEntityId !== metadata.localEntityId) mismatches.push('localEntityId')\n if (compareSyncRevision(current.revision, metadata.revision) !== 0) mismatches.push('revision')\n if (current.contentDigest !== metadata.contentDigest) mismatches.push('contentDigest')\n if (current.stateDigest !== metadata.stateDigest) mismatches.push('stateDigest')\n if (Boolean(current.deleted) !== Boolean(metadata.deleted)) mismatches.push('deleted')\n if (expectedWorkspace?.originInstanceId !== currentWorkspace?.originInstanceId ||\n expectedWorkspace?.originEntityId !== currentWorkspace?.originEntityId) mismatches.push('workspaceOrigin')\n if (hasExpectedEventSummary && (!eventSummary || eventSummary.eventCount !== expectedEntry.eventCount ||\n eventSummary.eventDigest !== expectedEntry.eventDigest || eventSummary.inheritedEventCount !== expectedEntry.inheritedEventCount)) {\n mismatches.push('eventSummary')\n }\n }\n if (mismatches.length > 0) {\n const detail = current && hasExpectedEventSummary\n ? ` expected(eventCount=${expectedEntry.eventCount}, inherited=${expectedEntry.inheritedEventCount}, digest=${expectedEntry.eventDigest})` +\n ` actual(${eventSummary ? `eventCount=${eventSummary.eventCount}, inherited=${eventSummary.inheritedEventCount}, digest=${eventSummary.eventDigest}` : 'no-events'})`\n : ''\n throw new SyncCoordinatorError('session-manifest-stale', `${side}会话内容在 manifest 之后发生变化(key=${key},字段=${mismatches.join(',')})${detail}`)\n }\n }\n}\n\nfunction sessionEventPrefixesFor(\n selectedEntries: readonly SyncSessionManifestEntry[],\n receiverEntries: readonly SyncSessionManifestEntry[],\n): SyncSessionEventPrefix[] {\n const receiverByKey = new Map(receiverEntries.map((entry) => [syncSessionEntryKey(entry), entry]))\n const prefixes: SyncSessionEventPrefix[] = []\n for (const selected of selectedEntries) {\n if (selected.branchId || selected.metadata.deleted) continue\n const receiver = receiverByKey.get(syncSessionEntryKey(selected))\n if (!receiver || receiver.branchId || receiver.metadata.deleted || receiver.eventCount === undefined ||\n receiver.eventDigest === undefined || receiver.inheritedEventCount === undefined) continue\n prefixes.push({\n key: syncSessionEntryKey(selected),\n eventCount: receiver.eventCount,\n eventDigest: receiver.eventDigest,\n inheritedEventCount: receiver.inheritedEventCount,\n })\n }\n return prefixes\n}\n\nexport class SyncCoordinator {\n private readonly inFlight = new Map<string, Promise<SyncRunSummary>>()\n\n constructor(\n private readonly peers: SyncPeerManager,\n private readonly state: SyncStateStore,\n private readonly nodes: SyncNodeService,\n private readonly transport: SyncSshTransport,\n private readonly now: () => number = Date.now,\n private readonly sessions?: SyncSessionService,\n ) {}\n\n run(peerId: string): Promise<SyncRunSummary> {\n const active = this.inFlight.get(peerId)\n if (active) return active\n const run = this.execute(peerId)\n this.inFlight.set(peerId, run)\n void run.then(() => this.clear(peerId, run), () => this.clear(peerId, run))\n return run\n }\n\n private async execute(peerId: string): Promise<SyncRunSummary> {\n const peer = await this.peers.get(peerId)\n if (!peer) throw new SyncCoordinatorError('peer-not-found', '同步节点已不存在')\n if (!peer.enabled) throw new SyncCoordinatorError('peer-disabled', '此同步节点已停用')\n\n const startedAt = new Date(this.now()).toISOString()\n this.peers.updateProgress(peer.id, {\n lastRunAt: startedAt,\n lastRunStatus: 'running',\n lastErrorCode: undefined,\n lastErrorMessage: undefined,\n })\n\n try {\n if (peer.syncSessions && !this.sessions) throw new SyncCoordinatorError('sessions-unavailable', '会话同步服务不可用')\n const remoteNodeManifest = await this.retryTransport(() => this.transport.fetchNodeManifest(peer.id))\n if (remoteNodeManifest.protocolVersion !== 1 || remoteNodeManifest.instanceId === this.state.instanceId()) {\n throw new SyncCoordinatorError('protocol-invalid', '同步 peer 协议版本或实例身份无效')\n }\n const remoteEntries = this.nodes.validateManifest(remoteNodeManifest.records)\n const localManifest = await this.nodes.exportManifest()\n const localEntries = this.nodes.validateManifest(localManifest.records)\n const plan = planNodeTransfers(peer.direction, localEntries, remoteEntries)\n\n const localResult = plan.toLocal.length > 0\n ? await this.nodes.applyManifest(plan.toLocal, peer.name, 'peer')\n : EMPTY_APPLY_RESULT\n const remoteResult = plan.toRemote.length > 0\n ? await this.retryTransport(() => this.transport.applyNodeManifest(peer.id, plan.toRemote, localManifest.instanceId))\n : EMPTY_APPLY_RESULT\n const localCounts = resultCount(localResult)\n const remoteCounts = resultCount(remoteResult)\n let importedSessions = 0\n let exportedSessions = 0\n let skippedSessions = 0\n let digest = manifestDigest(remoteEntries)\n if (peer.syncSessions) {\n const sessionService = this.sessions!\n let plan: ReturnType<typeof planSessionTransfers>\n let localBundleToApply: ReturnType<typeof makeSyncSessionBundle>\n let remoteBundleToApply: ReturnType<typeof makeSyncSessionBundle>\n let remoteSkipped = 0\n let remoteSessionDigest = ''\n let localSessionDigest = ''\n let localInstanceId = ''\n let modernManifests: { remote: SyncSessionManifest; local: SyncSessionManifest } | undefined\n if (remoteNodeManifest.capabilities?.includes(SYNC_SESSION_MANIFEST_CAPABILITY)) {\n const remoteManifest = validateSyncSessionManifest(\n await this.retryTransport(() => this.transport.fetchSessionManifest(peer.id)),\n )\n if (remoteManifest.instanceId !== remoteNodeManifest.instanceId) {\n throw new SyncCoordinatorError('protocol-invalid', '节点与会话 manifest 的实例身份不一致')\n }\n const localManifest = await sessionService.exportManifest()\n modernManifests = { remote: remoteManifest, local: localManifest }\n plan = planSessionTransfers(peer.direction, localManifest.entries, remoteManifest.entries)\n remoteSkipped = remoteManifest.skipped.length\n remoteSessionDigest = remoteManifest.digest\n localSessionDigest = localManifest.digest\n localInstanceId = localManifest.instanceId\n const useEventDeltas = remoteNodeManifest.capabilities.includes(SYNC_SESSION_EVENT_TAIL_CAPABILITY)\n const useSharedObjects = remoteNodeManifest.capabilities.includes(SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY)\n const toWireBundle = (wired: SyncSessionBundle, skippedReport: Array<{ sessionId: string; reason: string }>): SyncSessionBundle =>\n makeSyncSessionBundle(wired.instanceId, wired.entries, skippedReport, wired.objects)\n\n const remoteEntriesToApply = plan.toLocal.length > 0\n ? validateSyncSessionBundle(await this.retryTransport(() =>\n this.transport.fetchSessionEntries(\n peer.id,\n plan.toLocal.map(syncSessionEntryKey),\n remoteManifest.digest,\n useEventDeltas ? sessionEventPrefixesFor(plan.toLocal, localManifest.entries) : [],\n ),\n ))\n : makeSyncSessionBundle(remoteManifest.instanceId, [])\n if (remoteEntriesToApply.instanceId !== remoteManifest.instanceId) {\n throw new SyncCoordinatorError('protocol-invalid', '远端会话内容与 manifest 实例不一致')\n }\n assertSelectedSessionEntries(plan.toLocal, remoteEntriesToApply.entries, '远端')\n const localEntriesToApply = plan.toRemote.length > 0\n ? await sessionService.exportEntries(\n plan.toRemote.map(syncSessionEntryKey),\n localManifest.digest,\n useEventDeltas ? sessionEventPrefixesFor(plan.toRemote, remoteManifest.entries) : [],\n { sharedObjects: useSharedObjects },\n )\n : makeSyncSessionBundle(localManifest.instanceId, [])\n assertSelectedSessionEntries(plan.toRemote, localEntriesToApply.entries, '本地')\n const remoteSkippedReport = mergeSessionSkipped(remoteManifest.skipped, remoteEntriesToApply.skipped)\n const localSkippedReport = mergeSessionSkipped(localManifest.skipped, localEntriesToApply.skipped)\n remoteSkipped = remoteSkippedReport.length\n localBundleToApply = makeSyncSessionBundle(remoteManifest.instanceId, remoteEntriesToApply.entries, remoteSkippedReport)\n remoteBundleToApply = toWireBundle(localEntriesToApply, localSkippedReport)\n } else {\n const remoteBundle = validateSyncSessionBundle(await this.retryTransport(() => this.transport.fetchSessionBundle(peer.id)))\n if (remoteBundle.instanceId !== remoteNodeManifest.instanceId) {\n throw new SyncCoordinatorError('protocol-invalid', '节点与会话 manifest 的实例身份不一致')\n }\n const localBundle = await sessionService.exportBundle()\n plan = planSessionTransfers(peer.direction, localBundle.entries, remoteBundle.entries)\n remoteSkipped = remoteBundle.skipped.length\n remoteSessionDigest = remoteBundle.digest\n localSessionDigest = localBundle.digest\n localInstanceId = localBundle.instanceId\n localBundleToApply = makeSyncSessionBundle(remoteBundle.instanceId, plan.toLocal, remoteBundle.skipped)\n remoteBundleToApply = makeSyncSessionBundle(localBundle.instanceId, plan.toRemote, localBundle.skipped)\n }\n let sessionLocalResult = emptySessionResult()\n if (plan.toLocal.length > 0 || remoteSkipped > 0) {\n try {\n sessionLocalResult = await sessionService.applyBundle(localBundleToApply, peer.name, peer.id)\n } catch (error) {\n if (!(error instanceof SyncSessionError && error.code === 'session-delta-prefix') || !modernManifests) throw error\n const fullRemoteEntries = validateSyncSessionBundle(await this.retryTransport(() =>\n this.transport.fetchSessionEntries(\n peer.id,\n plan.toLocal.map(syncSessionEntryKey),\n modernManifests!.remote.digest,\n ),\n ))\n assertSelectedSessionEntries(plan.toLocal, fullRemoteEntries.entries, '远端')\n const skippedReport = mergeSessionSkipped(modernManifests.remote.skipped, fullRemoteEntries.skipped)\n remoteSkipped = skippedReport.length\n localBundleToApply = makeSyncSessionBundle(modernManifests.remote.instanceId, fullRemoteEntries.entries, skippedReport)\n sessionLocalResult = await sessionService.applyBundle(localBundleToApply, peer.name, peer.id)\n }\n }\n\n let sessionRemoteResult = emptySessionResult()\n if (plan.toRemote.length > 0) {\n try {\n sessionRemoteResult = await this.retryTransport(() =>\n this.transport.applySessionBundle(peer.id, remoteBundleToApply, localInstanceId),\n )\n } catch (error) {\n if (!(error instanceof SyncSessionDeltaPrefixError) || !modernManifests) throw error\n const fullLocalEntries = await sessionService.exportEntries(\n plan.toRemote.map(syncSessionEntryKey),\n modernManifests.local.digest,\n [],\n { sharedObjects: remoteBundleToApply?.objects !== undefined },\n )\n assertSelectedSessionEntries(plan.toRemote, fullLocalEntries.entries, '本地')\n const skippedReport = mergeSessionSkipped(modernManifests.local.skipped, fullLocalEntries.skipped)\n remoteBundleToApply = fullLocalEntries.objects !== undefined\n ? makeSyncSessionBundle(fullLocalEntries.instanceId, fullLocalEntries.entries, skippedReport, fullLocalEntries.objects)\n : makeSyncSessionBundle(fullLocalEntries.instanceId, fullLocalEntries.entries, skippedReport)\n sessionRemoteResult = await this.retryTransport(() =>\n this.transport.applySessionBundle(peer.id, remoteBundleToApply, localInstanceId),\n )\n }\n }\n importedSessions = sessionResultCount(sessionLocalResult)\n exportedSessions = sessionResultCount(sessionRemoteResult)\n skippedSessions = plan.skipped + sessionLocalResult.skipped + sessionRemoteResult.skipped\n digest = `sha256:${createHash('sha256').update(`${digest}\\0${remoteSessionDigest}\\0${localSessionDigest}`).digest('hex')}`\n }\n const finishedAt = new Date(this.now()).toISOString()\n const summary: SyncRunSummary = {\n peerId,\n direction: peer.direction,\n startedAt,\n finishedAt,\n remoteInstanceId: remoteNodeManifest.instanceId,\n imported: localCounts.applied,\n exported: remoteCounts.applied,\n skipped: plan.skipped + localCounts.skipped + remoteCounts.skipped + skippedSessions,\n pendingRemoval: localCounts.pendingRemoval + remoteCounts.pendingRemoval,\n importedSessions,\n exportedSessions,\n skippedSessions,\n }\n\n this.peers.updateProgress(peer.id, {\n lastRunAt: finishedAt,\n lastRunStatus: 'succeeded',\n lastSuccessAt: finishedAt,\n lastManifestDigest: digest,\n watermark: digest,\n lastErrorCode: undefined,\n lastErrorMessage: undefined,\n lastResult: {\n imported: summary.imported,\n exported: summary.exported,\n skipped: summary.skipped,\n pendingRemoval: summary.pendingRemoval,\n importedSessions,\n exportedSessions,\n skippedSessions,\n },\n })\n return summary\n } catch (error) {\n const failure = mapFailure(error)\n if (!(error instanceof SyncCoordinatorError) && !(error instanceof SyncSshProbeError) && !(error instanceof SyncSessionError) && !(error instanceof SyncNodeManifestError) && !(error instanceof SyncHostKeyChallengeError) && !(error instanceof SyncPeerNotFoundError)) {\n failure.message = `${failure.message}:${error instanceof Error ? error.message : String(error)}`\n }\n try {\n this.peers.updateProgress(peer.id, {\n lastRunAt: new Date(this.now()).toISOString(),\n lastRunStatus: 'failed',\n lastErrorCode: failure.code,\n lastErrorMessage: failure.message,\n })\n } catch {}\n throw failure\n }\n }\n\n private clear(peerId: string, run: Promise<SyncRunSummary>): void {\n if (this.inFlight.get(peerId) === run) this.inFlight.delete(peerId)\n }\n\n private async retryTransport<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation()\n } catch (error) {\n if (!(error instanceof SyncSshProbeError) || !['network', 'timeout', 'helper'].includes(error.category)) throw error\n await new Promise((resolve) => setTimeout(resolve, TRANSPORT_RETRY_DELAY_MS))\n return operation()\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"coordinator.js","names":[],"sources":["../../../src/core/sync/coordinator.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport { SyncNodeManifestError, type SyncNodeApplyResult, type SyncNodeEntry, type SyncNodeService } from './node-sync.js'\nimport { SyncHostKeyChallengeError } from './host-key.js'\nimport { compareSyncRevision } from './revision.js'\nimport { syncRecordKey, type SyncPeer } from './model.js'\nimport { SyncPeerManager, SyncPeerNotFoundError } from './peer-manager.js'\nimport type { SyncStateStore } from './stores.js'\nimport { SyncSessionDeltaPrefixError, SyncSshProbeError, SyncSshTransport } from './ssh-transport.js'\nimport {\n makeSyncSessionBundle,\n planSessionTransfers,\n SyncSessionError,\n SyncSessionService,\n sessionEventListDigest,\n syncSessionEntryKey,\n validateSyncSessionBundle,\n validateSyncSessionManifest,\n} from './session-sync.js'\nimport type {\n SyncSessionApplyResult,\n SyncSessionBundle,\n SyncSessionEntry,\n SyncSessionEventPrefix,\n SyncSessionManifest,\n SyncSessionManifestEntry,\n} from './session-sync.js'\nimport { SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY, SYNC_SESSION_EVENT_TAIL_CAPABILITY, SYNC_SESSION_MANIFEST_CAPABILITY } from './protocol.js'\n\nconst EMPTY_APPLY_RESULT: SyncNodeApplyResult = { created: 0, updated: 0, deleted: 0, pendingRemoval: 0, skipped: 0 }\nconst TRANSPORT_RETRY_DELAY_MS = 250\n// 会话分批交换的目标批体积与单批 key 数上限:目标值 ≈ JSON.parse 后对象图的\n// 堆占用仍远小于宿主堆上限(桌面端约 1.4GB);单批 key 上限限制批内的\n// 条目数,避免 manifest 条目本身过大。\nconst SESSION_CHUNK_TARGET_BYTES = 12 * 1024 * 1024\nconst SESSION_CHUNK_MAX_KEYS = 8\n\nexport interface NodeTransferPlan {\n toLocal: SyncNodeEntry[]\n toRemote: SyncNodeEntry[]\n skipped: number\n}\n\nexport interface SyncRunSummary {\n peerId: string\n direction: SyncPeer['direction']\n startedAt: string\n finishedAt: string\n remoteInstanceId: string\n imported: number\n exported: number\n skipped: number\n pendingRemoval: number\n importedSessions: number\n exportedSessions: number\n skippedSessions: number\n}\n\nexport class SyncCoordinatorError extends Error {\n constructor(readonly code: string, message: string) {\n super(message)\n this.name = 'SyncCoordinatorError'\n }\n}\n\nfunction addCount(result: SyncNodeApplyResult): number {\n return result.created + result.updated + result.deleted\n}\n\nfunction manifestDigest(entries: readonly SyncNodeEntry[]): string {\n const versions = entries\n .map((entry) => ({\n key: syncRecordKey('node', entry.metadata),\n revision: entry.metadata.revision,\n contentDigest: entry.metadata.contentDigest,\n deleted: Boolean(entry.metadata.deleted),\n }))\n .sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0)\n return `sha256:${createHash('sha256').update(JSON.stringify(versions)).digest('hex')}`\n}\n\nexport function planNodeTransfers(\n direction: SyncPeer['direction'],\n localEntries: readonly SyncNodeEntry[],\n remoteEntries: readonly SyncNodeEntry[],\n): NodeTransferPlan {\n const toLocal: SyncNodeEntry[] = []\n const toRemote: SyncNodeEntry[] = []\n if (direction === 'pull') return { toLocal: [...remoteEntries], toRemote, skipped: 0 }\n\n const localByKey = new Map(localEntries.map((entry) => [syncRecordKey('node', entry.metadata), entry]))\n const remoteByKey = new Map(remoteEntries.map((entry) => [syncRecordKey('node', entry.metadata), entry]))\n const keys = new Set([...localByKey.keys(), ...remoteByKey.keys()])\n let skipped = 0\n\n for (const key of keys) {\n const local = localByKey.get(key)\n const remote = remoteByKey.get(key)\n if (!local) {\n toLocal.push(remote!)\n continue\n }\n if (!remote) {\n toRemote.push(local)\n continue\n }\n\n const comparison = compareSyncRevision(local.metadata.revision, remote.metadata.revision)\n if (comparison > 0) {\n toRemote.push(local)\n } else if (comparison < 0) {\n toLocal.push(remote)\n } else if (\n local.metadata.contentDigest === remote.metadata.contentDigest &&\n Boolean(local.metadata.deleted) === Boolean(remote.metadata.deleted)\n ) {\n skipped += 1\n } else {\n throw new SyncCoordinatorError('revision-conflict', 'Equal node revisions contain different content')\n }\n }\n return { toLocal, toRemote, skipped }\n}\n\nfunction mapFailure(error: unknown): SyncCoordinatorError {\n if (error instanceof SyncCoordinatorError) return error\n if (error instanceof SyncPeerNotFoundError) return new SyncCoordinatorError('peer-not-found', '同步节点已不存在')\n if (error instanceof SyncHostKeyChallengeError) {\n const code = error.decision === 'changed' ? 'host-key-changed' : 'host-key-untrusted'\n return new SyncCoordinatorError(code, error.message)\n }\n if (error instanceof SyncSshProbeError) {\n const messages: Record<string, string> = {\n auth: 'SSH 认证失败',\n network: 'SSH 节点不可达',\n timeout: 'SSH 连接超时',\n hostkey: 'SSH 主机密钥校验失败',\n helper: '远端同步 helper 不可用',\n protocol: '同步协议不兼容',\n }\n const baseMsg = messages[error.category] ?? 'SSH 同步失败'\n const detail = (error as Error & { detail?: string }).detail\n return new SyncCoordinatorError(`ssh-${error.category}`, detail ? `${baseMsg}:${detail}` : baseMsg)\n }\n if (error instanceof SyncNodeManifestError) return new SyncCoordinatorError('manifest-invalid', '节点 manifest 校验失败')\n if (error instanceof SyncSessionError) {\n return new SyncCoordinatorError(error.code.startsWith('session-') ? error.code : `session-${error.code}`, error.message)\n }\n return new SyncCoordinatorError('sync-failed', '同步作业失败')\n}\n\nfunction resultCount(result: SyncNodeApplyResult): { applied: number; skipped: number; pendingRemoval: number } {\n return {\n applied: addCount(result),\n skipped: result.skipped,\n pendingRemoval: result.pendingRemoval,\n }\n}\n\nfunction emptySessionResult(): SyncSessionApplyResult {\n return { created: 0, updated: 0, archived: 0, deleted: 0, skipped: 0, unmapped: 0 }\n}\n\nfunction sessionResultCount(result: SyncSessionApplyResult): number {\n return result.created + result.updated + result.deleted\n}\n\nfunction addSessionResults(target: SyncSessionApplyResult, delta: SyncSessionApplyResult): SyncSessionApplyResult {\n return {\n created: target.created + delta.created,\n updated: target.updated + delta.updated,\n archived: target.archived + delta.archived,\n deleted: target.deleted + delta.deleted,\n skipped: target.skipped + delta.skipped,\n unmapped: target.unmapped + delta.unmapped,\n }\n}\n\nfunction mergeSessionSkipped(...reports: Array<readonly { sessionId: string; reason: string }[]>): Array<{ sessionId: string; reason: string }> {\n const merged = new Map<string, { sessionId: string; reason: string }>()\n for (const report of reports) {\n for (const entry of report) merged.set(JSON.stringify([entry.sessionId, entry.reason]), entry)\n }\n return [...merged.values()]\n}\n\nfunction assertSelectedSessionEntries(\n expectedEntries: readonly SyncSessionManifestEntry[],\n selectedEntries: readonly SyncSessionEntry[],\n side: string,\n): void {\n const expected = new Map(expectedEntries.map((entry) => [syncSessionEntryKey(entry), entry]))\n const selected = new Map(selectedEntries.map((entry) => [syncSessionEntryKey(entry), entry]))\n if (selected.size !== selectedEntries.length || selected.size !== expected.size) {\n throw new SyncCoordinatorError('session-manifest-stale', `${side}会话内容与 manifest 清单不一致`)\n }\n for (const [key, expectedEntry] of expected) {\n const metadata = expectedEntry.metadata\n const currentEntry = selected.get(key)\n const current = currentEntry?.metadata\n const eventSummary = currentEntry?.eventDelta\n ? {\n eventCount: currentEntry.eventDelta.fullEventCount,\n eventDigest: currentEntry.eventDelta.fullEventDigest,\n inheritedEventCount: currentEntry.inheritedEventCount,\n }\n : currentEntry?.events\n ? {\n eventCount: currentEntry.events.length,\n eventDigest: sessionEventListDigest(currentEntry.events),\n inheritedEventCount: currentEntry.inheritedEventCount,\n }\n : undefined\n const hasExpectedEventSummary = expectedEntry.eventCount !== undefined || expectedEntry.eventDigest !== undefined ||\n expectedEntry.inheritedEventCount !== undefined\n const expectedWorkspace = metadata.workspaceOrigin\n const currentWorkspace = current?.workspaceOrigin\n const mismatches: string[] = []\n if (!current) mismatches.push('entry-missing')\n else {\n if (current.localEntityId !== metadata.localEntityId) mismatches.push('localEntityId')\n if (compareSyncRevision(current.revision, metadata.revision) !== 0) mismatches.push('revision')\n if (current.contentDigest !== metadata.contentDigest) mismatches.push('contentDigest')\n if (current.stateDigest !== metadata.stateDigest) mismatches.push('stateDigest')\n if (Boolean(current.deleted) !== Boolean(metadata.deleted)) mismatches.push('deleted')\n if (expectedWorkspace?.originInstanceId !== currentWorkspace?.originInstanceId ||\n expectedWorkspace?.originEntityId !== currentWorkspace?.originEntityId) mismatches.push('workspaceOrigin')\n if (hasExpectedEventSummary && (!eventSummary || eventSummary.eventCount !== expectedEntry.eventCount ||\n eventSummary.eventDigest !== expectedEntry.eventDigest || eventSummary.inheritedEventCount !== expectedEntry.inheritedEventCount)) {\n mismatches.push('eventSummary')\n }\n }\n if (mismatches.length > 0) {\n const detail = current && hasExpectedEventSummary\n ? ` expected(eventCount=${expectedEntry.eventCount}, inherited=${expectedEntry.inheritedEventCount}, digest=${expectedEntry.eventDigest})` +\n ` actual(${eventSummary ? `eventCount=${eventSummary.eventCount}, inherited=${eventSummary.inheritedEventCount}, digest=${eventSummary.eventDigest}` : 'no-events'})`\n : ''\n throw new SyncCoordinatorError('session-manifest-stale', `${side}会话内容在 manifest 之后发生变化(key=${key},字段=${mismatches.join(',')})${detail}`)\n }\n }\n}\n\nfunction sessionEventPrefixesFor(\n selectedEntries: readonly SyncSessionManifestEntry[],\n receiverEntries: readonly SyncSessionManifestEntry[],\n): SyncSessionEventPrefix[] {\n const receiverByKey = new Map(receiverEntries.map((entry) => [syncSessionEntryKey(entry), entry]))\n const prefixes: SyncSessionEventPrefix[] = []\n for (const selected of selectedEntries) {\n if (selected.branchId || selected.metadata.deleted) continue\n const receiver = receiverByKey.get(syncSessionEntryKey(selected))\n if (!receiver || receiver.branchId || receiver.metadata.deleted || receiver.eventCount === undefined ||\n receiver.eventDigest === undefined || receiver.inheritedEventCount === undefined) continue\n prefixes.push({\n key: syncSessionEntryKey(selected),\n eventCount: receiver.eventCount,\n eventDigest: receiver.eventDigest,\n inheritedEventCount: receiver.inheritedEventCount,\n })\n }\n return prefixes\n}\n\nexport class SyncCoordinator {\n private readonly inFlight = new Map<string, Promise<SyncRunSummary>>()\n\n constructor(\n private readonly peers: SyncPeerManager,\n private readonly state: SyncStateStore,\n private readonly nodes: SyncNodeService,\n private readonly transport: SyncSshTransport,\n private readonly now: () => number = Date.now,\n private readonly sessions?: SyncSessionService,\n ) {}\n\n run(peerId: string): Promise<SyncRunSummary> {\n const active = this.inFlight.get(peerId)\n if (active) return active\n const run = this.execute(peerId)\n this.inFlight.set(peerId, run)\n void run.then(() => this.clear(peerId, run), () => this.clear(peerId, run))\n return run\n }\n\n private async execute(peerId: string): Promise<SyncRunSummary> {\n const peer = await this.peers.get(peerId)\n if (!peer) throw new SyncCoordinatorError('peer-not-found', '同步节点已不存在')\n if (!peer.enabled) throw new SyncCoordinatorError('peer-disabled', '此同步节点已停用')\n\n const startedAt = new Date(this.now()).toISOString()\n this.peers.updateProgress(peer.id, {\n lastRunAt: startedAt,\n lastRunStatus: 'running',\n lastErrorCode: undefined,\n lastErrorMessage: undefined,\n })\n\n try {\n if (peer.syncSessions && !this.sessions) throw new SyncCoordinatorError('sessions-unavailable', '会话同步服务不可用')\n const remoteNodeManifest = await this.retryTransport(() => this.transport.fetchNodeManifest(peer.id))\n if (remoteNodeManifest.protocolVersion !== 1 || remoteNodeManifest.instanceId === this.state.instanceId()) {\n throw new SyncCoordinatorError('protocol-invalid', '同步 peer 协议版本或实例身份无效')\n }\n const remoteEntries = this.nodes.validateManifest(remoteNodeManifest.records)\n const localManifest = await this.nodes.exportManifest()\n const localEntries = this.nodes.validateManifest(localManifest.records)\n const plan = planNodeTransfers(peer.direction, localEntries, remoteEntries)\n\n const localResult = plan.toLocal.length > 0\n ? await this.nodes.applyManifest(plan.toLocal, peer.name, 'peer')\n : EMPTY_APPLY_RESULT\n const remoteResult = plan.toRemote.length > 0\n ? await this.retryTransport(() => this.transport.applyNodeManifest(peer.id, plan.toRemote, localManifest.instanceId))\n : EMPTY_APPLY_RESULT\n const localCounts = resultCount(localResult)\n const remoteCounts = resultCount(remoteResult)\n let importedSessions = 0\n let exportedSessions = 0\n let skippedSessions = 0\n let digest = manifestDigest(remoteEntries)\n if (peer.syncSessions) {\n const sessionService = this.sessions!\n let plan: ReturnType<typeof planSessionTransfers>\n let remoteSkipped = 0\n let remoteSessionDigest = ''\n let localSessionDigest = ''\n let localInstanceId = ''\n let sessionLocalResult = emptySessionResult()\n let sessionRemoteResult = emptySessionResult()\n if (remoteNodeManifest.capabilities?.includes(SYNC_SESSION_MANIFEST_CAPABILITY)) {\n const remoteManifest = validateSyncSessionManifest(\n await this.retryTransport(() => this.transport.fetchSessionManifest(peer.id)),\n )\n if (remoteManifest.instanceId !== remoteNodeManifest.instanceId) {\n throw new SyncCoordinatorError('protocol-invalid', '节点与会话 manifest 的实例身份不一致')\n }\n const localManifest = await sessionService.exportManifest()\n plan = planSessionTransfers(peer.direction, localManifest.entries, remoteManifest.entries)\n remoteSessionDigest = remoteManifest.digest\n localSessionDigest = localManifest.digest\n localInstanceId = localManifest.instanceId\n const useEventDeltas = remoteNodeManifest.capabilities.includes(SYNC_SESSION_EVENT_TAIL_CAPABILITY)\n const useSharedObjects = remoteNodeManifest.capabilities.includes(SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY)\n const remoteSkippedReports: Array<readonly { sessionId: string; reason: string }[]> = [remoteManifest.skipped]\n\n // 分批交换:首次同步(或长时间未同步后)plan 可能包含全部会话,一次性\n // fetch/export/apply 会让两端同时把整包内容驻留堆内(桌面端 1.4GB 堆\n // OOM 崩溃的根因,2026-11-01 崩溃日志)。按观测到的单 key 体积自适应\n // 分批:每批 fetch → 断言 → apply,批间释放内存;小计划(≤ 单批上限)\n // 仍只发一次请求,行为与旧实现一致。\n\n // 拉取方向(远端 → 本地)\n if (plan.toLocal.length > 0) {\n let perKeyBytes = 0\n let firstChunk = true\n let cursor = 0\n const keys = plan.toLocal.map(syncSessionEntryKey)\n while (cursor < keys.length) {\n const take = perKeyBytes > 0\n ? Math.max(1, Math.min(SESSION_CHUNK_MAX_KEYS, Math.floor(SESSION_CHUNK_TARGET_BYTES / perKeyBytes)))\n : SESSION_CHUNK_MAX_KEYS\n const chunkKeys = keys.slice(cursor, cursor + take)\n cursor += chunkKeys.length\n const chunkKeySet = new Set(chunkKeys)\n const chunkManifest = plan.toLocal.filter((entry) => chunkKeySet.has(syncSessionEntryKey(entry)))\n const prefixes = useEventDeltas ? sessionEventPrefixesFor(chunkManifest, localManifest.entries) : []\n let fetched = validateSyncSessionBundle(await this.retryTransport(() =>\n this.transport.fetchSessionEntries(peer.id, chunkKeys, remoteManifest.digest, prefixes),\n ))\n if (fetched.instanceId !== remoteManifest.instanceId) {\n throw new SyncCoordinatorError('protocol-invalid', '远端会话内容与 manifest 实例不一致')\n }\n assertSelectedSessionEntries(chunkManifest, fetched.entries, '远端')\n remoteSkippedReports.push(fetched.skipped)\n const applySkipped = firstChunk ? mergeSessionSkipped(remoteManifest.skipped, fetched.skipped) : fetched.skipped\n try {\n const applied = await sessionService.applyBundle(\n makeSyncSessionBundle(remoteManifest.instanceId, fetched.entries, applySkipped),\n peer.name,\n peer.id,\n )\n sessionLocalResult = addSessionResults(sessionLocalResult, applied)\n } catch (error) {\n if (!(error instanceof SyncSessionError && error.code === 'session-delta-prefix')) throw error\n const fullRemoteEntries = validateSyncSessionBundle(await this.retryTransport(() =>\n this.transport.fetchSessionEntries(peer.id, chunkKeys, remoteManifest.digest),\n ))\n if (fullRemoteEntries.instanceId !== remoteManifest.instanceId) {\n throw new SyncCoordinatorError('protocol-invalid', '远端会话内容与 manifest 实例不一致')\n }\n assertSelectedSessionEntries(chunkManifest, fullRemoteEntries.entries, '远端')\n remoteSkippedReports.push(fullRemoteEntries.skipped)\n const applied = await sessionService.applyBundle(\n makeSyncSessionBundle(\n remoteManifest.instanceId,\n fullRemoteEntries.entries,\n firstChunk ? mergeSessionSkipped(remoteManifest.skipped, fullRemoteEntries.skipped) : fullRemoteEntries.skipped,\n ),\n peer.name,\n peer.id,\n )\n sessionLocalResult = addSessionResults(sessionLocalResult, applied)\n fetched = fullRemoteEntries\n }\n const chunkBytes = Buffer.byteLength(JSON.stringify(fetched), 'utf8')\n if (chunkKeys.length > 0) perKeyBytes = Math.max(perKeyBytes, Math.ceil(chunkBytes / chunkKeys.length))\n firstChunk = false\n }\n } else if (remoteManifest.skipped.length > 0) {\n const applied = await sessionService.applyBundle(\n makeSyncSessionBundle(remoteManifest.instanceId, [], remoteManifest.skipped),\n peer.name,\n peer.id,\n )\n sessionLocalResult = addSessionResults(sessionLocalResult, applied)\n }\n remoteSkipped = mergeSessionSkipped(...remoteSkippedReports).length\n\n // 推送方向(本地 → 远端)\n if (plan.toRemote.length > 0) {\n const keys = plan.toRemote.map(syncSessionEntryKey)\n let perKeyBytes = 0\n let firstChunk = true\n let cursor = 0\n while (cursor < keys.length) {\n const take = perKeyBytes > 0\n ? Math.max(1, Math.min(SESSION_CHUNK_MAX_KEYS, Math.floor(SESSION_CHUNK_TARGET_BYTES / perKeyBytes)))\n : SESSION_CHUNK_MAX_KEYS\n const chunkKeys = keys.slice(cursor, cursor + take)\n cursor += chunkKeys.length\n const chunkKeySet = new Set(chunkKeys)\n const chunkManifest = plan.toRemote.filter((entry) => chunkKeySet.has(syncSessionEntryKey(entry)))\n const prefixes = useEventDeltas ? sessionEventPrefixesFor(chunkManifest, remoteManifest.entries) : []\n const exported = await sessionService.exportEntries(\n chunkKeys,\n localManifest.digest,\n prefixes,\n { sharedObjects: useSharedObjects },\n )\n assertSelectedSessionEntries(chunkManifest, exported.entries, '本地')\n const wireSkipped = firstChunk ? mergeSessionSkipped(localManifest.skipped, exported.skipped) : exported.skipped\n let wireBundle = exported.objects !== undefined\n ? makeSyncSessionBundle(exported.instanceId, exported.entries, wireSkipped, exported.objects)\n : makeSyncSessionBundle(exported.instanceId, exported.entries, wireSkipped)\n try {\n const applied = await this.retryTransport(() =>\n this.transport.applySessionBundle(peer.id, wireBundle, localInstanceId),\n )\n sessionRemoteResult = addSessionResults(sessionRemoteResult, applied)\n } catch (error) {\n if (!(error instanceof SyncSessionDeltaPrefixError)) throw error\n const fullLocalEntries = await sessionService.exportEntries(\n chunkKeys,\n localManifest.digest,\n [],\n { sharedObjects: wireBundle.objects !== undefined },\n )\n assertSelectedSessionEntries(chunkManifest, fullLocalEntries.entries, '本地')\n const skippedReport = firstChunk ? mergeSessionSkipped(localManifest.skipped, fullLocalEntries.skipped) : fullLocalEntries.skipped\n wireBundle = fullLocalEntries.objects !== undefined\n ? makeSyncSessionBundle(fullLocalEntries.instanceId, fullLocalEntries.entries, skippedReport, fullLocalEntries.objects)\n : makeSyncSessionBundle(fullLocalEntries.instanceId, fullLocalEntries.entries, skippedReport)\n const applied = await this.retryTransport(() =>\n this.transport.applySessionBundle(peer.id, wireBundle, localInstanceId),\n )\n sessionRemoteResult = addSessionResults(sessionRemoteResult, applied)\n }\n const chunkBytes = Buffer.byteLength(JSON.stringify(wireBundle), 'utf8')\n if (chunkKeys.length > 0) perKeyBytes = Math.max(perKeyBytes, Math.ceil(chunkBytes / chunkKeys.length))\n firstChunk = false\n }\n }\n } else {\n const remoteBundle = validateSyncSessionBundle(await this.retryTransport(() => this.transport.fetchSessionBundle(peer.id)))\n if (remoteBundle.instanceId !== remoteNodeManifest.instanceId) {\n throw new SyncCoordinatorError('protocol-invalid', '节点与会话 manifest 的实例身份不一致')\n }\n const localBundle = await sessionService.exportBundle()\n plan = planSessionTransfers(peer.direction, localBundle.entries, remoteBundle.entries)\n remoteSkipped = remoteBundle.skipped.length\n remoteSessionDigest = remoteBundle.digest\n localSessionDigest = localBundle.digest\n localInstanceId = localBundle.instanceId\n if (plan.toLocal.length > 0 || remoteSkipped > 0) {\n sessionLocalResult = await sessionService.applyBundle(\n makeSyncSessionBundle(remoteBundle.instanceId, plan.toLocal, remoteBundle.skipped),\n peer.name,\n peer.id,\n )\n }\n if (plan.toRemote.length > 0) {\n sessionRemoteResult = await this.retryTransport(() =>\n this.transport.applySessionBundle(\n peer.id,\n makeSyncSessionBundle(localBundle.instanceId, plan.toRemote, localBundle.skipped),\n localInstanceId,\n ),\n )\n }\n }\n importedSessions = sessionResultCount(sessionLocalResult)\n exportedSessions = sessionResultCount(sessionRemoteResult)\n skippedSessions = plan.skipped + sessionLocalResult.skipped + sessionRemoteResult.skipped\n digest = `sha256:${createHash('sha256').update(`${digest}\\0${remoteSessionDigest}\\0${localSessionDigest}`).digest('hex')}`\n }\n const finishedAt = new Date(this.now()).toISOString()\n const summary: SyncRunSummary = {\n peerId,\n direction: peer.direction,\n startedAt,\n finishedAt,\n remoteInstanceId: remoteNodeManifest.instanceId,\n imported: localCounts.applied,\n exported: remoteCounts.applied,\n skipped: plan.skipped + localCounts.skipped + remoteCounts.skipped + skippedSessions,\n pendingRemoval: localCounts.pendingRemoval + remoteCounts.pendingRemoval,\n importedSessions,\n exportedSessions,\n skippedSessions,\n }\n\n this.peers.updateProgress(peer.id, {\n lastRunAt: finishedAt,\n lastRunStatus: 'succeeded',\n lastSuccessAt: finishedAt,\n lastManifestDigest: digest,\n watermark: digest,\n lastErrorCode: undefined,\n lastErrorMessage: undefined,\n lastResult: {\n imported: summary.imported,\n exported: summary.exported,\n skipped: summary.skipped,\n pendingRemoval: summary.pendingRemoval,\n importedSessions,\n exportedSessions,\n skippedSessions,\n },\n })\n return summary\n } catch (error) {\n const failure = mapFailure(error)\n if (!(error instanceof SyncCoordinatorError) && !(error instanceof SyncSshProbeError) && !(error instanceof SyncSessionError) && !(error instanceof SyncNodeManifestError) && !(error instanceof SyncHostKeyChallengeError) && !(error instanceof SyncPeerNotFoundError)) {\n failure.message = `${failure.message}:${error instanceof Error ? error.message : String(error)}`\n }\n try {\n this.peers.updateProgress(peer.id, {\n lastRunAt: new Date(this.now()).toISOString(),\n lastRunStatus: 'failed',\n lastErrorCode: failure.code,\n lastErrorMessage: failure.message,\n })\n } catch {}\n throw failure\n }\n }\n\n private clear(peerId: string, run: Promise<SyncRunSummary>): void {\n if (this.inFlight.get(peerId) === run) this.inFlight.delete(peerId)\n }\n\n private async retryTransport<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation()\n } catch (error) {\n if (!(error instanceof SyncSshProbeError) || !['network', 'timeout', 'helper'].includes(error.category)) throw error\n await new Promise((resolve) => setTimeout(resolve, TRANSPORT_RETRY_DELAY_MS))\n return operation()\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as SyncStateStore } from "../../stores-CKyEt0yk.js";
|
|
2
2
|
import { s as SyncNodeService } from "../../node-sync-3Z9QLz2n.js";
|
|
3
|
-
import {
|
|
3
|
+
import { p as SyncSessionService } from "../../session-sync-Zfo1Q-qT.js";
|
|
4
4
|
import { Context } from "@deepseek-ai/cordis";
|
|
5
5
|
//#region src/core/sync/internal-routes.d.ts
|
|
6
6
|
declare function registerSyncInternalRoutes(ctx: Context, state: SyncStateStore, nodeSync?: SyncNodeService, sessionSync?: SyncSessionService): void;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { isLoopbackRequest } from "../../auth/loopback.js";
|
|
2
2
|
import { SyncNodeManifestError } from "./node-sync.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { n as SyncSessionError } from "../../session-sync-BQt3HXkp.js";
|
|
4
|
+
import { SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY, SYNC_SESSION_EVENT_TAIL_CAPABILITY, SYNC_SESSION_MANIFEST_CAPABILITY } from "./protocol.js";
|
|
5
5
|
//#region src/core/sync/internal-routes.ts
|
|
6
|
-
const MAX_APPLY_BODY_BYTES =
|
|
6
|
+
const MAX_APPLY_BODY_BYTES = 134217728;
|
|
7
7
|
function sendJson(res, status, body) {
|
|
8
8
|
res.writeHead(status, {
|
|
9
9
|
"content-type": "application/json",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"internal-routes.js","names":[],"sources":["../../../src/core/sync/internal-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 { isLoopbackRequest } from '../../auth/loopback.js'\nimport { SyncNodeManifestError, type SyncNodeService } from './node-sync.js'\nimport { SyncSessionError, type SyncSessionEventPrefix, type SyncSessionService } from './session-sync.js'\nimport {\n MAX_SYNC_TRANSFER_BYTES,\n SYNC_PROTOCOL_VERSION,\n SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY,\n SYNC_SESSION_EVENT_TAIL_CAPABILITY,\n SYNC_SESSION_MANIFEST_CAPABILITY,\n} from './protocol.js'\nimport type { SyncStateStore } from './stores.js'\n\nconst MAX_APPLY_BODY_BYTES = MAX_SYNC_TRANSFER_BYTES\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' })\n res.end(JSON.stringify(body))\n}\n\nfunction pathSegments(req: IncomingMessage): string[] {\n return new URL(req.url ?? '/', 'http://x').pathname.split('/').filter(Boolean)\n}\n\nfunction readJsonBody(req: IncomingMessage): Promise<Record<string, unknown> | null> {\n return new Promise((resolve) => {\n if (req.headers['content-type']?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json') {\n resolve(null)\n return\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_APPLY_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\nexport function registerSyncInternalRoutes(\n ctx: Context,\n state: SyncStateStore,\n nodeSync?: SyncNodeService,\n sessionSync?: SyncSessionService,\n): void {\n const capabilities = [\n 'peer-handshake',\n ...(nodeSync ? ['node-sync'] : []),\n ...(sessionSync ? ['session-sync', SYNC_SESSION_MANIFEST_CAPABILITY, SYNC_SESSION_EVENT_TAIL_CAPABILITY, SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY] : []),\n ]\n ctx.effect(() =>\n ctx.webServer.register({\n kind: 'exact',\n path: '/api/dshb/sync/internal/handshake',\n handler: (req: IncomingMessage, res: ServerResponse) => {\n if (!isLoopbackRequest(req.socket.remoteAddress, req.headers.host)) {\n sendJson(res, 403, { ok: false, error: 'loopback access required' })\n return\n }\n if (req.method !== 'GET') {\n sendJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n try {\n sendJson(res, 200, {\n ok: true,\n protocolVersion: SYNC_PROTOCOL_VERSION,\n instanceId: state.instanceId(),\n capabilities,\n })\n } catch {\n sendJson(res, 503, { ok: false, error: 'sync identity unavailable' })\n }\n },\n }),\n )\n\n if (nodeSync) ctx.effect(() =>\n ctx.webServer.register({\n kind: 'prefix',\n path: '/api/dshb/sync/internal/nodes',\n handler: async (req, res) => {\n if (!isLoopbackRequest(req.socket.remoteAddress, req.headers.host)) {\n sendJson(res, 403, { ok: false, error: 'loopback access required' })\n return\n }\n const segments = pathSegments(req)\n const action = segments[5]\n if (segments.length !== 6) {\n sendJson(res, 404, { ok: false, error: 'not found' })\n return\n }\n if (req.method === 'GET' && action === 'manifest') {\n try {\n sendJson(res, 200, { ok: true, manifest: await nodeSync.exportManifest(), capabilities })\n } catch {\n sendJson(res, 500, { ok: false, error: 'node manifest export failed' })\n }\n return\n }\n if (req.method === 'POST' && action === 'apply') {\n const body = await readJsonBody(req)\n if (!body || !Array.isArray(body.records) || typeof body.sourceName !== 'string') {\n sendJson(res, 400, { ok: false, error: 'records and sourceName are required' })\n return\n }\n try {\n sendJson(res, 200, { ok: true, result: await nodeSync.applyManifest(body.records, body.sourceName) })\n } catch (error) {\n const status = error instanceof SyncNodeManifestError ? 400 : 500\n sendJson(res, status, { ok: false, error: status === 400 ? 'node manifest rejected' : 'node manifest apply failed' })\n }\n return\n }\n sendJson(res, 404, { ok: false, error: 'not found' })\n },\n }),\n )\n\n if (!sessionSync) return\n ctx.effect(() =>\n ctx.webServer.register({\n kind: 'prefix',\n path: '/api/dshb/sync/internal/sessions',\n handler: async (req, res) => {\n if (!isLoopbackRequest(req.socket.remoteAddress, req.headers.host)) {\n sendJson(res, 403, { ok: false, error: 'loopback access required' })\n return\n }\n const segments = pathSegments(req)\n const action = segments[5]\n if (segments.length !== 6) {\n sendJson(res, 404, { ok: false, error: 'not found' })\n return\n }\n if (req.method === 'GET' && action === 'bundle') {\n try {\n sendJson(res, 200, { ok: true, bundle: await sessionSync.exportBundle() })\n } catch (error) {\n const code = error instanceof SyncSessionError ? error.code : 'session-export-failed'\n const status = code === 'bundle-too-large' ? 413 : 500\n sendJson(res, status, { ok: false, code, error: error instanceof SyncSessionError ? error.message : 'session bundle export failed' })\n }\n return\n }\n if (req.method === 'GET' && action === 'manifest') {\n try {\n sendJson(res, 200, { ok: true, manifest: await sessionSync.exportManifest() })\n } catch (error) {\n const code = error instanceof SyncSessionError ? error.code : 'session-export-failed'\n const status = code === 'bundle-too-large' ? 413 : 500\n sendJson(res, status, { ok: false, code, error: error instanceof SyncSessionError ? error.message : 'session manifest export failed' })\n }\n return\n }\n if (req.method === 'POST' && action === 'entries') {\n const body = await readJsonBody(req)\n const prefixesValid = body?.prefixes === undefined || (Array.isArray(body.prefixes) &&\n body.prefixes.every((prefix) => prefix !== null && typeof prefix === 'object' && !Array.isArray(prefix)))\n if (!body || !Array.isArray(body.keys) || !body.keys.every((key) => typeof key === 'string') ||\n typeof body.manifestDigest !== 'string' || !prefixesValid) {\n sendJson(res, 400, { ok: false, code: 'session-request-invalid', error: 'keys, manifestDigest, and valid prefixes are required' })\n return\n }\n try {\n sendJson(res, 200, {\n ok: true,\n bundle: await sessionSync.exportEntries(\n body.keys as string[],\n body.manifestDigest,\n (body.prefixes ?? []) as SyncSessionEventPrefix[],\n ),\n })\n } catch (error) {\n const code = error instanceof SyncSessionError ? error.code : 'session-export-failed'\n const status = code === 'bundle-too-large' ? 413 : 500\n sendJson(res, status, { ok: false, code, error: error instanceof SyncSessionError ? error.message : 'session entries export failed' })\n }\n return\n }\n if (req.method === 'POST' && action === 'apply') {\n const body = await readJsonBody(req)\n if (!body || body.bundle === null || typeof body.bundle !== 'object' ||\n typeof body.sourceName !== 'string' || typeof body.peerId !== 'string' || !/^p-[a-f0-9]{24}$/.test(body.peerId)) {\n sendJson(res, 400, { ok: false, code: 'session-request-invalid', error: 'bundle, sourceName, and peerId are required' })\n return\n }\n try {\n sendJson(res, 200, {\n ok: true,\n result: await sessionSync.applyBundle(body.bundle, body.sourceName, body.peerId),\n })\n } catch (error) {\n const code = error instanceof SyncSessionError ? error.code : 'session-apply-failed'\n const status = code === 'bundle-too-large' ? 413\n : ['manifest-invalid', 'manifest-digest', 'object-invalid', 'session-format'].includes(code) ? 400\n : code === 'workspace-unmapped' || code === 'session-delta-prefix' ? 409 : 500\n sendJson(res, status, { ok: false, code, error: error instanceof SyncSessionError ? error.message : 'session bundle apply failed' })\n }\n return\n }\n sendJson(res, 404, { ok: false, error: 'not found' })\n },\n }),\n )\n}\n"],"mappings":";;;;;AAeA,MAAM,uBAAuB;AAE7B,SAAS,SAAS,KAAqB,QAAgB,MAAqB;CAC1E,IAAI,UAAU,QAAQ;EAAE,gBAAgB;EAAoB,iBAAiB;CAAW,CAAC;CACzF,IAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAC9B;AAEA,SAAS,aAAa,KAAgC;CACpD,OAAO,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC,CAAC,SAAS,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO;AAC/E;AAEA,SAAS,aAAa,KAA+D;CACnF,OAAO,IAAI,SAAS,YAAY;EAC9B,IAAI,IAAI,QAAQ,eAAe,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,YAAY,MAAM,oBAAoB;GAC9F,QAAQ,IAAI;GACZ;EACF;EACA,MAAM,SAAmB,CAAC;EAC1B,IAAI,OAAO;EACX,IAAI,UAAU;EACd,MAAM,aAAmB;GACvB,IAAI,SAAS;GACb,UAAU;GACV,QAAQ,IAAI;EACd;EACA,IAAI,GAAG,SAAS,UAAkB;GAChC,IAAI,SAAS;GACb,QAAQ,MAAM;GACd,IAAI,OAAO,sBAAsB;IAC/B,KAAK;IACL,IAAI,QAAQ;IACZ;GACF;GACA,OAAO,KAAK,KAAK;EACnB,CAAC;EACD,IAAI,GAAG,aAAa;GAClB,IAAI,SAAS;GACb,IAAI;IACF,MAAM,QAAiB,KAAK,MAAM,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC;IACxE,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG;KACvE,KAAK;KACL;IACF;IACA,UAAU;IACV,QAAQ,KAAgC;GAC1C,QAAQ;IACN,KAAK;GACP;EACF,CAAC;EACD,IAAI,GAAG,SAAS,IAAI;CACtB,CAAC;AACH;AAEA,SAAgB,2BACd,KACA,OACA,UACA,aACM;CACN,MAAM,eAAe;EACnB;EACA,GAAI,WAAW,CAAC,WAAW,IAAI,CAAC;EAChC,GAAI,cAAc;GAAC;GAAgB;GAAkC;GAAoC;EAAsC,IAAI,CAAC;CACtJ;CACA,IAAI,aACF,IAAI,UAAU,SAAS;EACrB,MAAM;EACN,MAAM;EACN,UAAU,KAAsB,QAAwB;GACtD,IAAI,CAAC,kBAAkB,IAAI,OAAO,eAAe,IAAI,QAAQ,IAAI,GAAG;IAClE,SAAS,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAA2B,CAAC;IACnE;GACF;GACA,IAAI,IAAI,WAAW,OAAO;IACxB,SAAS,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAAqB,CAAC;IAC7D;GACF;GACA,IAAI;IACF,SAAS,KAAK,KAAK;KACjB,IAAI;KACJ,iBAAA;KACA,YAAY,MAAM,WAAW;KAC7B;IACF,CAAC;GACH,QAAQ;IACN,SAAS,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAA4B,CAAC;GACtE;EACF;CACF,CAAC,CACH;CAEA,IAAI,UAAU,IAAI,aAChB,IAAI,UAAU,SAAS;EACrB,MAAM;EACN,MAAM;EACN,SAAS,OAAO,KAAK,QAAQ;GAC3B,IAAI,CAAC,kBAAkB,IAAI,OAAO,eAAe,IAAI,QAAQ,IAAI,GAAG;IAClE,SAAS,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAA2B,CAAC;IACnE;GACF;GACA,MAAM,WAAW,aAAa,GAAG;GACjC,MAAM,SAAS,SAAS;GACxB,IAAI,SAAS,WAAW,GAAG;IACzB,SAAS,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAAY,CAAC;IACpD;GACF;GACA,IAAI,IAAI,WAAW,SAAS,WAAW,YAAY;IACjD,IAAI;KACF,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM,UAAU,MAAM,SAAS,eAAe;MAAG;KAAa,CAAC;IAC1F,QAAQ;KACN,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAA8B,CAAC;IACxE;IACA;GACF;GACA,IAAI,IAAI,WAAW,UAAU,WAAW,SAAS;IAC/C,MAAM,OAAO,MAAM,aAAa,GAAG;IACnC,IAAI,CAAC,QAAQ,CAAC,MAAM,QAAQ,KAAK,OAAO,KAAK,OAAO,KAAK,eAAe,UAAU;KAChF,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAsC,CAAC;KAC9E;IACF;IACA,IAAI;KACF,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM,QAAQ,MAAM,SAAS,cAAc,KAAK,SAAS,KAAK,UAAU;KAAE,CAAC;IACtG,SAAS,OAAO;KACd,MAAM,SAAS,iBAAiB,wBAAwB,MAAM;KAC9D,SAAS,KAAK,QAAQ;MAAE,IAAI;MAAO,OAAO,WAAW,MAAM,2BAA2B;KAA6B,CAAC;IACtH;IACA;GACF;GACA,SAAS,KAAK,KAAK;IAAE,IAAI;IAAO,OAAO;GAAY,CAAC;EACtD;CACF,CAAC,CACH;CAEA,IAAI,CAAC,aAAa;CAClB,IAAI,aACF,IAAI,UAAU,SAAS;EACrB,MAAM;EACN,MAAM;EACN,SAAS,OAAO,KAAK,QAAQ;GAC3B,IAAI,CAAC,kBAAkB,IAAI,OAAO,eAAe,IAAI,QAAQ,IAAI,GAAG;IAClE,SAAS,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAA2B,CAAC;IACnE;GACF;GACA,MAAM,WAAW,aAAa,GAAG;GACjC,MAAM,SAAS,SAAS;GACxB,IAAI,SAAS,WAAW,GAAG;IACzB,SAAS,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAAY,CAAC;IACpD;GACF;GACA,IAAI,IAAI,WAAW,SAAS,WAAW,UAAU;IAC/C,IAAI;KACF,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM,QAAQ,MAAM,YAAY,aAAa;KAAE,CAAC;IAC3E,SAAS,OAAO;KACd,MAAM,OAAO,iBAAiB,mBAAmB,MAAM,OAAO;KAE9D,SAAS,KADM,SAAS,qBAAqB,MAAM,KAC7B;MAAE,IAAI;MAAO;MAAM,OAAO,iBAAiB,mBAAmB,MAAM,UAAU;KAA+B,CAAC;IACtI;IACA;GACF;GACA,IAAI,IAAI,WAAW,SAAS,WAAW,YAAY;IACjD,IAAI;KACF,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM,UAAU,MAAM,YAAY,eAAe;KAAE,CAAC;IAC/E,SAAS,OAAO;KACd,MAAM,OAAO,iBAAiB,mBAAmB,MAAM,OAAO;KAE9D,SAAS,KADM,SAAS,qBAAqB,MAAM,KAC7B;MAAE,IAAI;MAAO;MAAM,OAAO,iBAAiB,mBAAmB,MAAM,UAAU;KAAiC,CAAC;IACxI;IACA;GACF;GACA,IAAI,IAAI,WAAW,UAAU,WAAW,WAAW;IACjD,MAAM,OAAO,MAAM,aAAa,GAAG;IACnC,MAAM,gBAAgB,MAAM,aAAa,KAAA,KAAc,MAAM,QAAQ,KAAK,QAAQ,KAChF,KAAK,SAAS,OAAO,WAAW,WAAW,QAAQ,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,CAAC;IACzG,IAAI,CAAC,QAAQ,CAAC,MAAM,QAAQ,KAAK,IAAI,KAAK,CAAC,KAAK,KAAK,OAAO,QAAQ,OAAO,QAAQ,QAAQ,KACzF,OAAO,KAAK,mBAAmB,YAAY,CAAC,eAAe;KAC3D,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,MAAM;MAA2B,OAAO;KAAwD,CAAC;KACjI;IACF;IACA,IAAI;KACF,SAAS,KAAK,KAAK;MACjB,IAAI;MACJ,QAAQ,MAAM,YAAY,cACxB,KAAK,MACL,KAAK,gBACJ,KAAK,YAAY,CAAC,CACrB;KACF,CAAC;IACH,SAAS,OAAO;KACd,MAAM,OAAO,iBAAiB,mBAAmB,MAAM,OAAO;KAE9D,SAAS,KADM,SAAS,qBAAqB,MAAM,KAC7B;MAAE,IAAI;MAAO;MAAM,OAAO,iBAAiB,mBAAmB,MAAM,UAAU;KAAgC,CAAC;IACvI;IACA;GACF;GACA,IAAI,IAAI,WAAW,UAAU,WAAW,SAAS;IAC/C,MAAM,OAAO,MAAM,aAAa,GAAG;IACnC,IAAI,CAAC,QAAQ,KAAK,WAAW,QAAQ,OAAO,KAAK,WAAW,YAC1D,OAAO,KAAK,eAAe,YAAY,OAAO,KAAK,WAAW,YAAY,CAAC,mBAAmB,KAAK,KAAK,MAAM,GAAG;KACjH,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,MAAM;MAA2B,OAAO;KAA8C,CAAC;KACvH;IACF;IACA,IAAI;KACF,SAAS,KAAK,KAAK;MACjB,IAAI;MACJ,QAAQ,MAAM,YAAY,YAAY,KAAK,QAAQ,KAAK,YAAY,KAAK,MAAM;KACjF,CAAC;IACH,SAAS,OAAO;KACd,MAAM,OAAO,iBAAiB,mBAAmB,MAAM,OAAO;KAI9D,SAAS,KAHM,SAAS,qBAAqB,MACzC;MAAC;MAAoB;MAAmB;MAAkB;KAAgB,CAAC,CAAC,SAAS,IAAI,IAAI,MAC3F,SAAS,wBAAwB,SAAS,yBAAyB,MAAM,KACzD;MAAE,IAAI;MAAO;MAAM,OAAO,iBAAiB,mBAAmB,MAAM,UAAU;KAA8B,CAAC;IACrI;IACA;GACF;GACA,SAAS,KAAK,KAAK;IAAE,IAAI;IAAO,OAAO;GAAY,CAAC;EACtD;CACF,CAAC,CACH;AACF"}
|
|
1
|
+
{"version":3,"file":"internal-routes.js","names":[],"sources":["../../../src/core/sync/internal-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 { isLoopbackRequest } from '../../auth/loopback.js'\nimport { SyncNodeManifestError, type SyncNodeService } from './node-sync.js'\nimport { SyncSessionError, type SyncSessionEventPrefix, type SyncSessionService } from './session-sync.js'\nimport {\n SYNC_PROTOCOL_VERSION,\n SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY,\n SYNC_SESSION_EVENT_TAIL_CAPABILITY,\n SYNC_SESSION_MANIFEST_CAPABILITY,\n} from './protocol.js'\nimport type { SyncStateStore } from './stores.js'\n\n// 与 MAX_SYNC_TRANSFER_BYTES 对齐的请求体硬上限:超限直接断开(resolve null → 400),\n// 避免对端(或恶意请求)把宿主堆吃满。协调器分批后正常请求远小于该值。\nconst MAX_APPLY_BODY_BYTES = 128 * 1024 * 1024\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' })\n res.end(JSON.stringify(body))\n}\n\nfunction pathSegments(req: IncomingMessage): string[] {\n return new URL(req.url ?? '/', 'http://x').pathname.split('/').filter(Boolean)\n}\n\nfunction readJsonBody(req: IncomingMessage): Promise<Record<string, unknown> | null> {\n return new Promise((resolve) => {\n if (req.headers['content-type']?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json') {\n resolve(null)\n return\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_APPLY_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\nexport function registerSyncInternalRoutes(\n ctx: Context,\n state: SyncStateStore,\n nodeSync?: SyncNodeService,\n sessionSync?: SyncSessionService,\n): void {\n const capabilities = [\n 'peer-handshake',\n ...(nodeSync ? ['node-sync'] : []),\n ...(sessionSync ? ['session-sync', SYNC_SESSION_MANIFEST_CAPABILITY, SYNC_SESSION_EVENT_TAIL_CAPABILITY, SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY] : []),\n ]\n ctx.effect(() =>\n ctx.webServer.register({\n kind: 'exact',\n path: '/api/dshb/sync/internal/handshake',\n handler: (req: IncomingMessage, res: ServerResponse) => {\n if (!isLoopbackRequest(req.socket.remoteAddress, req.headers.host)) {\n sendJson(res, 403, { ok: false, error: 'loopback access required' })\n return\n }\n if (req.method !== 'GET') {\n sendJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n try {\n sendJson(res, 200, {\n ok: true,\n protocolVersion: SYNC_PROTOCOL_VERSION,\n instanceId: state.instanceId(),\n capabilities,\n })\n } catch {\n sendJson(res, 503, { ok: false, error: 'sync identity unavailable' })\n }\n },\n }),\n )\n\n if (nodeSync) ctx.effect(() =>\n ctx.webServer.register({\n kind: 'prefix',\n path: '/api/dshb/sync/internal/nodes',\n handler: async (req, res) => {\n if (!isLoopbackRequest(req.socket.remoteAddress, req.headers.host)) {\n sendJson(res, 403, { ok: false, error: 'loopback access required' })\n return\n }\n const segments = pathSegments(req)\n const action = segments[5]\n if (segments.length !== 6) {\n sendJson(res, 404, { ok: false, error: 'not found' })\n return\n }\n if (req.method === 'GET' && action === 'manifest') {\n try {\n sendJson(res, 200, { ok: true, manifest: await nodeSync.exportManifest(), capabilities })\n } catch {\n sendJson(res, 500, { ok: false, error: 'node manifest export failed' })\n }\n return\n }\n if (req.method === 'POST' && action === 'apply') {\n const body = await readJsonBody(req)\n if (!body || !Array.isArray(body.records) || typeof body.sourceName !== 'string') {\n sendJson(res, 400, { ok: false, error: 'records and sourceName are required' })\n return\n }\n try {\n sendJson(res, 200, { ok: true, result: await nodeSync.applyManifest(body.records, body.sourceName) })\n } catch (error) {\n const status = error instanceof SyncNodeManifestError ? 400 : 500\n sendJson(res, status, { ok: false, error: status === 400 ? 'node manifest rejected' : 'node manifest apply failed' })\n }\n return\n }\n sendJson(res, 404, { ok: false, error: 'not found' })\n },\n }),\n )\n\n if (!sessionSync) return\n ctx.effect(() =>\n ctx.webServer.register({\n kind: 'prefix',\n path: '/api/dshb/sync/internal/sessions',\n handler: async (req, res) => {\n if (!isLoopbackRequest(req.socket.remoteAddress, req.headers.host)) {\n sendJson(res, 403, { ok: false, error: 'loopback access required' })\n return\n }\n const segments = pathSegments(req)\n const action = segments[5]\n if (segments.length !== 6) {\n sendJson(res, 404, { ok: false, error: 'not found' })\n return\n }\n if (req.method === 'GET' && action === 'bundle') {\n try {\n sendJson(res, 200, { ok: true, bundle: await sessionSync.exportBundle() })\n } catch (error) {\n const code = error instanceof SyncSessionError ? error.code : 'session-export-failed'\n const status = code === 'bundle-too-large' ? 413 : 500\n sendJson(res, status, { ok: false, code, error: error instanceof SyncSessionError ? error.message : 'session bundle export failed' })\n }\n return\n }\n if (req.method === 'GET' && action === 'manifest') {\n try {\n sendJson(res, 200, { ok: true, manifest: await sessionSync.exportManifest() })\n } catch (error) {\n const code = error instanceof SyncSessionError ? error.code : 'session-export-failed'\n const status = code === 'bundle-too-large' ? 413 : 500\n sendJson(res, status, { ok: false, code, error: error instanceof SyncSessionError ? error.message : 'session manifest export failed' })\n }\n return\n }\n if (req.method === 'POST' && action === 'entries') {\n const body = await readJsonBody(req)\n const prefixesValid = body?.prefixes === undefined || (Array.isArray(body.prefixes) &&\n body.prefixes.every((prefix) => prefix !== null && typeof prefix === 'object' && !Array.isArray(prefix)))\n if (!body || !Array.isArray(body.keys) || !body.keys.every((key) => typeof key === 'string') ||\n typeof body.manifestDigest !== 'string' || !prefixesValid) {\n sendJson(res, 400, { ok: false, code: 'session-request-invalid', error: 'keys, manifestDigest, and valid prefixes are required' })\n return\n }\n try {\n sendJson(res, 200, {\n ok: true,\n bundle: await sessionSync.exportEntries(\n body.keys as string[],\n body.manifestDigest,\n (body.prefixes ?? []) as SyncSessionEventPrefix[],\n ),\n })\n } catch (error) {\n const code = error instanceof SyncSessionError ? error.code : 'session-export-failed'\n const status = code === 'bundle-too-large' ? 413 : 500\n sendJson(res, status, { ok: false, code, error: error instanceof SyncSessionError ? error.message : 'session entries export failed' })\n }\n return\n }\n if (req.method === 'POST' && action === 'apply') {\n const body = await readJsonBody(req)\n if (!body || body.bundle === null || typeof body.bundle !== 'object' ||\n typeof body.sourceName !== 'string' || typeof body.peerId !== 'string' || !/^p-[a-f0-9]{24}$/.test(body.peerId)) {\n sendJson(res, 400, { ok: false, code: 'session-request-invalid', error: 'bundle, sourceName, and peerId are required' })\n return\n }\n try {\n sendJson(res, 200, {\n ok: true,\n result: await sessionSync.applyBundle(body.bundle, body.sourceName, body.peerId),\n })\n } catch (error) {\n const code = error instanceof SyncSessionError ? error.code : 'session-apply-failed'\n const status = code === 'bundle-too-large' ? 413\n : ['manifest-invalid', 'manifest-digest', 'object-invalid', 'session-format'].includes(code) ? 400\n : code === 'workspace-unmapped' || code === 'session-delta-prefix' ? 409 : 500\n sendJson(res, status, { ok: false, code, error: error instanceof SyncSessionError ? error.message : 'session bundle apply failed' })\n }\n return\n }\n sendJson(res, 404, { ok: false, error: 'not found' })\n },\n }),\n )\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as SyncStateStore } from "../../stores-CKyEt0yk.js";
|
|
2
2
|
import { s as SyncNodeService } from "../../node-sync-3Z9QLz2n.js";
|
|
3
|
-
import {
|
|
3
|
+
import { p as SyncSessionService } from "../../session-sync-Zfo1Q-qT.js";
|
|
4
4
|
//#region src/core/sync/offline-archive.d.ts
|
|
5
5
|
declare const MAX_OFFLINE_ARCHIVE_BYTES: number;
|
|
6
6
|
declare class SyncOfflineArchiveError extends Error {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as SyncSessionError, u as validateSyncSessionBundle } from "../../session-sync-BQt3HXkp.js";
|
|
2
2
|
import { SyncOfflineArchiveJournalStore } from "./offline-journal.js";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { appendFileSync, chmodSync, closeSync, createReadStream, createWriteStream, mkdirSync, mkdtempSync, openSync, readSync, readdirSync, rmSync, statSync, statfsSync, writeFileSync } from "node:fs";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as SyncSessionError } from "../../session-sync-BQt3HXkp.js";
|
|
2
2
|
import { SyncOfflineArchiveError } from "./offline-archive.js";
|
|
3
3
|
import { createReadStream } from "node:fs";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
@@ -6,7 +6,7 @@ const SYNC_SESSION_EVENT_TAIL_CAPABILITY = "session-event-tail-delta-v1";
|
|
|
6
6
|
const SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY = "session-bundle-objects-v1";
|
|
7
7
|
const MAX_SYNC_CONTROL_FRAME_BYTES = 65536;
|
|
8
8
|
const MAX_SYNC_DATA_CHUNK_BYTES = 1048576;
|
|
9
|
-
const MAX_SYNC_TRANSFER_BYTES =
|
|
9
|
+
const MAX_SYNC_TRANSFER_BYTES = 134217728;
|
|
10
10
|
const FRAME_HEADER_BYTES = 5;
|
|
11
11
|
const CONTROL_FRAME = 0;
|
|
12
12
|
const DATA_FRAME = 1;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.js","names":[],"sources":["../../../src/core/sync/protocol.ts"],"sourcesContent":["import { createHash, timingSafeEqual } from 'node:crypto'\n\nexport const SYNC_PROTOCOL_VERSION = 1\nexport const SYNC_SESSION_MANIFEST_CAPABILITY = 'session-manifest-v1'\nexport const SYNC_SESSION_EVENT_TAIL_CAPABILITY = 'session-event-tail-delta-v1'\nexport const SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY = 'session-bundle-objects-v1'\nexport const MAX_SYNC_CONTROL_FRAME_BYTES = 64 * 1024\nexport const MAX_SYNC_DATA_CHUNK_BYTES = 1024 * 1024\nexport const MAX_SYNC_TRANSFER_BYTES =
|
|
1
|
+
{"version":3,"file":"protocol.js","names":[],"sources":["../../../src/core/sync/protocol.ts"],"sourcesContent":["import { createHash, timingSafeEqual } from 'node:crypto'\n\nexport const SYNC_PROTOCOL_VERSION = 1\nexport const SYNC_SESSION_MANIFEST_CAPABILITY = 'session-manifest-v1'\nexport const SYNC_SESSION_EVENT_TAIL_CAPABILITY = 'session-event-tail-delta-v1'\nexport const SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY = 'session-bundle-objects-v1'\nexport const MAX_SYNC_CONTROL_FRAME_BYTES = 64 * 1024\nexport const MAX_SYNC_DATA_CHUNK_BYTES = 1024 * 1024\n// 单次传输(bundle 请求/响应、原始附件/溢出流)的硬上限。必须远小于宿主进程\n// JS 堆(桌面端约 1.4GB):JSON.parse 的对象图膨胀约为 JSON 文本的 5-10 倍,\n// 100GB 级上限等于无内存保护。协调器按更小的目标分批传输(见 coordinator.ts)。\nexport const MAX_SYNC_TRANSFER_BYTES = 128 * 1024 * 1024\n\nconst FRAME_HEADER_BYTES = 5\nconst CONTROL_FRAME = 0\nconst DATA_FRAME = 1\nconst DATA_METADATA_BYTES = 36\n\nexport type SyncFrame =\n | { kind: 'control'; value: Record<string, unknown> }\n | { kind: 'data'; sequence: number; digest: string; data: Buffer }\n\nexport class SyncProtocolError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'SyncProtocolError'\n }\n}\n\nfunction makeHeader(type: number, payloadLength: number): Buffer {\n const header = Buffer.allocUnsafe(FRAME_HEADER_BYTES)\n header[0] = type\n header.writeUInt32BE(payloadLength, 1)\n return header\n}\n\nexport function encodeSyncControlFrame(value: Record<string, unknown>): Buffer {\n const payload = Buffer.from(`${JSON.stringify(value)}\\n`, 'utf8')\n if (payload.length > MAX_SYNC_CONTROL_FRAME_BYTES) throw new SyncProtocolError('control frame exceeds size limit')\n return Buffer.concat([makeHeader(CONTROL_FRAME, payload.length), payload])\n}\n\nexport function encodeSyncDataFrame(sequence: number, data: Buffer): Buffer {\n if (!Number.isSafeInteger(sequence) || sequence < 0 || sequence > 0xffffffff) {\n throw new SyncProtocolError('data sequence is invalid')\n }\n if (data.length > MAX_SYNC_DATA_CHUNK_BYTES) throw new SyncProtocolError('data chunk exceeds size limit')\n const metadata = Buffer.allocUnsafe(DATA_METADATA_BYTES)\n metadata.writeUInt32BE(sequence, 0)\n createHash('sha256').update(data).digest().copy(metadata, 4)\n return Buffer.concat([makeHeader(DATA_FRAME, metadata.length + data.length), metadata, data])\n}\n\nexport class SyncFrameDecoder {\n private readonly header = Buffer.allocUnsafe(FRAME_HEADER_BYTES)\n private headerOffset = 0\n private frameType = -1\n private payload: Buffer | undefined\n private payloadOffset = 0\n private nextDataSequence = 0\n private ended = false\n\n push(chunk: Buffer): SyncFrame[] {\n if (this.ended) throw new SyncProtocolError('frame decoder is already finished')\n const frames: SyncFrame[] = []\n let inputOffset = 0\n\n while (inputOffset < chunk.length) {\n if (!this.payload) {\n const copied = Math.min(FRAME_HEADER_BYTES - this.headerOffset, chunk.length - inputOffset)\n chunk.copy(this.header, this.headerOffset, inputOffset, inputOffset + copied)\n this.headerOffset += copied\n inputOffset += copied\n if (this.headerOffset < FRAME_HEADER_BYTES) continue\n\n this.frameType = this.header[0]\n const payloadLength = this.header.readUInt32BE(1)\n if (this.frameType === CONTROL_FRAME) {\n if (payloadLength < 2 || payloadLength > MAX_SYNC_CONTROL_FRAME_BYTES) {\n throw new SyncProtocolError('control frame length is invalid')\n }\n } else if (this.frameType === DATA_FRAME) {\n if (payloadLength < DATA_METADATA_BYTES || payloadLength > DATA_METADATA_BYTES + MAX_SYNC_DATA_CHUNK_BYTES) {\n throw new SyncProtocolError('data frame length is invalid')\n }\n } else {\n throw new SyncProtocolError('unknown frame type')\n }\n this.payload = Buffer.allocUnsafe(payloadLength)\n this.payloadOffset = 0\n }\n\n const payload = this.payload\n const copied = Math.min(payload.length - this.payloadOffset, chunk.length - inputOffset)\n chunk.copy(payload, this.payloadOffset, inputOffset, inputOffset + copied)\n this.payloadOffset += copied\n inputOffset += copied\n\n if (this.payloadOffset === payload.length) {\n frames.push(this.decodeFrame(this.frameType, payload))\n this.resetFrame()\n }\n }\n return frames\n }\n\n finish(): void {\n if (this.headerOffset !== 0 || this.payload !== undefined) throw new SyncProtocolError('truncated frame stream')\n this.ended = true\n }\n\n private decodeFrame(type: number, payload: Buffer): SyncFrame {\n if (type === CONTROL_FRAME) {\n if (payload[payload.length - 1] !== 0x0a) throw new SyncProtocolError('control frame must end with a newline')\n let value: unknown\n try {\n value = JSON.parse(payload.subarray(0, -1).toString('utf8'))\n } catch {\n throw new SyncProtocolError('control frame contains invalid JSON')\n }\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new SyncProtocolError('control frame must contain an object')\n }\n return { kind: 'control', value: value as Record<string, unknown> }\n }\n\n const sequence = payload.readUInt32BE(0)\n if (sequence !== this.nextDataSequence) throw new SyncProtocolError('data sequence is out of order')\n const expectedDigest = payload.subarray(4, DATA_METADATA_BYTES)\n const data = payload.subarray(DATA_METADATA_BYTES)\n const actualDigest = createHash('sha256').update(data).digest()\n if (!timingSafeEqual(expectedDigest, actualDigest)) throw new SyncProtocolError('data chunk digest mismatch')\n this.nextDataSequence += 1\n return { kind: 'data', sequence, digest: actualDigest.toString('hex'), data: Buffer.from(data) }\n }\n\n private resetFrame(): void {\n this.headerOffset = 0\n this.frameType = -1\n this.payload = undefined\n this.payloadOffset = 0\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SyncPeerManager } from "./peer-manager.js";
|
|
2
|
-
import { s as SyncSshTransport } from "../../ssh-transport-
|
|
2
|
+
import { s as SyncSshTransport } from "../../ssh-transport-Cfb_nwkY.js";
|
|
3
3
|
import { SyncCoordinator } from "./coordinator.js";
|
|
4
4
|
import { Context } from "@deepseek-ai/cordis";
|
|
5
5
|
//#region src/core/sync/routes.d.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { p as SyncSessionService } from "../../session-sync-Zfo1Q-qT.js";
|
|
2
2
|
import { Context } from "@deepseek-ai/cordis";
|
|
3
3
|
//#region src/core/sync/session-routes.d.ts
|
|
4
4
|
declare function registerSyncSessionRoutes(ctx: Context, sessions: SyncSessionService): void;
|