@artenx/dshb 0.1.22 → 0.1.24
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 +2 -2
- package/lib/core/index.js.map +1 -1
- package/lib/core/sync/coordinator.d.ts +2 -2
- package/lib/core/sync/coordinator.js +135 -15
- 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 +58 -9
- 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.d.ts +2 -2
- package/lib/core/sync/protocol.js +4 -1
- package/lib/core/sync/protocol.js.map +1 -1
- package/lib/core/sync/remote-helper.js +10 -4
- package/lib/core/sync/remote-helper.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 +2 -2
- package/lib/core/sync/ssh-transport.js +127 -107
- package/lib/core/sync/ssh-transport.js.map +1 -1
- package/lib/{protocol-CV7LGE44.d.ts → protocol-KaWZArY8.d.ts} +5 -2
- package/lib/{session-sync-EdfwUON5.js → session-sync-C0uoizyB.js} +834 -98
- package/lib/session-sync-C0uoizyB.js.map +1 -0
- package/lib/{session-sync-g6qKZzjV.d.ts → session-sync-C2j2rk9K.d.ts} +56 -4
- package/lib/{ssh-transport-BcIiLxsX.d.ts → ssh-transport-gb2PVatu.d.ts} +15 -6
- package/package.json +1 -1
- package/lib/session-sync-EdfwUON5.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 { n as SyncSessionService } from "../session-sync-
|
|
14
|
+
import { n as SyncSessionService } from "../session-sync-C0uoizyB.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";
|
|
@@ -42,7 +42,7 @@ function apply(ctx) {
|
|
|
42
42
|
sshWorlds?.remove?.(nodeId);
|
|
43
43
|
dockerWorlds?.remove?.(nodeId);
|
|
44
44
|
});
|
|
45
|
-
const syncSessions = new SyncSessionService(syncState, syncNodes, registry, bindings, () => workspaceRegistry, () => ctx.get("sessionPersistence"), () => ctx.get("attachments"), () => ctx.get("spillStore"), sharedSessionInstructionSnapshotStore());
|
|
45
|
+
const syncSessions = new SyncSessionService(syncState, syncNodes, registry, bindings, () => workspaceRegistry, () => ctx.get("sessionPersistence"), () => ctx.get("attachments"), () => ctx.get("spillStore"), sharedSessionInstructionSnapshotStore(), void 0, void 0, process.env.DSH_HOME);
|
|
46
46
|
const syncOfflineArchives = new SyncOfflineArchiveService(syncNodes, syncSessions, void 0, void 0, syncState);
|
|
47
47
|
try {
|
|
48
48
|
syncOfflineArchives.cleanupStagingDirectories();
|
package/lib/core/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/core/index.ts"],"sourcesContent":["import type { Context } from '@deepseek-ai/cordis'\nimport { sharedWorldResolver } from '../router/resolve.js'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport { sharedAuditLogger } from './audit.js'\nimport { KnownHostsStore } from './known-hosts.js'\nimport { NodeRegistry } from './node-registry.js'\nimport { registerNodeRoutes, registerSshConfigRoutes } from './routes.js'\nimport { registerWorkspaceRoutes, registerRemoteDownloadRoutes } from './workspaces.js'\nimport { registerWorkspaceInstructionRoutes } from './workspace-instruction-routes.js'\nimport { testNode, type DockerNodeTester, type SshHandshakeTester } from './test.js'\nimport { WorkspaceBindingsStore } from './workspace-bindings.js'\nimport { sharedWorkspaceInstructionStore } from './workspace-instructions.js'\nimport { registerWorkspaceInstructionInjector } from './workspace-instruction-context.js'\nimport { SyncPeerManager } from './sync/peer-manager.js'\nimport { registerSyncInternalRoutes } from './sync/internal-routes.js'\nimport { registerSyncPeerRoutes } from './sync/routes.js'\nimport { SyncSshTransport } from './sync/ssh-transport.js'\nimport { SyncCoordinator } from './sync/coordinator.js'\nimport { SyncScheduler } from './sync/scheduler.js'\nimport { SyncNodeService } from './sync/node-sync.js'\nimport { SyncSessionService, type WorkspaceRegistryLike } from './sync/session-sync.js'\nimport { registerSyncSessionRoutes } from './sync/session-routes.js'\nimport { SyncOfflineArchiveService } from './sync/offline-archive.js'\nimport { registerSyncOfflineRoutes } from './sync/offline-routes.js'\nimport { SyncPeerStore, SyncStateStore } from './sync/stores.js'\nimport { sharedSessionInstructionSnapshotStore } from './workspace-instructions.js'\n\nexport const name = 'dshb-core'\n\nexport const inject = ['webServer', 'credentials']\n\nexport function apply(ctx: Context): void {\n const knownHosts = new KnownHostsStore()\n const registry = new NodeRegistry(ctx)\n const bindings = new WorkspaceBindingsStore()\n const syncPeers = new SyncPeerStore()\n const syncState = new SyncStateStore()\n const syncPeerManager = new SyncPeerManager(syncPeers, syncState, ctx.credentials as never)\n const syncSshTransport = new SyncSshTransport(syncPeerManager)\n let workspaceRegistry: WorkspaceRegistryLike | undefined\n if (typeof ctx.inject === 'function') {\n ctx.inject(['workspaceRegistry'], (workspaceCtx: Context) => {\n workspaceRegistry = (workspaceCtx as unknown as { workspaceRegistry?: typeof workspaceRegistry }).workspaceRegistry\n })\n }\n const syncNodes = new SyncNodeService(registry, bindings, syncState, () => workspaceRegistry, (nodeId) => {\n const sshWorlds = ctx.get('dshbSshWorlds') as { remove?: (id: string) => void } | undefined\n const dockerWorlds = ctx.get('dshbDockerWorlds') as { remove?: (id: string) => void } | undefined\n sshWorlds?.remove?.(nodeId)\n dockerWorlds?.remove?.(nodeId)\n })\n const syncSessions = new SyncSessionService(\n syncState,\n syncNodes,\n registry,\n bindings,\n () => workspaceRegistry,\n () => ctx.get('sessionPersistence') as never,\n () => ctx.get('attachments') as never,\n () => ctx.get('spillStore') as never,\n sharedSessionInstructionSnapshotStore(),\n )\n const syncOfflineArchives = new SyncOfflineArchiveService(syncNodes, syncSessions, undefined, undefined, syncState)\n try {\n syncOfflineArchives.cleanupStagingDirectories()\n syncSessions.cleanupOrphanedStaging()\n } catch {\n ctx.logger.warn('sync staging cleanup failed')\n }\n const syncCoordinator = new SyncCoordinator(syncPeerManager, syncState, syncNodes, syncSshTransport, Date.now, syncSessions)\n const syncScheduler = new SyncScheduler(syncPeerManager, syncCoordinator)\n ctx.provide('nodeRegistry', registry)\n ctx.provide('knownHosts', knownHosts)\n ctx.provide('workspaceBindings', bindings)\n ctx.provide('syncPeerStore', syncPeers)\n ctx.provide('syncStateStore', syncState)\n ctx.provide('syncNodeService', syncNodes)\n ctx.provide('syncSessionService', syncSessions)\n ctx.provide('syncOfflineArchiveService', syncOfflineArchives)\n ctx.provide('syncPeerManager', syncPeerManager)\n ctx.provide('syncSshTransport', syncSshTransport)\n ctx.provide('syncCoordinator', syncCoordinator)\n ctx.provide('syncScheduler', syncScheduler)\n ctx.provide('dshbAudit', sharedAuditLogger())\n sharedWorldResolver().setBindings(bindings)\n void registerNodeRoutes(ctx, registry, bindings, () => workspaceRegistry, syncNodes)\n void registerSshConfigRoutes(ctx, knownHosts)\n void registerWorkspaceRoutes(ctx, registry, bindings, syncNodes)\n void registerRemoteDownloadRoutes(ctx, registry, bindings)\n void registerWorkspaceInstructionRoutes(ctx, sharedWorkspaceInstructionStore())\n registerSyncPeerRoutes(ctx, syncPeerManager, syncSshTransport, syncCoordinator)\n registerSyncInternalRoutes(ctx, syncState, syncNodes, syncSessions)\n registerSyncSessionRoutes(ctx, syncSessions)\n registerSyncOfflineRoutes(ctx, syncOfflineArchives)\n ctx.effect(() => {\n const stopScheduler = syncScheduler.start()\n let disposed = false\n setImmediate(() => {\n if (disposed) return\n void (async () => {\n try {\n await syncSessions.recoverPending()\n } catch {\n ctx.logger.warn('sync session recovery remains pending')\n }\n try {\n await syncOfflineArchives.recoverPending()\n } catch {\n ctx.logger.warn('offline archive recovery remains pending')\n }\n })()\n })\n return () => {\n disposed = true\n stopScheduler()\n }\n })\n registerWorkspaceInstructionInjector(ctx)\n warmupWorlds(ctx, registry, bindings)\n startHeartbeat(ctx, registry)\n reprovisionDockerNodes(ctx, registry)\n}\n\nconst HEARTBEAT_MS = 5 * 60 * 1000\n\nfunction startHeartbeat(ctx: Context, registry: NodeRegistry): void {\n const tick = async (): Promise<void> => {\n const sshTester = ctx.get('dshbSshPool') as SshHandshakeTester | undefined\n const dockerTester = ctx.get('dshbDockerTester') as DockerNodeTester | undefined\n for (const node of registry.list()) {\n try {\n await testNode(registry, node.id, sshTester, undefined, dockerTester)\n } catch {\n // 单节点心跳失败不影响其他节点\n }\n }\n }\n const timer = setInterval(() => void tick(), HEARTBEAT_MS)\n ctx.effect(() => () => clearInterval(timer))\n setImmediate(() => void tick())\n}\n\nfunction reprovisionDockerNodes(ctx: Context, registry: NodeRegistry): void {\n setImmediate(() => {\n void (async () => {\n const provisioner = ctx.get('dshbDockerProvisioner') as { provision(nodeId: string): Promise<unknown> } | undefined\n if (!provisioner) return\n for (const node of registry.list()) {\n if (node.type !== 'local-docker' && node.type !== 'remote-docker') continue\n if (!node.docker?.image) continue\n if (node.docker?.containerId) continue\n try {\n await provisioner.provision(node.id)\n } catch {\n // 失败已记录到 provisionStatuses\n }\n }\n })()\n })\n}\n\ninterface WarmupWorldsLike {\n ensure(nodeId: string): Promise<{ ensureDir(path: string): Promise<void> } | undefined>\n}\n\nfunction warmupWorlds(ctx: Context, registry: NodeRegistry, bindings: WorkspaceBindingsStore): void {\n setImmediate(() => {\n void (async () => {\n for (const b of bindings.list()) {\n const node = registry.get(b.nodeId)\n if (!node || node.type === 'local-host') continue\n try {\n const isDocker = node.type === 'local-docker' || node.type === 'remote-docker'\n const worlds = isDocker\n ? (ctx.get('dshbDockerWorldsGeneric') as WarmupWorldsLike | undefined)\n : (ctx.get('dshbWorlds') as WarmupWorldsLike | undefined)\n if (!worlds) continue\n const world = await worlds.ensure(b.nodeId)\n if (world) await world.ensureDir(b.remotePath)\n } catch {\n // 预热失败不应阻塞启动;运行时可再次 ensure\n }\n }\n })()\n })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/core/index.ts"],"sourcesContent":["import type { Context } from '@deepseek-ai/cordis'\nimport { sharedWorldResolver } from '../router/resolve.js'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport { sharedAuditLogger } from './audit.js'\nimport { KnownHostsStore } from './known-hosts.js'\nimport { NodeRegistry } from './node-registry.js'\nimport { registerNodeRoutes, registerSshConfigRoutes } from './routes.js'\nimport { registerWorkspaceRoutes, registerRemoteDownloadRoutes } from './workspaces.js'\nimport { registerWorkspaceInstructionRoutes } from './workspace-instruction-routes.js'\nimport { testNode, type DockerNodeTester, type SshHandshakeTester } from './test.js'\nimport { WorkspaceBindingsStore } from './workspace-bindings.js'\nimport { sharedWorkspaceInstructionStore } from './workspace-instructions.js'\nimport { registerWorkspaceInstructionInjector } from './workspace-instruction-context.js'\nimport { SyncPeerManager } from './sync/peer-manager.js'\nimport { registerSyncInternalRoutes } from './sync/internal-routes.js'\nimport { registerSyncPeerRoutes } from './sync/routes.js'\nimport { SyncSshTransport } from './sync/ssh-transport.js'\nimport { SyncCoordinator } from './sync/coordinator.js'\nimport { SyncScheduler } from './sync/scheduler.js'\nimport { SyncNodeService } from './sync/node-sync.js'\nimport { SyncSessionService, type WorkspaceRegistryLike } from './sync/session-sync.js'\nimport { registerSyncSessionRoutes } from './sync/session-routes.js'\nimport { SyncOfflineArchiveService } from './sync/offline-archive.js'\nimport { registerSyncOfflineRoutes } from './sync/offline-routes.js'\nimport { SyncPeerStore, SyncStateStore } from './sync/stores.js'\nimport { sharedSessionInstructionSnapshotStore } from './workspace-instructions.js'\n\nexport const name = 'dshb-core'\n\nexport const inject = ['webServer', 'credentials']\n\nexport function apply(ctx: Context): void {\n const knownHosts = new KnownHostsStore()\n const registry = new NodeRegistry(ctx)\n const bindings = new WorkspaceBindingsStore()\n const syncPeers = new SyncPeerStore()\n const syncState = new SyncStateStore()\n const syncPeerManager = new SyncPeerManager(syncPeers, syncState, ctx.credentials as never)\n const syncSshTransport = new SyncSshTransport(syncPeerManager)\n let workspaceRegistry: WorkspaceRegistryLike | undefined\n if (typeof ctx.inject === 'function') {\n ctx.inject(['workspaceRegistry'], (workspaceCtx: Context) => {\n workspaceRegistry = (workspaceCtx as unknown as { workspaceRegistry?: typeof workspaceRegistry }).workspaceRegistry\n })\n }\n const syncNodes = new SyncNodeService(registry, bindings, syncState, () => workspaceRegistry, (nodeId) => {\n const sshWorlds = ctx.get('dshbSshWorlds') as { remove?: (id: string) => void } | undefined\n const dockerWorlds = ctx.get('dshbDockerWorlds') as { remove?: (id: string) => void } | undefined\n sshWorlds?.remove?.(nodeId)\n dockerWorlds?.remove?.(nodeId)\n })\n const syncSessions = new SyncSessionService(\n syncState,\n syncNodes,\n registry,\n bindings,\n () => workspaceRegistry,\n () => ctx.get('sessionPersistence') as never,\n () => ctx.get('attachments') as never,\n () => ctx.get('spillStore') as never,\n sharedSessionInstructionSnapshotStore(),\n undefined,\n undefined,\n process.env.DSH_HOME,\n )\n const syncOfflineArchives = new SyncOfflineArchiveService(syncNodes, syncSessions, undefined, undefined, syncState)\n try {\n syncOfflineArchives.cleanupStagingDirectories()\n syncSessions.cleanupOrphanedStaging()\n } catch {\n ctx.logger.warn('sync staging cleanup failed')\n }\n const syncCoordinator = new SyncCoordinator(syncPeerManager, syncState, syncNodes, syncSshTransport, Date.now, syncSessions)\n const syncScheduler = new SyncScheduler(syncPeerManager, syncCoordinator)\n ctx.provide('nodeRegistry', registry)\n ctx.provide('knownHosts', knownHosts)\n ctx.provide('workspaceBindings', bindings)\n ctx.provide('syncPeerStore', syncPeers)\n ctx.provide('syncStateStore', syncState)\n ctx.provide('syncNodeService', syncNodes)\n ctx.provide('syncSessionService', syncSessions)\n ctx.provide('syncOfflineArchiveService', syncOfflineArchives)\n ctx.provide('syncPeerManager', syncPeerManager)\n ctx.provide('syncSshTransport', syncSshTransport)\n ctx.provide('syncCoordinator', syncCoordinator)\n ctx.provide('syncScheduler', syncScheduler)\n ctx.provide('dshbAudit', sharedAuditLogger())\n sharedWorldResolver().setBindings(bindings)\n void registerNodeRoutes(ctx, registry, bindings, () => workspaceRegistry, syncNodes)\n void registerSshConfigRoutes(ctx, knownHosts)\n void registerWorkspaceRoutes(ctx, registry, bindings, syncNodes)\n void registerRemoteDownloadRoutes(ctx, registry, bindings)\n void registerWorkspaceInstructionRoutes(ctx, sharedWorkspaceInstructionStore())\n registerSyncPeerRoutes(ctx, syncPeerManager, syncSshTransport, syncCoordinator)\n registerSyncInternalRoutes(ctx, syncState, syncNodes, syncSessions)\n registerSyncSessionRoutes(ctx, syncSessions)\n registerSyncOfflineRoutes(ctx, syncOfflineArchives)\n ctx.effect(() => {\n const stopScheduler = syncScheduler.start()\n let disposed = false\n setImmediate(() => {\n if (disposed) return\n void (async () => {\n try {\n await syncSessions.recoverPending()\n } catch {\n ctx.logger.warn('sync session recovery remains pending')\n }\n try {\n await syncOfflineArchives.recoverPending()\n } catch {\n ctx.logger.warn('offline archive recovery remains pending')\n }\n })()\n })\n return () => {\n disposed = true\n stopScheduler()\n }\n })\n registerWorkspaceInstructionInjector(ctx)\n warmupWorlds(ctx, registry, bindings)\n startHeartbeat(ctx, registry)\n reprovisionDockerNodes(ctx, registry)\n}\n\nconst HEARTBEAT_MS = 5 * 60 * 1000\n\nfunction startHeartbeat(ctx: Context, registry: NodeRegistry): void {\n const tick = async (): Promise<void> => {\n const sshTester = ctx.get('dshbSshPool') as SshHandshakeTester | undefined\n const dockerTester = ctx.get('dshbDockerTester') as DockerNodeTester | undefined\n for (const node of registry.list()) {\n try {\n await testNode(registry, node.id, sshTester, undefined, dockerTester)\n } catch {\n // 单节点心跳失败不影响其他节点\n }\n }\n }\n const timer = setInterval(() => void tick(), HEARTBEAT_MS)\n ctx.effect(() => () => clearInterval(timer))\n setImmediate(() => void tick())\n}\n\nfunction reprovisionDockerNodes(ctx: Context, registry: NodeRegistry): void {\n setImmediate(() => {\n void (async () => {\n const provisioner = ctx.get('dshbDockerProvisioner') as { provision(nodeId: string): Promise<unknown> } | undefined\n if (!provisioner) return\n for (const node of registry.list()) {\n if (node.type !== 'local-docker' && node.type !== 'remote-docker') continue\n if (!node.docker?.image) continue\n if (node.docker?.containerId) continue\n try {\n await provisioner.provision(node.id)\n } catch {\n // 失败已记录到 provisionStatuses\n }\n }\n })()\n })\n}\n\ninterface WarmupWorldsLike {\n ensure(nodeId: string): Promise<{ ensureDir(path: string): Promise<void> } | undefined>\n}\n\nfunction warmupWorlds(ctx: Context, registry: NodeRegistry, bindings: WorkspaceBindingsStore): void {\n setImmediate(() => {\n void (async () => {\n for (const b of bindings.list()) {\n const node = registry.get(b.nodeId)\n if (!node || node.type === 'local-host') continue\n try {\n const isDocker = node.type === 'local-docker' || node.type === 'remote-docker'\n const worlds = isDocker\n ? (ctx.get('dshbDockerWorldsGeneric') as WarmupWorldsLike | undefined)\n : (ctx.get('dshbWorlds') as WarmupWorldsLike | undefined)\n if (!worlds) continue\n const world = await worlds.ensure(b.nodeId)\n if (world) await world.ensureDir(b.remotePath)\n } catch {\n // 预热失败不应阻塞启动;运行时可再次 ensure\n }\n }\n })()\n })\n}\n"],"mappings":"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"}
|
|
@@ -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 {
|
|
5
|
+
import { f as SyncSessionService } from "../../session-sync-C2j2rk9K.js";
|
|
6
|
+
import { s as SyncSshTransport } from "../../ssh-transport-gb2PVatu.js";
|
|
7
7
|
//#region src/core/sync/coordinator.d.ts
|
|
8
8
|
interface NodeTransferPlan {
|
|
9
9
|
toLocal: SyncNodeEntry[];
|
|
@@ -2,9 +2,10 @@ 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 validateSyncSessionBundle,
|
|
5
|
+
import { a as planSessionTransfers, c as validateSyncSessionBundle, l as validateSyncSessionManifest, o as sessionEventListDigest, r as makeSyncSessionBundle, s as syncSessionEntryKey, t as SyncSessionError } from "../../session-sync-C0uoizyB.js";
|
|
6
|
+
import { SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY, SYNC_SESSION_EVENT_TAIL_CAPABILITY } from "./protocol.js";
|
|
6
7
|
import { SyncHostKeyChallengeError } from "./host-key.js";
|
|
7
|
-
import { SyncSshProbeError } from "./ssh-transport.js";
|
|
8
|
+
import { SyncSessionDeltaPrefixError, SyncSshProbeError } from "./ssh-transport.js";
|
|
8
9
|
import { createHash } from "node:crypto";
|
|
9
10
|
//#region src/core/sync/coordinator.ts
|
|
10
11
|
const EMPTY_APPLY_RESULT = {
|
|
@@ -110,6 +111,64 @@ function emptySessionResult() {
|
|
|
110
111
|
function sessionResultCount(result) {
|
|
111
112
|
return result.created + result.updated + result.deleted;
|
|
112
113
|
}
|
|
114
|
+
function mergeSessionSkipped(...reports) {
|
|
115
|
+
const merged = /* @__PURE__ */ new Map();
|
|
116
|
+
for (const report of reports) for (const entry of report) merged.set(JSON.stringify([entry.sessionId, entry.reason]), entry);
|
|
117
|
+
return [...merged.values()];
|
|
118
|
+
}
|
|
119
|
+
function assertSelectedSessionEntries(expectedEntries, selectedEntries, side) {
|
|
120
|
+
const expected = new Map(expectedEntries.map((entry) => [syncSessionEntryKey(entry), entry]));
|
|
121
|
+
const selected = new Map(selectedEntries.map((entry) => [syncSessionEntryKey(entry), entry]));
|
|
122
|
+
if (selected.size !== selectedEntries.length || selected.size !== expected.size) throw new SyncCoordinatorError("session-manifest-stale", `${side}会话内容与 manifest 清单不一致`);
|
|
123
|
+
for (const [key, expectedEntry] of expected) {
|
|
124
|
+
const metadata = expectedEntry.metadata;
|
|
125
|
+
const currentEntry = selected.get(key);
|
|
126
|
+
const current = currentEntry?.metadata;
|
|
127
|
+
const eventSummary = currentEntry?.eventDelta ? {
|
|
128
|
+
eventCount: currentEntry.eventDelta.fullEventCount,
|
|
129
|
+
eventDigest: currentEntry.eventDelta.fullEventDigest,
|
|
130
|
+
inheritedEventCount: currentEntry.inheritedEventCount
|
|
131
|
+
} : currentEntry?.events ? {
|
|
132
|
+
eventCount: currentEntry.events.length,
|
|
133
|
+
eventDigest: sessionEventListDigest(currentEntry.events),
|
|
134
|
+
inheritedEventCount: currentEntry.inheritedEventCount
|
|
135
|
+
} : void 0;
|
|
136
|
+
const hasExpectedEventSummary = expectedEntry.eventCount !== void 0 || expectedEntry.eventDigest !== void 0 || expectedEntry.inheritedEventCount !== void 0;
|
|
137
|
+
const expectedWorkspace = metadata.workspaceOrigin;
|
|
138
|
+
const currentWorkspace = current?.workspaceOrigin;
|
|
139
|
+
const mismatches = [];
|
|
140
|
+
if (!current) mismatches.push("entry-missing");
|
|
141
|
+
else {
|
|
142
|
+
if (current.localEntityId !== metadata.localEntityId) mismatches.push("localEntityId");
|
|
143
|
+
if (compareSyncRevision(current.revision, metadata.revision) !== 0) mismatches.push("revision");
|
|
144
|
+
if (current.contentDigest !== metadata.contentDigest) mismatches.push("contentDigest");
|
|
145
|
+
if (current.stateDigest !== metadata.stateDigest) mismatches.push("stateDigest");
|
|
146
|
+
if (Boolean(current.deleted) !== Boolean(metadata.deleted)) mismatches.push("deleted");
|
|
147
|
+
if (expectedWorkspace?.originInstanceId !== currentWorkspace?.originInstanceId || expectedWorkspace?.originEntityId !== currentWorkspace?.originEntityId) mismatches.push("workspaceOrigin");
|
|
148
|
+
if (hasExpectedEventSummary && (!eventSummary || eventSummary.eventCount !== expectedEntry.eventCount || eventSummary.eventDigest !== expectedEntry.eventDigest || eventSummary.inheritedEventCount !== expectedEntry.inheritedEventCount)) mismatches.push("eventSummary");
|
|
149
|
+
}
|
|
150
|
+
if (mismatches.length > 0) {
|
|
151
|
+
const detail = current && hasExpectedEventSummary ? ` expected(eventCount=${expectedEntry.eventCount}, inherited=${expectedEntry.inheritedEventCount}, digest=${expectedEntry.eventDigest}) actual(${eventSummary ? `eventCount=${eventSummary.eventCount}, inherited=${eventSummary.inheritedEventCount}, digest=${eventSummary.eventDigest}` : "no-events"})` : "";
|
|
152
|
+
throw new SyncCoordinatorError("session-manifest-stale", `${side}会话内容在 manifest 之后发生变化(key=${key},字段=${mismatches.join(",")})${detail}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function sessionEventPrefixesFor(selectedEntries, receiverEntries) {
|
|
157
|
+
const receiverByKey = new Map(receiverEntries.map((entry) => [syncSessionEntryKey(entry), entry]));
|
|
158
|
+
const prefixes = [];
|
|
159
|
+
for (const selected of selectedEntries) {
|
|
160
|
+
if (selected.branchId || selected.metadata.deleted) continue;
|
|
161
|
+
const receiver = receiverByKey.get(syncSessionEntryKey(selected));
|
|
162
|
+
if (!receiver || receiver.branchId || receiver.metadata.deleted || receiver.eventCount === void 0 || receiver.eventDigest === void 0 || receiver.inheritedEventCount === void 0) continue;
|
|
163
|
+
prefixes.push({
|
|
164
|
+
key: syncSessionEntryKey(selected),
|
|
165
|
+
eventCount: receiver.eventCount,
|
|
166
|
+
eventDigest: receiver.eventDigest,
|
|
167
|
+
inheritedEventCount: receiver.inheritedEventCount
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return prefixes;
|
|
171
|
+
}
|
|
113
172
|
var SyncCoordinator = class {
|
|
114
173
|
peers;
|
|
115
174
|
state;
|
|
@@ -147,9 +206,9 @@ var SyncCoordinator = class {
|
|
|
147
206
|
});
|
|
148
207
|
try {
|
|
149
208
|
if (peer.syncSessions && !this.sessions) throw new SyncCoordinatorError("sessions-unavailable", "会话同步服务不可用");
|
|
150
|
-
const
|
|
151
|
-
if (
|
|
152
|
-
const remoteEntries = this.nodes.validateManifest(
|
|
209
|
+
const remoteNodeManifest = await this.retryTransport(() => this.transport.fetchNodeManifest(peer.id));
|
|
210
|
+
if (remoteNodeManifest.protocolVersion !== 1 || remoteNodeManifest.instanceId === this.state.instanceId()) throw new SyncCoordinatorError("protocol-invalid", "同步 peer 协议版本或实例身份无效");
|
|
211
|
+
const remoteEntries = this.nodes.validateManifest(remoteNodeManifest.records);
|
|
153
212
|
const localManifest = await this.nodes.exportManifest();
|
|
154
213
|
const localEntries = this.nodes.validateManifest(localManifest.records);
|
|
155
214
|
const plan = planNodeTransfers(peer.direction, localEntries, remoteEntries);
|
|
@@ -163,18 +222,79 @@ var SyncCoordinator = class {
|
|
|
163
222
|
let digest = manifestDigest(remoteEntries);
|
|
164
223
|
if (peer.syncSessions) {
|
|
165
224
|
const sessionService = this.sessions;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
225
|
+
let plan;
|
|
226
|
+
let localBundleToApply;
|
|
227
|
+
let remoteBundleToApply;
|
|
228
|
+
let remoteSkipped = 0;
|
|
229
|
+
let remoteSessionDigest = "";
|
|
230
|
+
let localSessionDigest = "";
|
|
231
|
+
let localInstanceId = "";
|
|
232
|
+
let modernManifests;
|
|
233
|
+
if (remoteNodeManifest.capabilities?.includes("session-manifest-v1")) {
|
|
234
|
+
const remoteManifest = validateSyncSessionManifest(await this.retryTransport(() => this.transport.fetchSessionManifest(peer.id)));
|
|
235
|
+
if (remoteManifest.instanceId !== remoteNodeManifest.instanceId) throw new SyncCoordinatorError("protocol-invalid", "节点与会话 manifest 的实例身份不一致");
|
|
236
|
+
const localManifest = await sessionService.exportManifest();
|
|
237
|
+
modernManifests = {
|
|
238
|
+
remote: remoteManifest,
|
|
239
|
+
local: localManifest
|
|
240
|
+
};
|
|
241
|
+
plan = planSessionTransfers(peer.direction, localManifest.entries, remoteManifest.entries);
|
|
242
|
+
remoteSkipped = remoteManifest.skipped.length;
|
|
243
|
+
remoteSessionDigest = remoteManifest.digest;
|
|
244
|
+
localSessionDigest = localManifest.digest;
|
|
245
|
+
localInstanceId = localManifest.instanceId;
|
|
246
|
+
const useEventDeltas = remoteNodeManifest.capabilities.includes(SYNC_SESSION_EVENT_TAIL_CAPABILITY);
|
|
247
|
+
const useSharedObjects = remoteNodeManifest.capabilities.includes(SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY);
|
|
248
|
+
const toWireBundle = (wired, skippedReport) => makeSyncSessionBundle(wired.instanceId, wired.entries, skippedReport, wired.objects);
|
|
249
|
+
const remoteEntriesToApply = plan.toLocal.length > 0 ? validateSyncSessionBundle(await this.retryTransport(() => this.transport.fetchSessionEntries(peer.id, plan.toLocal.map(syncSessionEntryKey), remoteManifest.digest, useEventDeltas ? sessionEventPrefixesFor(plan.toLocal, localManifest.entries) : []))) : makeSyncSessionBundle(remoteManifest.instanceId, []);
|
|
250
|
+
if (remoteEntriesToApply.instanceId !== remoteManifest.instanceId) throw new SyncCoordinatorError("protocol-invalid", "远端会话内容与 manifest 实例不一致");
|
|
251
|
+
assertSelectedSessionEntries(plan.toLocal, remoteEntriesToApply.entries, "远端");
|
|
252
|
+
const localEntriesToApply = plan.toRemote.length > 0 ? await sessionService.exportEntries(plan.toRemote.map(syncSessionEntryKey), localManifest.digest, useEventDeltas ? sessionEventPrefixesFor(plan.toRemote, remoteManifest.entries) : [], { sharedObjects: useSharedObjects }) : makeSyncSessionBundle(localManifest.instanceId, []);
|
|
253
|
+
assertSelectedSessionEntries(plan.toRemote, localEntriesToApply.entries, "本地");
|
|
254
|
+
const remoteSkippedReport = mergeSessionSkipped(remoteManifest.skipped, remoteEntriesToApply.skipped);
|
|
255
|
+
const localSkippedReport = mergeSessionSkipped(localManifest.skipped, localEntriesToApply.skipped);
|
|
256
|
+
remoteSkipped = remoteSkippedReport.length;
|
|
257
|
+
localBundleToApply = makeSyncSessionBundle(remoteManifest.instanceId, remoteEntriesToApply.entries, remoteSkippedReport);
|
|
258
|
+
remoteBundleToApply = toWireBundle(localEntriesToApply, localSkippedReport);
|
|
259
|
+
} else {
|
|
260
|
+
const remoteBundle = validateSyncSessionBundle(await this.retryTransport(() => this.transport.fetchSessionBundle(peer.id)));
|
|
261
|
+
if (remoteBundle.instanceId !== remoteNodeManifest.instanceId) throw new SyncCoordinatorError("protocol-invalid", "节点与会话 manifest 的实例身份不一致");
|
|
262
|
+
const localBundle = await sessionService.exportBundle();
|
|
263
|
+
plan = planSessionTransfers(peer.direction, localBundle.entries, remoteBundle.entries);
|
|
264
|
+
remoteSkipped = remoteBundle.skipped.length;
|
|
265
|
+
remoteSessionDigest = remoteBundle.digest;
|
|
266
|
+
localSessionDigest = localBundle.digest;
|
|
267
|
+
localInstanceId = localBundle.instanceId;
|
|
268
|
+
localBundleToApply = makeSyncSessionBundle(remoteBundle.instanceId, plan.toLocal, remoteBundle.skipped);
|
|
269
|
+
remoteBundleToApply = makeSyncSessionBundle(localBundle.instanceId, plan.toRemote, localBundle.skipped);
|
|
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));
|
|
293
|
+
}
|
|
174
294
|
importedSessions = sessionResultCount(sessionLocalResult);
|
|
175
295
|
exportedSessions = sessionResultCount(sessionRemoteResult);
|
|
176
296
|
skippedSessions = plan.skipped + sessionLocalResult.skipped + sessionRemoteResult.skipped;
|
|
177
|
-
digest = `sha256:${createHash("sha256").update(`${digest}\0${
|
|
297
|
+
digest = `sha256:${createHash("sha256").update(`${digest}\0${remoteSessionDigest}\0${localSessionDigest}`).digest("hex")}`;
|
|
178
298
|
}
|
|
179
299
|
const finishedAt = new Date(this.now()).toISOString();
|
|
180
300
|
const summary = {
|
|
@@ -182,7 +302,7 @@ var SyncCoordinator = class {
|
|
|
182
302
|
direction: peer.direction,
|
|
183
303
|
startedAt,
|
|
184
304
|
finishedAt,
|
|
185
|
-
remoteInstanceId:
|
|
305
|
+
remoteInstanceId: remoteNodeManifest.instanceId,
|
|
186
306
|
imported: localCounts.applied,
|
|
187
307
|
exported: remoteCounts.applied,
|
|
188
308
|
skipped: plan.skipped + localCounts.skipped + remoteCounts.skipped + skippedSessions,
|
|
@@ -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 { SyncSshProbeError, SyncSshTransport } from './ssh-transport.js'\nimport { makeSyncSessionBundle, planSessionTransfers, SyncSessionError, SyncSessionService, validateSyncSessionBundle } from './session-sync.js'\nimport type { SyncSessionApplyResult } from './session-sync.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\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 remoteManifest = await this.retryTransport(() => this.transport.fetchNodeManifest(peer.id))\n if (remoteManifest.protocolVersion !== 1 || remoteManifest.instanceId === this.state.instanceId()) {\n throw new SyncCoordinatorError('protocol-invalid', '同步 peer 协议版本或实例身份无效')\n }\n const remoteEntries = this.nodes.validateManifest(remoteManifest.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 const remoteBundle = validateSyncSessionBundle(await this.retryTransport(() => this.transport.fetchSessionBundle(peer.id)))\n if (remoteBundle.instanceId !== remoteManifest.instanceId) {\n throw new SyncCoordinatorError('protocol-invalid', '节点与会话 manifest 的实例身份不一致')\n }\n const localBundle = await sessionService.exportBundle()\n const plan = planSessionTransfers(peer.direction, localBundle.entries, remoteBundle.entries)\n const localBundleToApply = makeSyncSessionBundle(remoteBundle.instanceId, plan.toLocal, remoteBundle.skipped)\n const remoteBundleToApply = makeSyncSessionBundle(localBundle.instanceId, plan.toRemote, localBundle.skipped)\n const sessionLocalResult = plan.toLocal.length > 0 || remoteBundle.skipped.length > 0\n ? await sessionService.applyBundle(localBundleToApply, peer.name, peer.id)\n : emptySessionResult()\n const sessionRemoteResult = plan.toRemote.length > 0\n ? await this.retryTransport(() => this.transport.applySessionBundle(peer.id, remoteBundleToApply, localBundle.instanceId))\n : emptySessionResult()\n importedSessions = sessionResultCount(sessionLocalResult)\n exportedSessions = sessionResultCount(sessionRemoteResult)\n skippedSessions = plan.skipped + sessionLocalResult.skipped + sessionRemoteResult.skipped\n digest = `sha256:${createHash('sha256').update(`${digest}\\0${remoteBundle.digest}\\0${localBundle.digest}`).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: remoteManifest.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\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":";;;;;;;;;;AA4BA,MAAM,qBAA0C;CAAE,SAAS;CAAG,SAAS;CAAG,SAAS;CAAG,gBAAgB;CAAG,SAAS;AAAE;AACpH,MAAM,2BAA2B;AAuBjC,IAAa,uBAAb,cAA0C,MAAM;CACzB;CAArB,YAAY,MAAuB,SAAiB;EAClD,MAAM,OAAO;EADM,KAAA,OAAA;EAEnB,KAAK,OAAO;CACd;AACF;AAEA,SAAS,SAAS,QAAqC;CACrD,OAAO,OAAO,UAAU,OAAO,UAAU,OAAO;AAClD;AAEA,SAAS,eAAe,SAA2C;CACjE,MAAM,WAAW,QACd,KAAK,WAAW;EACf,KAAK,cAAc,QAAQ,MAAM,QAAQ;EACzC,UAAU,MAAM,SAAS;EACzB,eAAe,MAAM,SAAS;EAC9B,SAAS,QAAQ,MAAM,SAAS,OAAO;CACzC,EAAE,CAAC,CACF,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC;CAC5D,OAAO,UAAU,WAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,UAAU,QAAQ,CAAC,CAAC,CAAC,OAAO,KAAK;AACrF;AAEA,SAAgB,kBACd,WACA,cACA,eACkB;CAClB,MAAM,UAA2B,CAAC;CAClC,MAAM,WAA4B,CAAC;CACnC,IAAI,cAAc,QAAQ,OAAO;EAAE,SAAS,CAAC,GAAG,aAAa;EAAG;EAAU,SAAS;CAAE;CAErF,MAAM,aAAa,IAAI,IAAI,aAAa,KAAK,UAAU,CAAC,cAAc,QAAQ,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC;CACtG,MAAM,cAAc,IAAI,IAAI,cAAc,KAAK,UAAU,CAAC,cAAc,QAAQ,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC;CACxG,MAAM,uBAAO,IAAI,IAAI,CAAC,GAAG,WAAW,KAAK,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC;CAClE,IAAI,UAAU;CAEd,KAAK,MAAM,OAAO,MAAM;EACtB,MAAM,QAAQ,WAAW,IAAI,GAAG;EAChC,MAAM,SAAS,YAAY,IAAI,GAAG;EAClC,IAAI,CAAC,OAAO;GACV,QAAQ,KAAK,MAAO;GACpB;EACF;EACA,IAAI,CAAC,QAAQ;GACX,SAAS,KAAK,KAAK;GACnB;EACF;EAEA,MAAM,aAAa,oBAAoB,MAAM,SAAS,UAAU,OAAO,SAAS,QAAQ;EACxF,IAAI,aAAa,GACf,SAAS,KAAK,KAAK;OACd,IAAI,aAAa,GACtB,QAAQ,KAAK,MAAM;OACd,IACL,MAAM,SAAS,kBAAkB,OAAO,SAAS,iBACjD,QAAQ,MAAM,SAAS,OAAO,MAAM,QAAQ,OAAO,SAAS,OAAO,GAEnE,WAAW;OAEX,MAAM,IAAI,qBAAqB,qBAAqB,gDAAgD;CAExG;CACA,OAAO;EAAE;EAAS;EAAU;CAAQ;AACtC;AAEA,SAAS,WAAW,OAAsC;CACxD,IAAI,iBAAiB,sBAAsB,OAAO;CAClD,IAAI,iBAAiB,uBAAuB,OAAO,IAAI,qBAAqB,kBAAkB,UAAU;CACxG,IAAI,iBAAiB,2BAEnB,OAAO,IAAI,qBADE,MAAM,aAAa,YAAY,qBAAqB,sBAC3B,MAAM,OAAO;CAErD,IAAI,iBAAiB,mBAAmB;EAStC,MAAM,UAAU;GAPd,MAAM;GACN,SAAS;GACT,SAAS;GACT,SAAS;GACT,QAAQ;GACR,UAAU;EAEW,EAAE,MAAM,aAAa;EAC5C,MAAM,SAAU,MAAsC;EACtD,OAAO,IAAI,qBAAqB,OAAO,MAAM,YAAY,SAAS,GAAG,QAAQ,GAAG,WAAW,OAAO;CACpG;CACA,IAAI,iBAAiB,uBAAuB,OAAO,IAAI,qBAAqB,oBAAoB,kBAAkB;CAClH,IAAI,iBAAiB,kBACnB,OAAO,IAAI,qBAAqB,MAAM,KAAK,WAAW,UAAU,IAAI,MAAM,OAAO,WAAW,MAAM,QAAQ,MAAM,OAAO;CAEzH,OAAO,IAAI,qBAAqB,eAAe,QAAQ;AACzD;AAEA,SAAS,YAAY,QAA2F;CAC9G,OAAO;EACL,SAAS,SAAS,MAAM;EACxB,SAAS,OAAO;EAChB,gBAAgB,OAAO;CACzB;AACF;AAEA,SAAS,qBAA6C;CACpD,OAAO;EAAE,SAAS;EAAG,SAAS;EAAG,UAAU;EAAG,SAAS;EAAG,SAAS;EAAG,UAAU;CAAE;AACpF;AAEA,SAAS,mBAAmB,QAAwC;CAClE,OAAO,OAAO,UAAU,OAAO,UAAU,OAAO;AAClD;AAEA,SAAS,oBAAoB,GAAG,SAAgH;CAC9I,MAAM,yBAAS,IAAI,IAAmD;CACtE,KAAK,MAAM,UAAU,SACnB,KAAK,MAAM,SAAS,QAAQ,OAAO,IAAI,KAAK,UAAU,CAAC,MAAM,WAAW,MAAM,MAAM,CAAC,GAAG,KAAK;CAE/F,OAAO,CAAC,GAAG,OAAO,OAAO,CAAC;AAC5B;AAEA,SAAS,6BACP,iBACA,iBACA,MACM;CACN,MAAM,WAAW,IAAI,IAAI,gBAAgB,KAAK,UAAU,CAAC,oBAAoB,KAAK,GAAG,KAAK,CAAC,CAAC;CAC5F,MAAM,WAAW,IAAI,IAAI,gBAAgB,KAAK,UAAU,CAAC,oBAAoB,KAAK,GAAG,KAAK,CAAC,CAAC;CAC5F,IAAI,SAAS,SAAS,gBAAgB,UAAU,SAAS,SAAS,SAAS,MACzE,MAAM,IAAI,qBAAqB,0BAA0B,GAAG,KAAK,qBAAqB;CAExF,KAAK,MAAM,CAAC,KAAK,kBAAkB,UAAU;EAC3C,MAAM,WAAW,cAAc;EAC/B,MAAM,eAAe,SAAS,IAAI,GAAG;EACrC,MAAM,UAAU,cAAc;EAC9B,MAAM,eAAe,cAAc,aAC/B;GACE,YAAY,aAAa,WAAW;GACpC,aAAa,aAAa,WAAW;GACrC,qBAAqB,aAAa;EACpC,IACA,cAAc,SACZ;GACE,YAAY,aAAa,OAAO;GAChC,aAAa,uBAAuB,aAAa,MAAM;GACvD,qBAAqB,aAAa;EACpC,IACA,KAAA;EACN,MAAM,0BAA0B,cAAc,eAAe,KAAA,KAAa,cAAc,gBAAgB,KAAA,KACtG,cAAc,wBAAwB,KAAA;EACxC,MAAM,oBAAoB,SAAS;EACnC,MAAM,mBAAmB,SAAS;EAClC,MAAM,aAAuB,CAAC;EAC9B,IAAI,CAAC,SAAS,WAAW,KAAK,eAAe;OACxC;GACH,IAAI,QAAQ,kBAAkB,SAAS,eAAe,WAAW,KAAK,eAAe;GACrF,IAAI,oBAAoB,QAAQ,UAAU,SAAS,QAAQ,MAAM,GAAG,WAAW,KAAK,UAAU;GAC9F,IAAI,QAAQ,kBAAkB,SAAS,eAAe,WAAW,KAAK,eAAe;GACrF,IAAI,QAAQ,gBAAgB,SAAS,aAAa,WAAW,KAAK,aAAa;GAC/E,IAAI,QAAQ,QAAQ,OAAO,MAAM,QAAQ,SAAS,OAAO,GAAG,WAAW,KAAK,SAAS;GACrF,IAAI,mBAAmB,qBAAqB,kBAAkB,oBAC5D,mBAAmB,mBAAmB,kBAAkB,gBAAgB,WAAW,KAAK,iBAAiB;GAC3G,IAAI,4BAA4B,CAAC,gBAAgB,aAAa,eAAe,cAAc,cACzF,aAAa,gBAAgB,cAAc,eAAe,aAAa,wBAAwB,cAAc,sBAC7G,WAAW,KAAK,cAAc;EAElC;EACA,IAAI,WAAW,SAAS,GAAG;GACzB,MAAM,SAAS,WAAW,0BACtB,wBAAwB,cAAc,WAAW,cAAc,cAAc,oBAAoB,WAAW,cAAc,YAAY,WAC3H,eAAe,cAAc,aAAa,WAAW,cAAc,aAAa,oBAAoB,WAAW,aAAa,gBAAgB,YAAY,KACnK;GACJ,MAAM,IAAI,qBAAqB,0BAA0B,GAAG,KAAK,4BAA4B,IAAI,MAAM,WAAW,KAAK,GAAG,EAAE,GAAG,QAAQ;EACzI;CACF;AACF;AAEA,SAAS,wBACP,iBACA,iBAC0B;CAC1B,MAAM,gBAAgB,IAAI,IAAI,gBAAgB,KAAK,UAAU,CAAC,oBAAoB,KAAK,GAAG,KAAK,CAAC,CAAC;CACjG,MAAM,WAAqC,CAAC;CAC5C,KAAK,MAAM,YAAY,iBAAiB;EACtC,IAAI,SAAS,YAAY,SAAS,SAAS,SAAS;EACpD,MAAM,WAAW,cAAc,IAAI,oBAAoB,QAAQ,CAAC;EAChE,IAAI,CAAC,YAAY,SAAS,YAAY,SAAS,SAAS,WAAW,SAAS,eAAe,KAAA,KACzF,SAAS,gBAAgB,KAAA,KAAa,SAAS,wBAAwB,KAAA,GAAW;EACpF,SAAS,KAAK;GACZ,KAAK,oBAAoB,QAAQ;GACjC,YAAY,SAAS;GACrB,aAAa,SAAS;GACtB,qBAAqB,SAAS;EAChC,CAAC;CACH;CACA,OAAO;AACT;AAEA,IAAa,kBAAb,MAA6B;CAIR;CACA;CACA;CACA;CACA;CACA;CARnB,2BAA4B,IAAI,IAAqC;CAErE,YACE,OACA,OACA,OACA,WACA,MAAqC,KAAK,KAC1C,UACA;EANiB,KAAA,QAAA;EACA,KAAA,QAAA;EACA,KAAA,QAAA;EACA,KAAA,YAAA;EACA,KAAA,MAAA;EACA,KAAA,WAAA;CAChB;CAEH,IAAI,QAAyC;EAC3C,MAAM,SAAS,KAAK,SAAS,IAAI,MAAM;EACvC,IAAI,QAAQ,OAAO;EACnB,MAAM,MAAM,KAAK,QAAQ,MAAM;EAC/B,KAAK,SAAS,IAAI,QAAQ,GAAG;EAC7B,IAAS,WAAW,KAAK,MAAM,QAAQ,GAAG,SAAS,KAAK,MAAM,QAAQ,GAAG,CAAC;EAC1E,OAAO;CACT;CAEA,MAAc,QAAQ,QAAyC;EAC7D,MAAM,OAAO,MAAM,KAAK,MAAM,IAAI,MAAM;EACxC,IAAI,CAAC,MAAM,MAAM,IAAI,qBAAqB,kBAAkB,UAAU;EACtE,IAAI,CAAC,KAAK,SAAS,MAAM,IAAI,qBAAqB,iBAAiB,UAAU;EAE7E,MAAM,YAAY,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY;EACnD,KAAK,MAAM,eAAe,KAAK,IAAI;GACjC,WAAW;GACX,eAAe;GACf,eAAe,KAAA;GACf,kBAAkB,KAAA;EACpB,CAAC;EAED,IAAI;GACF,IAAI,KAAK,gBAAgB,CAAC,KAAK,UAAU,MAAM,IAAI,qBAAqB,wBAAwB,WAAW;GAC3G,MAAM,qBAAqB,MAAM,KAAK,qBAAqB,KAAK,UAAU,kBAAkB,KAAK,EAAE,CAAC;GACpG,IAAI,mBAAmB,oBAAoB,KAAK,mBAAmB,eAAe,KAAK,MAAM,WAAW,GACtG,MAAM,IAAI,qBAAqB,oBAAoB,qBAAqB;GAE1E,MAAM,gBAAgB,KAAK,MAAM,iBAAiB,mBAAmB,OAAO;GAC5E,MAAM,gBAAgB,MAAM,KAAK,MAAM,eAAe;GACtD,MAAM,eAAe,KAAK,MAAM,iBAAiB,cAAc,OAAO;GACtE,MAAM,OAAO,kBAAkB,KAAK,WAAW,cAAc,aAAa;GAE1E,MAAM,cAAc,KAAK,QAAQ,SAAS,IACtC,MAAM,KAAK,MAAM,cAAc,KAAK,SAAS,KAAK,MAAM,MAAM,IAC9D;GACJ,MAAM,eAAe,KAAK,SAAS,SAAS,IACxC,MAAM,KAAK,qBAAqB,KAAK,UAAU,kBAAkB,KAAK,IAAI,KAAK,UAAU,cAAc,UAAU,CAAC,IAClH;GACJ,MAAM,cAAc,YAAY,WAAW;GAC3C,MAAM,eAAe,YAAY,YAAY;GAC7C,IAAI,mBAAmB;GACvB,IAAI,mBAAmB;GACvB,IAAI,kBAAkB;GACtB,IAAI,SAAS,eAAe,aAAa;GACzC,IAAI,KAAK,cAAc;IACrB,MAAM,iBAAiB,KAAK;IAC5B,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI,gBAAgB;IACpB,IAAI,sBAAsB;IAC1B,IAAI,qBAAqB;IACzB,IAAI,kBAAkB;IACtB,IAAI;IACJ,IAAI,mBAAmB,cAAc,SAAA,qBAAyC,GAAG;KAC/E,MAAM,iBAAiB,4BACrB,MAAM,KAAK,qBAAqB,KAAK,UAAU,qBAAqB,KAAK,EAAE,CAAC,CAC9E;KACA,IAAI,eAAe,eAAe,mBAAmB,YACnD,MAAM,IAAI,qBAAqB,oBAAoB,yBAAyB;KAE9E,MAAM,gBAAgB,MAAM,eAAe,eAAe;KAC1D,kBAAkB;MAAE,QAAQ;MAAgB,OAAO;KAAc;KACjE,OAAO,qBAAqB,KAAK,WAAW,cAAc,SAAS,eAAe,OAAO;KACzF,gBAAgB,eAAe,QAAQ;KACvC,sBAAsB,eAAe;KACrC,qBAAqB,cAAc;KACnC,kBAAkB,cAAc;KAChC,MAAM,iBAAiB,mBAAmB,aAAa,SAAS,kCAAkC;KAClG,MAAM,mBAAmB,mBAAmB,aAAa,SAAS,sCAAsC;KACxG,MAAM,gBAAgB,OAA0B,kBAC9C,sBAAsB,MAAM,YAAY,MAAM,SAAS,eAAe,MAAM,OAAO;KAErF,MAAM,uBAAuB,KAAK,QAAQ,SAAS,IAC/C,0BAA0B,MAAM,KAAK,qBACnC,KAAK,UAAU,oBACb,KAAK,IACL,KAAK,QAAQ,IAAI,mBAAmB,GACpC,eAAe,QACf,iBAAiB,wBAAwB,KAAK,SAAS,cAAc,OAAO,IAAI,CAAC,CACnF,CACF,CAAC,IACD,sBAAsB,eAAe,YAAY,CAAC,CAAC;KACvD,IAAI,qBAAqB,eAAe,eAAe,YACrD,MAAM,IAAI,qBAAqB,oBAAoB,wBAAwB;KAE7E,6BAA6B,KAAK,SAAS,qBAAqB,SAAS,IAAI;KAC7E,MAAM,sBAAsB,KAAK,SAAS,SAAS,IAC/C,MAAM,eAAe,cACnB,KAAK,SAAS,IAAI,mBAAmB,GACrC,cAAc,QACd,iBAAiB,wBAAwB,KAAK,UAAU,eAAe,OAAO,IAAI,CAAC,GACnF,EAAE,eAAe,iBAAiB,CACpC,IACA,sBAAsB,cAAc,YAAY,CAAC,CAAC;KACtD,6BAA6B,KAAK,UAAU,oBAAoB,SAAS,IAAI;KAC7E,MAAM,sBAAsB,oBAAoB,eAAe,SAAS,qBAAqB,OAAO;KACpG,MAAM,qBAAqB,oBAAoB,cAAc,SAAS,oBAAoB,OAAO;KACjG,gBAAgB,oBAAoB;KACpC,qBAAqB,sBAAsB,eAAe,YAAY,qBAAqB,SAAS,mBAAmB;KACvH,sBAAsB,aAAa,qBAAqB,kBAAkB;IAC5E,OAAO;KACL,MAAM,eAAe,0BAA0B,MAAM,KAAK,qBAAqB,KAAK,UAAU,mBAAmB,KAAK,EAAE,CAAC,CAAC;KAC1H,IAAI,aAAa,eAAe,mBAAmB,YACjD,MAAM,IAAI,qBAAqB,oBAAoB,yBAAyB;KAE9E,MAAM,cAAc,MAAM,eAAe,aAAa;KACtD,OAAO,qBAAqB,KAAK,WAAW,YAAY,SAAS,aAAa,OAAO;KACrF,gBAAgB,aAAa,QAAQ;KACrC,sBAAsB,aAAa;KACnC,qBAAqB,YAAY;KACjC,kBAAkB,YAAY;KAC9B,qBAAqB,sBAAsB,aAAa,YAAY,KAAK,SAAS,aAAa,OAAO;KACtG,sBAAsB,sBAAsB,YAAY,YAAY,KAAK,UAAU,YAAY,OAAO;IACxG;IACA,IAAI,qBAAqB,mBAAmB;IAC5C,IAAI,KAAK,QAAQ,SAAS,KAAK,gBAAgB,GAC7C,IAAI;KACF,qBAAqB,MAAM,eAAe,YAAY,oBAAoB,KAAK,MAAM,KAAK,EAAE;IAC9F,SAAS,OAAO;KACd,IAAI,EAAE,iBAAiB,oBAAoB,MAAM,SAAS,2BAA2B,CAAC,iBAAiB,MAAM;KAC7G,MAAM,oBAAoB,0BAA0B,MAAM,KAAK,qBAC7D,KAAK,UAAU,oBACb,KAAK,IACL,KAAK,QAAQ,IAAI,mBAAmB,GACpC,gBAAiB,OAAO,MAC1B,CACF,CAAC;KACD,6BAA6B,KAAK,SAAS,kBAAkB,SAAS,IAAI;KAC1E,MAAM,gBAAgB,oBAAoB,gBAAgB,OAAO,SAAS,kBAAkB,OAAO;KACnG,gBAAgB,cAAc;KAC9B,qBAAqB,sBAAsB,gBAAgB,OAAO,YAAY,kBAAkB,SAAS,aAAa;KACtH,qBAAqB,MAAM,eAAe,YAAY,oBAAoB,KAAK,MAAM,KAAK,EAAE;IAC9F;IAGF,IAAI,sBAAsB,mBAAmB;IAC7C,IAAI,KAAK,SAAS,SAAS,GACzB,IAAI;KACF,sBAAsB,MAAM,KAAK,qBAC/B,KAAK,UAAU,mBAAmB,KAAK,IAAI,qBAAqB,eAAe,CACjF;IACF,SAAS,OAAO;KACd,IAAI,EAAE,iBAAiB,gCAAgC,CAAC,iBAAiB,MAAM;KAC/E,MAAM,mBAAmB,MAAM,eAAe,cAC5C,KAAK,SAAS,IAAI,mBAAmB,GACrC,gBAAgB,MAAM,QACtB,CAAC,GACD,EAAE,eAAe,qBAAqB,YAAY,KAAA,EAAU,CAC9D;KACA,6BAA6B,KAAK,UAAU,iBAAiB,SAAS,IAAI;KAC1E,MAAM,gBAAgB,oBAAoB,gBAAgB,MAAM,SAAS,iBAAiB,OAAO;KACjG,sBAAsB,iBAAiB,YAAY,KAAA,IAC/C,sBAAsB,iBAAiB,YAAY,iBAAiB,SAAS,eAAe,iBAAiB,OAAO,IACpH,sBAAsB,iBAAiB,YAAY,iBAAiB,SAAS,aAAa;KAC9F,sBAAsB,MAAM,KAAK,qBAC/B,KAAK,UAAU,mBAAmB,KAAK,IAAI,qBAAqB,eAAe,CACjF;IACF;IAEF,mBAAmB,mBAAmB,kBAAkB;IACxD,mBAAmB,mBAAmB,mBAAmB;IACzD,kBAAkB,KAAK,UAAU,mBAAmB,UAAU,oBAAoB;IAClF,SAAS,UAAU,WAAW,QAAQ,CAAC,CAAC,OAAO,GAAG,OAAO,IAAI,oBAAoB,IAAI,oBAAoB,CAAC,CAAC,OAAO,KAAK;GACzH;GACA,MAAM,aAAa,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY;GACpD,MAAM,UAA0B;IAC9B;IACA,WAAW,KAAK;IAChB;IACA;IACA,kBAAkB,mBAAmB;IACrC,UAAU,YAAY;IACtB,UAAU,aAAa;IACvB,SAAS,KAAK,UAAU,YAAY,UAAU,aAAa,UAAU;IACrE,gBAAgB,YAAY,iBAAiB,aAAa;IAC1D;IACA;IACA;GACF;GAEA,KAAK,MAAM,eAAe,KAAK,IAAI;IACjC,WAAW;IACX,eAAe;IACf,eAAe;IACf,oBAAoB;IACpB,WAAW;IACX,eAAe,KAAA;IACf,kBAAkB,KAAA;IAClB,YAAY;KACV,UAAU,QAAQ;KAClB,UAAU,QAAQ;KAClB,SAAS,QAAQ;KACjB,gBAAgB,QAAQ;KACxB;KACA;KACA;IACF;GACF,CAAC;GACD,OAAO;EACT,SAAS,OAAO;GACd,MAAM,UAAU,WAAW,KAAK;GAChC,IAAI,EAAE,iBAAiB,yBAAyB,EAAE,iBAAiB,sBAAsB,EAAE,iBAAiB,qBAAqB,EAAE,iBAAiB,0BAA0B,EAAE,iBAAiB,8BAA8B,EAAE,iBAAiB,wBAChP,QAAQ,UAAU,GAAG,QAAQ,QAAQ,GAAG,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAE/F,IAAI;IACF,KAAK,MAAM,eAAe,KAAK,IAAI;KACjC,WAAW,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY;KAC5C,eAAe;KACf,eAAe,QAAQ;KACvB,kBAAkB,QAAQ;IAC5B,CAAC;GACH,QAAQ,CAAC;GACT,MAAM;EACR;CACF;CAEA,MAAc,QAAgB,KAAoC;EAChE,IAAI,KAAK,SAAS,IAAI,MAAM,MAAM,KAAK,KAAK,SAAS,OAAO,MAAM;CACpE;CAEA,MAAc,eAAkB,WAAyC;EACvE,IAAI;GACF,OAAO,MAAM,UAAU;EACzB,SAAS,OAAO;GACd,IAAI,EAAE,iBAAiB,sBAAsB,CAAC;IAAC;IAAW;IAAW;GAAQ,CAAC,CAAC,SAAS,MAAM,QAAQ,GAAG,MAAM;GAC/G,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,wBAAwB,CAAC;GAC5E,OAAO,UAAU;EACnB;CACF;AACF"}
|
|
@@ -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 { f as SyncSessionService } from "../../session-sync-C2j2rk9K.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,7 +1,7 @@
|
|
|
1
1
|
import { isLoopbackRequest } from "../../auth/loopback.js";
|
|
2
2
|
import { SyncNodeManifestError } from "./node-sync.js";
|
|
3
|
-
import { t as SyncSessionError } from "../../session-sync-
|
|
4
|
-
import { MAX_SYNC_TRANSFER_BYTES } from "./protocol.js";
|
|
3
|
+
import { t as SyncSessionError } from "../../session-sync-C0uoizyB.js";
|
|
4
|
+
import { MAX_SYNC_TRANSFER_BYTES, 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
6
|
const MAX_APPLY_BODY_BYTES = MAX_SYNC_TRANSFER_BYTES;
|
|
7
7
|
function sendJson(res, status, body) {
|
|
@@ -56,6 +56,16 @@ function readJsonBody(req) {
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
function registerSyncInternalRoutes(ctx, state, nodeSync, sessionSync) {
|
|
59
|
+
const capabilities = [
|
|
60
|
+
"peer-handshake",
|
|
61
|
+
...nodeSync ? ["node-sync"] : [],
|
|
62
|
+
...sessionSync ? [
|
|
63
|
+
"session-sync",
|
|
64
|
+
SYNC_SESSION_MANIFEST_CAPABILITY,
|
|
65
|
+
SYNC_SESSION_EVENT_TAIL_CAPABILITY,
|
|
66
|
+
SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY
|
|
67
|
+
] : []
|
|
68
|
+
];
|
|
59
69
|
ctx.effect(() => ctx.webServer.register({
|
|
60
70
|
kind: "exact",
|
|
61
71
|
path: "/api/dshb/sync/internal/handshake",
|
|
@@ -79,11 +89,7 @@ function registerSyncInternalRoutes(ctx, state, nodeSync, sessionSync) {
|
|
|
79
89
|
ok: true,
|
|
80
90
|
protocolVersion: 1,
|
|
81
91
|
instanceId: state.instanceId(),
|
|
82
|
-
capabilities
|
|
83
|
-
"peer-handshake",
|
|
84
|
-
...nodeSync ? ["node-sync"] : [],
|
|
85
|
-
...sessionSync ? ["session-sync"] : []
|
|
86
|
-
]
|
|
92
|
+
capabilities
|
|
87
93
|
});
|
|
88
94
|
} catch {
|
|
89
95
|
sendJson(res, 503, {
|
|
@@ -117,7 +123,8 @@ function registerSyncInternalRoutes(ctx, state, nodeSync, sessionSync) {
|
|
|
117
123
|
try {
|
|
118
124
|
sendJson(res, 200, {
|
|
119
125
|
ok: true,
|
|
120
|
-
manifest: await nodeSync.exportManifest()
|
|
126
|
+
manifest: await nodeSync.exportManifest(),
|
|
127
|
+
capabilities
|
|
121
128
|
});
|
|
122
129
|
} catch {
|
|
123
130
|
sendJson(res, 500, {
|
|
@@ -193,6 +200,48 @@ function registerSyncInternalRoutes(ctx, state, nodeSync, sessionSync) {
|
|
|
193
200
|
}
|
|
194
201
|
return;
|
|
195
202
|
}
|
|
203
|
+
if (req.method === "GET" && action === "manifest") {
|
|
204
|
+
try {
|
|
205
|
+
sendJson(res, 200, {
|
|
206
|
+
ok: true,
|
|
207
|
+
manifest: await sessionSync.exportManifest()
|
|
208
|
+
});
|
|
209
|
+
} catch (error) {
|
|
210
|
+
const code = error instanceof SyncSessionError ? error.code : "session-export-failed";
|
|
211
|
+
sendJson(res, code === "bundle-too-large" ? 413 : 500, {
|
|
212
|
+
ok: false,
|
|
213
|
+
code,
|
|
214
|
+
error: error instanceof SyncSessionError ? error.message : "session manifest export failed"
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (req.method === "POST" && action === "entries") {
|
|
220
|
+
const body = await readJsonBody(req);
|
|
221
|
+
const prefixesValid = body?.prefixes === void 0 || Array.isArray(body.prefixes) && body.prefixes.every((prefix) => prefix !== null && typeof prefix === "object" && !Array.isArray(prefix));
|
|
222
|
+
if (!body || !Array.isArray(body.keys) || !body.keys.every((key) => typeof key === "string") || typeof body.manifestDigest !== "string" || !prefixesValid) {
|
|
223
|
+
sendJson(res, 400, {
|
|
224
|
+
ok: false,
|
|
225
|
+
code: "session-request-invalid",
|
|
226
|
+
error: "keys, manifestDigest, and valid prefixes are required"
|
|
227
|
+
});
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
sendJson(res, 200, {
|
|
232
|
+
ok: true,
|
|
233
|
+
bundle: await sessionSync.exportEntries(body.keys, body.manifestDigest, body.prefixes ?? [])
|
|
234
|
+
});
|
|
235
|
+
} catch (error) {
|
|
236
|
+
const code = error instanceof SyncSessionError ? error.code : "session-export-failed";
|
|
237
|
+
sendJson(res, code === "bundle-too-large" ? 413 : 500, {
|
|
238
|
+
ok: false,
|
|
239
|
+
code,
|
|
240
|
+
error: error instanceof SyncSessionError ? error.message : "session entries export failed"
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
196
245
|
if (req.method === "POST" && action === "apply") {
|
|
197
246
|
const body = await readJsonBody(req);
|
|
198
247
|
if (!body || body.bundle === null || typeof body.bundle !== "object" || typeof body.sourceName !== "string" || typeof body.peerId !== "string" || !/^p-[a-f0-9]{24}$/.test(body.peerId)) {
|
|
@@ -215,7 +264,7 @@ function registerSyncInternalRoutes(ctx, state, nodeSync, sessionSync) {
|
|
|
215
264
|
"manifest-digest",
|
|
216
265
|
"object-invalid",
|
|
217
266
|
"session-format"
|
|
218
|
-
].includes(code) ? 400 : code === "workspace-unmapped" ? 409 : 500, {
|
|
267
|
+
].includes(code) ? 400 : code === "workspace-unmapped" || code === "session-delta-prefix" ? 409 : 500, {
|
|
219
268
|
ok: false,
|
|
220
269
|
code,
|
|
221
270
|
error: error instanceof SyncSessionError ? error.message : "session bundle apply failed"
|
|
@@ -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 SyncSessionService } from './session-sync.js'\nimport { MAX_SYNC_TRANSFER_BYTES, SYNC_PROTOCOL_VERSION } 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 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 'peer-handshake',\n ...(nodeSync ? ['node-sync'] : []),\n ...(sessionSync ? ['session-sync'] : []),\n ],\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() })\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 === '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' ? 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
|
+
{"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,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 { f as SyncSessionService } from "../../session-sync-C2j2rk9K.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 { c as validateSyncSessionBundle, t as SyncSessionError } from "../../session-sync-C0uoizyB.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";
|