@artenx/dshb 0.1.30 → 0.1.32

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.
Files changed (31) hide show
  1. package/lib/core/index.js +6 -1
  2. package/lib/core/index.js.map +1 -1
  3. package/lib/core/sync/coordinator.d.ts +2 -2
  4. package/lib/core/sync/coordinator.js +108 -46
  5. package/lib/core/sync/coordinator.js.map +1 -1
  6. package/lib/core/sync/internal-routes.d.ts +1 -1
  7. package/lib/core/sync/internal-routes.js +81 -5
  8. package/lib/core/sync/internal-routes.js.map +1 -1
  9. package/lib/core/sync/offline-archive.d.ts +1 -1
  10. package/lib/core/sync/offline-archive.js +1 -1
  11. package/lib/core/sync/offline-routes.js +1 -1
  12. package/lib/core/sync/protocol.d.ts +2 -2
  13. package/lib/core/sync/protocol.js +5 -2
  14. package/lib/core/sync/protocol.js.map +1 -1
  15. package/lib/core/sync/remote-helper.js +107 -76
  16. package/lib/core/sync/remote-helper.js.map +1 -1
  17. package/lib/core/sync/routes.d.ts +1 -1
  18. package/lib/core/sync/session-routes.d.ts +1 -1
  19. package/lib/core/sync/session-routes.js +1 -1
  20. package/lib/core/sync/session-sync.d.ts +2 -2
  21. package/lib/core/sync/session-sync.js +2 -2
  22. package/lib/core/sync/ssh-transport.d.ts +1 -1
  23. package/lib/core/sync/ssh-transport.js +104 -6
  24. package/lib/core/sync/ssh-transport.js.map +1 -1
  25. package/lib/{protocol-KaWZArY8.d.ts → protocol-DJ0zyVlB.d.ts} +5 -2
  26. package/lib/{session-sync-C2j2rk9K.d.ts → session-sync-BuOJvFxK.d.ts} +64 -2
  27. package/lib/{session-sync-C0uoizyB.js → session-sync-CIqjuWkh.js} +548 -74
  28. package/lib/session-sync-CIqjuWkh.js.map +1 -0
  29. package/lib/{ssh-transport-uK9Ep0yp.d.ts → ssh-transport-B2oNDs0D.d.ts} +14 -4
  30. package/package.json +1 -1
  31. package/lib/session-sync-C0uoizyB.js.map +0 -1
package/lib/core/index.js CHANGED
@@ -11,7 +11,7 @@ import { registerWorkspaceInstructionRoutes } from "./workspace-instruction-rout
11
11
  import { WorkspaceBindingsStore } from "./workspace-bindings.js";
12
12
  import { registerWorkspaceInstructionInjector } from "./workspace-instruction-context.js";
13
13
  import { SyncPeerManager } from "./sync/peer-manager.js";
14
- import { n as SyncSessionService } from "../session-sync-C0uoizyB.js";
14
+ import { i as SyncSessionService } from "../session-sync-CIqjuWkh.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";
@@ -77,6 +77,10 @@ function apply(ctx) {
77
77
  registerSyncOfflineRoutes(ctx, syncOfflineArchives);
78
78
  ctx.effect(() => {
79
79
  const stopScheduler = syncScheduler.start();
80
+ const inboxSweeper = setInterval(() => {
81
+ syncSessions.objectInbox.sweep().catch(() => {});
82
+ }, 36e5);
83
+ inboxSweeper.unref();
80
84
  let disposed = false;
81
85
  setImmediate(() => {
82
86
  if (disposed) return;
@@ -95,6 +99,7 @@ function apply(ctx) {
95
99
  });
96
100
  return () => {
97
101
  disposed = true;
102
+ clearInterval(inboxSweeper);
98
103
  stopScheduler();
99
104
  };
100
105
  });
@@ -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 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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"}
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 // 带外对象收件箱 TTL 清扫:apply 失败/进程中断留下的孤儿文件按天回收。\n const inboxSweeper = setInterval(() => {\n void syncSessions.objectInbox.sweep().catch(() => {})\n }, 60 * 60 * 1000)\n inboxSweeper.unref()\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 clearInterval(inboxSweeper)\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 { f as SyncSessionService } from "../../session-sync-C2j2rk9K.js";
6
- import { s as SyncSshTransport } from "../../ssh-transport-uK9Ep0yp.js";
5
+ import { h as SyncSessionService } from "../../session-sync-BuOJvFxK.js";
6
+ import { s as SyncSshTransport } from "../../ssh-transport-B2oNDs0D.js";
7
7
  //#region src/core/sync/coordinator.d.ts
8
8
  interface NodeTransferPlan {
9
9
  toLocal: SyncNodeEntry[];
@@ -2,8 +2,8 @@ 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 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";
5
+ import { SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY, SYNC_SESSION_EVENT_TAIL_CAPABILITY, SYNC_SESSION_OBJECT_STREAM_CAPABILITY } from "./protocol.js";
6
+ import { c as planSessionTransfers, f as validateSyncSessionBundle, l as sessionEventListDigest, n as SyncSessionError, o as makeSyncSessionBundle, p as validateSyncSessionManifest, u as syncSessionEntryKey } from "../../session-sync-CIqjuWkh.js";
7
7
  import { SyncHostKeyChallengeError } from "./host-key.js";
8
8
  import { SyncSessionDeltaPrefixError, SyncSshProbeError } from "./ssh-transport.js";
9
9
  import { createHash } from "node:crypto";
@@ -16,6 +16,8 @@ const EMPTY_APPLY_RESULT = {
16
16
  skipped: 0
17
17
  };
18
18
  const TRANSPORT_RETRY_DELAY_MS = 250;
19
+ const SESSION_CHUNK_TARGET_BYTES = 12582912;
20
+ const SESSION_CHUNK_MAX_KEYS = 8;
19
21
  var SyncCoordinatorError = class extends Error {
20
22
  code;
21
23
  constructor(code, message) {
@@ -111,6 +113,16 @@ function emptySessionResult() {
111
113
  function sessionResultCount(result) {
112
114
  return result.created + result.updated + result.deleted;
113
115
  }
116
+ function addSessionResults(target, delta) {
117
+ return {
118
+ created: target.created + delta.created,
119
+ updated: target.updated + delta.updated,
120
+ archived: target.archived + delta.archived,
121
+ deleted: target.deleted + delta.deleted,
122
+ skipped: target.skipped + delta.skipped,
123
+ unmapped: target.unmapped + delta.unmapped
124
+ };
125
+ }
114
126
  function mergeSessionSkipped(...reports) {
115
127
  const merged = /* @__PURE__ */ new Map();
116
128
  for (const report of reports) for (const entry of report) merged.set(JSON.stringify([entry.sessionId, entry.reason]), entry);
@@ -223,39 +235,112 @@ var SyncCoordinator = class {
223
235
  if (peer.syncSessions) {
224
236
  const sessionService = this.sessions;
225
237
  let plan;
226
- let localBundleToApply;
227
- let remoteBundleToApply;
228
238
  let remoteSkipped = 0;
229
239
  let remoteSessionDigest = "";
230
240
  let localSessionDigest = "";
231
241
  let localInstanceId = "";
232
- let modernManifests;
242
+ let sessionLocalResult = emptySessionResult();
243
+ let sessionRemoteResult = emptySessionResult();
233
244
  if (remoteNodeManifest.capabilities?.includes("session-manifest-v1")) {
234
245
  const remoteManifest = validateSyncSessionManifest(await this.retryTransport(() => this.transport.fetchSessionManifest(peer.id)));
235
246
  if (remoteManifest.instanceId !== remoteNodeManifest.instanceId) throw new SyncCoordinatorError("protocol-invalid", "节点与会话 manifest 的实例身份不一致");
236
247
  const localManifest = await sessionService.exportManifest();
237
- modernManifests = {
238
- remote: remoteManifest,
239
- local: localManifest
240
- };
241
248
  plan = planSessionTransfers(peer.direction, localManifest.entries, remoteManifest.entries);
242
- remoteSkipped = remoteManifest.skipped.length;
243
249
  remoteSessionDigest = remoteManifest.digest;
244
250
  localSessionDigest = localManifest.digest;
245
251
  localInstanceId = localManifest.instanceId;
246
252
  const useEventDeltas = remoteNodeManifest.capabilities.includes(SYNC_SESSION_EVENT_TAIL_CAPABILITY);
247
253
  const useSharedObjects = remoteNodeManifest.capabilities.includes(SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY);
248
- const 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);
254
+ const useObjectStream = remoteNodeManifest.capabilities.includes(SYNC_SESSION_OBJECT_STREAM_CAPABILITY);
255
+ const remoteSkippedReports = [remoteManifest.skipped];
256
+ if (plan.toLocal.length > 0) {
257
+ let perKeyBytes = 0;
258
+ let firstChunk = true;
259
+ let cursor = 0;
260
+ const keys = plan.toLocal.map(syncSessionEntryKey);
261
+ while (cursor < keys.length) {
262
+ const take = perKeyBytes > 0 ? Math.max(1, Math.min(SESSION_CHUNK_MAX_KEYS, Math.floor(SESSION_CHUNK_TARGET_BYTES / perKeyBytes))) : SESSION_CHUNK_MAX_KEYS;
263
+ const chunkKeys = keys.slice(cursor, cursor + take);
264
+ cursor += chunkKeys.length;
265
+ const chunkKeySet = new Set(chunkKeys);
266
+ const chunkManifest = plan.toLocal.filter((entry) => chunkKeySet.has(syncSessionEntryKey(entry)));
267
+ const prefixes = useEventDeltas ? sessionEventPrefixesFor(chunkManifest, localManifest.entries) : [];
268
+ let fetched = validateSyncSessionBundle(await this.retryTransport(() => this.transport.fetchSessionEntries(peer.id, chunkKeys, remoteManifest.digest, prefixes, useObjectStream)));
269
+ if (fetched.instanceId !== remoteManifest.instanceId) throw new SyncCoordinatorError("protocol-invalid", "远端会话内容与 manifest 实例不一致");
270
+ assertSelectedSessionEntries(chunkManifest, fetched.entries, "远端");
271
+ remoteSkippedReports.push(fetched.skipped);
272
+ if (useObjectStream) {
273
+ const deferredObjects = fetched.entries.flatMap((entry) => (entry.objects ?? []).filter((object) => object.deferred === true));
274
+ if (deferredObjects.length > 0) await this.retryTransport(async () => {
275
+ await sessionService.ingestObjectStream(this.transport.fetchSessionObjects(peer.id, deferredObjects));
276
+ });
277
+ }
278
+ const applySkipped = firstChunk ? mergeSessionSkipped(remoteManifest.skipped, fetched.skipped) : fetched.skipped;
279
+ try {
280
+ const applied = await sessionService.applyBundle(makeSyncSessionBundle(remoteManifest.instanceId, fetched.entries, applySkipped), peer.name, peer.id);
281
+ sessionLocalResult = addSessionResults(sessionLocalResult, applied);
282
+ } catch (error) {
283
+ if (!(error instanceof SyncSessionError && error.code === "session-delta-prefix")) throw error;
284
+ const fullRemoteEntries = validateSyncSessionBundle(await this.retryTransport(() => this.transport.fetchSessionEntries(peer.id, chunkKeys, remoteManifest.digest, void 0, false)));
285
+ if (fullRemoteEntries.instanceId !== remoteManifest.instanceId) throw new SyncCoordinatorError("protocol-invalid", "远端会话内容与 manifest 实例不一致");
286
+ assertSelectedSessionEntries(chunkManifest, fullRemoteEntries.entries, "远端");
287
+ remoteSkippedReports.push(fullRemoteEntries.skipped);
288
+ const applied = await sessionService.applyBundle(makeSyncSessionBundle(remoteManifest.instanceId, fullRemoteEntries.entries, firstChunk ? mergeSessionSkipped(remoteManifest.skipped, fullRemoteEntries.skipped) : fullRemoteEntries.skipped), peer.name, peer.id);
289
+ sessionLocalResult = addSessionResults(sessionLocalResult, applied);
290
+ fetched = fullRemoteEntries;
291
+ }
292
+ const chunkBytes = Buffer.byteLength(JSON.stringify(fetched), "utf8");
293
+ if (chunkKeys.length > 0) perKeyBytes = Math.max(perKeyBytes, Math.ceil(chunkBytes / chunkKeys.length));
294
+ firstChunk = false;
295
+ }
296
+ } else if (remoteManifest.skipped.length > 0) {
297
+ const applied = await sessionService.applyBundle(makeSyncSessionBundle(remoteManifest.instanceId, [], remoteManifest.skipped), peer.name, peer.id);
298
+ sessionLocalResult = addSessionResults(sessionLocalResult, applied);
299
+ }
300
+ remoteSkipped = mergeSessionSkipped(...remoteSkippedReports).length;
301
+ if (plan.toRemote.length > 0) {
302
+ const keys = plan.toRemote.map(syncSessionEntryKey);
303
+ let perKeyBytes = 0;
304
+ let firstChunk = true;
305
+ let cursor = 0;
306
+ while (cursor < keys.length) {
307
+ const take = perKeyBytes > 0 ? Math.max(1, Math.min(SESSION_CHUNK_MAX_KEYS, Math.floor(SESSION_CHUNK_TARGET_BYTES / perKeyBytes))) : SESSION_CHUNK_MAX_KEYS;
308
+ const chunkKeys = keys.slice(cursor, cursor + take);
309
+ cursor += chunkKeys.length;
310
+ const chunkKeySet = new Set(chunkKeys);
311
+ const chunkManifest = plan.toRemote.filter((entry) => chunkKeySet.has(syncSessionEntryKey(entry)));
312
+ const prefixes = useEventDeltas ? sessionEventPrefixesFor(chunkManifest, remoteManifest.entries) : [];
313
+ const exported = await sessionService.exportEntries(chunkKeys, localManifest.digest, prefixes, {
314
+ sharedObjects: useSharedObjects,
315
+ deferredObjects: useObjectStream
316
+ });
317
+ assertSelectedSessionEntries(chunkManifest, exported.entries, "本地");
318
+ const wireSkipped = firstChunk ? mergeSessionSkipped(localManifest.skipped, exported.skipped) : exported.skipped;
319
+ let wireBundle = exported.objects !== void 0 ? makeSyncSessionBundle(exported.instanceId, exported.entries, wireSkipped, exported.objects) : makeSyncSessionBundle(exported.instanceId, exported.entries, wireSkipped);
320
+ if (useObjectStream) {
321
+ const deferredObjects = (wireBundle.objects ?? wireBundle.entries.flatMap((entry) => entry.objects ?? [])).filter((object) => object.deferred === true);
322
+ if (deferredObjects.length > 0) await this.retryTransport(() => this.transport.pushSessionObjects(peer.id, sessionService.streamObjects(deferredObjects)));
323
+ }
324
+ try {
325
+ const applied = await this.retryTransport(() => this.transport.applySessionBundle(peer.id, wireBundle, localInstanceId));
326
+ sessionRemoteResult = addSessionResults(sessionRemoteResult, applied);
327
+ } catch (error) {
328
+ if (!(error instanceof SyncSessionDeltaPrefixError)) throw error;
329
+ const fullLocalEntries = await sessionService.exportEntries(chunkKeys, localManifest.digest, [], {
330
+ sharedObjects: wireBundle.objects !== void 0,
331
+ deferredObjects: false
332
+ });
333
+ assertSelectedSessionEntries(chunkManifest, fullLocalEntries.entries, "本地");
334
+ const skippedReport = firstChunk ? mergeSessionSkipped(localManifest.skipped, fullLocalEntries.skipped) : fullLocalEntries.skipped;
335
+ wireBundle = fullLocalEntries.objects !== void 0 ? makeSyncSessionBundle(fullLocalEntries.instanceId, fullLocalEntries.entries, skippedReport, fullLocalEntries.objects) : makeSyncSessionBundle(fullLocalEntries.instanceId, fullLocalEntries.entries, skippedReport);
336
+ const applied = await this.retryTransport(() => this.transport.applySessionBundle(peer.id, wireBundle, localInstanceId));
337
+ sessionRemoteResult = addSessionResults(sessionRemoteResult, applied);
338
+ }
339
+ const chunkBytes = Buffer.byteLength(JSON.stringify(wireBundle), "utf8");
340
+ if (chunkKeys.length > 0) perKeyBytes = Math.max(perKeyBytes, Math.ceil(chunkBytes / chunkKeys.length));
341
+ firstChunk = false;
342
+ }
343
+ }
259
344
  } else {
260
345
  const remoteBundle = validateSyncSessionBundle(await this.retryTransport(() => this.transport.fetchSessionBundle(peer.id)));
261
346
  if (remoteBundle.instanceId !== remoteNodeManifest.instanceId) throw new SyncCoordinatorError("protocol-invalid", "节点与会话 manifest 的实例身份不一致");
@@ -265,31 +350,8 @@ var SyncCoordinator = class {
265
350
  remoteSessionDigest = remoteBundle.digest;
266
351
  localSessionDigest = localBundle.digest;
267
352
  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));
353
+ if (plan.toLocal.length > 0 || remoteSkipped > 0) sessionLocalResult = await sessionService.applyBundle(makeSyncSessionBundle(remoteBundle.instanceId, plan.toLocal, remoteBundle.skipped), peer.name, peer.id);
354
+ if (plan.toRemote.length > 0) sessionRemoteResult = await this.retryTransport(() => this.transport.applySessionBundle(peer.id, makeSyncSessionBundle(localBundle.instanceId, plan.toRemote, localBundle.skipped), localInstanceId));
293
355
  }
294
356
  importedSessions = sessionResultCount(sessionLocalResult);
295
357
  exportedSessions = sessionResultCount(sessionRemoteResult);
@@ -1 +1 @@
1
- {"version":3,"file":"coordinator.js","names":[],"sources":["../../../src/core/sync/coordinator.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport { SyncNodeManifestError, type SyncNodeApplyResult, type SyncNodeEntry, type SyncNodeService } from './node-sync.js'\nimport { SyncHostKeyChallengeError } from './host-key.js'\nimport { compareSyncRevision } from './revision.js'\nimport { syncRecordKey, type SyncPeer } from './model.js'\nimport { SyncPeerManager, SyncPeerNotFoundError } from './peer-manager.js'\nimport type { SyncStateStore } from './stores.js'\nimport { SyncSessionDeltaPrefixError, SyncSshProbeError, SyncSshTransport } from './ssh-transport.js'\nimport {\n makeSyncSessionBundle,\n planSessionTransfers,\n SyncSessionError,\n SyncSessionService,\n sessionEventListDigest,\n syncSessionEntryKey,\n validateSyncSessionBundle,\n validateSyncSessionManifest,\n} from './session-sync.js'\nimport type {\n SyncSessionApplyResult,\n SyncSessionBundle,\n SyncSessionEntry,\n SyncSessionEventPrefix,\n SyncSessionManifest,\n SyncSessionManifestEntry,\n} from './session-sync.js'\nimport { SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY, SYNC_SESSION_EVENT_TAIL_CAPABILITY, SYNC_SESSION_MANIFEST_CAPABILITY } from './protocol.js'\n\nconst EMPTY_APPLY_RESULT: SyncNodeApplyResult = { created: 0, updated: 0, deleted: 0, pendingRemoval: 0, skipped: 0 }\nconst TRANSPORT_RETRY_DELAY_MS = 250\n\nexport interface NodeTransferPlan {\n toLocal: SyncNodeEntry[]\n toRemote: SyncNodeEntry[]\n skipped: number\n}\n\nexport interface SyncRunSummary {\n peerId: string\n direction: SyncPeer['direction']\n startedAt: string\n finishedAt: string\n remoteInstanceId: string\n imported: number\n exported: number\n skipped: number\n pendingRemoval: number\n importedSessions: number\n exportedSessions: number\n skippedSessions: number\n}\n\nexport class SyncCoordinatorError extends Error {\n constructor(readonly code: string, message: string) {\n super(message)\n this.name = 'SyncCoordinatorError'\n }\n}\n\nfunction addCount(result: SyncNodeApplyResult): number {\n return result.created + result.updated + result.deleted\n}\n\nfunction manifestDigest(entries: readonly SyncNodeEntry[]): string {\n const versions = entries\n .map((entry) => ({\n key: syncRecordKey('node', entry.metadata),\n revision: entry.metadata.revision,\n contentDigest: entry.metadata.contentDigest,\n deleted: Boolean(entry.metadata.deleted),\n }))\n .sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0)\n return `sha256:${createHash('sha256').update(JSON.stringify(versions)).digest('hex')}`\n}\n\nexport function planNodeTransfers(\n direction: SyncPeer['direction'],\n localEntries: readonly SyncNodeEntry[],\n remoteEntries: readonly SyncNodeEntry[],\n): NodeTransferPlan {\n const toLocal: SyncNodeEntry[] = []\n const toRemote: SyncNodeEntry[] = []\n if (direction === 'pull') return { toLocal: [...remoteEntries], toRemote, skipped: 0 }\n\n const localByKey = new Map(localEntries.map((entry) => [syncRecordKey('node', entry.metadata), entry]))\n const remoteByKey = new Map(remoteEntries.map((entry) => [syncRecordKey('node', entry.metadata), entry]))\n const keys = new Set([...localByKey.keys(), ...remoteByKey.keys()])\n let skipped = 0\n\n for (const key of keys) {\n const local = localByKey.get(key)\n const remote = remoteByKey.get(key)\n if (!local) {\n toLocal.push(remote!)\n continue\n }\n if (!remote) {\n toRemote.push(local)\n continue\n }\n\n const comparison = compareSyncRevision(local.metadata.revision, remote.metadata.revision)\n if (comparison > 0) {\n toRemote.push(local)\n } else if (comparison < 0) {\n toLocal.push(remote)\n } else if (\n local.metadata.contentDigest === remote.metadata.contentDigest &&\n Boolean(local.metadata.deleted) === Boolean(remote.metadata.deleted)\n ) {\n skipped += 1\n } else {\n throw new SyncCoordinatorError('revision-conflict', 'Equal node revisions contain different content')\n }\n }\n return { toLocal, toRemote, skipped }\n}\n\nfunction mapFailure(error: unknown): SyncCoordinatorError {\n if (error instanceof SyncCoordinatorError) return error\n if (error instanceof SyncPeerNotFoundError) return new SyncCoordinatorError('peer-not-found', '同步节点已不存在')\n if (error instanceof SyncHostKeyChallengeError) {\n const code = error.decision === 'changed' ? 'host-key-changed' : 'host-key-untrusted'\n return new SyncCoordinatorError(code, error.message)\n }\n if (error instanceof SyncSshProbeError) {\n const messages: Record<string, string> = {\n auth: 'SSH 认证失败',\n network: 'SSH 节点不可达',\n timeout: 'SSH 连接超时',\n hostkey: 'SSH 主机密钥校验失败',\n helper: '远端同步 helper 不可用',\n protocol: '同步协议不兼容',\n }\n const baseMsg = messages[error.category] ?? 'SSH 同步失败'\n const detail = (error as Error & { detail?: string }).detail\n return new SyncCoordinatorError(`ssh-${error.category}`, detail ? `${baseMsg}:${detail}` : baseMsg)\n }\n if (error instanceof SyncNodeManifestError) return new SyncCoordinatorError('manifest-invalid', '节点 manifest 校验失败')\n if (error instanceof SyncSessionError) {\n return new SyncCoordinatorError(error.code.startsWith('session-') ? error.code : `session-${error.code}`, error.message)\n }\n return new SyncCoordinatorError('sync-failed', '同步作业失败')\n}\n\nfunction resultCount(result: SyncNodeApplyResult): { applied: number; skipped: number; pendingRemoval: number } {\n return {\n applied: addCount(result),\n skipped: result.skipped,\n pendingRemoval: result.pendingRemoval,\n }\n}\n\nfunction emptySessionResult(): SyncSessionApplyResult {\n return { created: 0, updated: 0, archived: 0, deleted: 0, skipped: 0, unmapped: 0 }\n}\n\nfunction sessionResultCount(result: SyncSessionApplyResult): number {\n return result.created + result.updated + result.deleted\n}\n\nfunction mergeSessionSkipped(...reports: Array<readonly { sessionId: string; reason: string }[]>): Array<{ sessionId: string; reason: string }> {\n const merged = new Map<string, { sessionId: string; reason: string }>()\n for (const report of reports) {\n for (const entry of report) merged.set(JSON.stringify([entry.sessionId, entry.reason]), entry)\n }\n return [...merged.values()]\n}\n\nfunction assertSelectedSessionEntries(\n expectedEntries: readonly SyncSessionManifestEntry[],\n selectedEntries: readonly SyncSessionEntry[],\n side: string,\n): void {\n const expected = new Map(expectedEntries.map((entry) => [syncSessionEntryKey(entry), entry]))\n const selected = new Map(selectedEntries.map((entry) => [syncSessionEntryKey(entry), entry]))\n if (selected.size !== selectedEntries.length || selected.size !== expected.size) {\n throw new SyncCoordinatorError('session-manifest-stale', `${side}会话内容与 manifest 清单不一致`)\n }\n for (const [key, expectedEntry] of expected) {\n const metadata = expectedEntry.metadata\n const currentEntry = selected.get(key)\n const current = currentEntry?.metadata\n const eventSummary = currentEntry?.eventDelta\n ? {\n eventCount: currentEntry.eventDelta.fullEventCount,\n eventDigest: currentEntry.eventDelta.fullEventDigest,\n inheritedEventCount: currentEntry.inheritedEventCount,\n }\n : currentEntry?.events\n ? {\n eventCount: currentEntry.events.length,\n eventDigest: sessionEventListDigest(currentEntry.events),\n inheritedEventCount: currentEntry.inheritedEventCount,\n }\n : undefined\n const hasExpectedEventSummary = expectedEntry.eventCount !== undefined || expectedEntry.eventDigest !== undefined ||\n expectedEntry.inheritedEventCount !== undefined\n const expectedWorkspace = metadata.workspaceOrigin\n const currentWorkspace = current?.workspaceOrigin\n const mismatches: string[] = []\n if (!current) mismatches.push('entry-missing')\n else {\n if (current.localEntityId !== metadata.localEntityId) mismatches.push('localEntityId')\n if (compareSyncRevision(current.revision, metadata.revision) !== 0) mismatches.push('revision')\n if (current.contentDigest !== metadata.contentDigest) mismatches.push('contentDigest')\n if (current.stateDigest !== metadata.stateDigest) mismatches.push('stateDigest')\n if (Boolean(current.deleted) !== Boolean(metadata.deleted)) mismatches.push('deleted')\n if (expectedWorkspace?.originInstanceId !== currentWorkspace?.originInstanceId ||\n expectedWorkspace?.originEntityId !== currentWorkspace?.originEntityId) mismatches.push('workspaceOrigin')\n if (hasExpectedEventSummary && (!eventSummary || eventSummary.eventCount !== expectedEntry.eventCount ||\n eventSummary.eventDigest !== expectedEntry.eventDigest || eventSummary.inheritedEventCount !== expectedEntry.inheritedEventCount)) {\n mismatches.push('eventSummary')\n }\n }\n if (mismatches.length > 0) {\n const detail = current && hasExpectedEventSummary\n ? ` expected(eventCount=${expectedEntry.eventCount}, inherited=${expectedEntry.inheritedEventCount}, digest=${expectedEntry.eventDigest})` +\n ` actual(${eventSummary ? `eventCount=${eventSummary.eventCount}, inherited=${eventSummary.inheritedEventCount}, digest=${eventSummary.eventDigest}` : 'no-events'})`\n : ''\n throw new SyncCoordinatorError('session-manifest-stale', `${side}会话内容在 manifest 之后发生变化(key=${key},字段=${mismatches.join(',')})${detail}`)\n }\n }\n}\n\nfunction sessionEventPrefixesFor(\n selectedEntries: readonly SyncSessionManifestEntry[],\n receiverEntries: readonly SyncSessionManifestEntry[],\n): SyncSessionEventPrefix[] {\n const receiverByKey = new Map(receiverEntries.map((entry) => [syncSessionEntryKey(entry), entry]))\n const prefixes: SyncSessionEventPrefix[] = []\n for (const selected of selectedEntries) {\n if (selected.branchId || selected.metadata.deleted) continue\n const receiver = receiverByKey.get(syncSessionEntryKey(selected))\n if (!receiver || receiver.branchId || receiver.metadata.deleted || receiver.eventCount === undefined ||\n receiver.eventDigest === undefined || receiver.inheritedEventCount === undefined) continue\n prefixes.push({\n key: syncSessionEntryKey(selected),\n eventCount: receiver.eventCount,\n eventDigest: receiver.eventDigest,\n inheritedEventCount: receiver.inheritedEventCount,\n })\n }\n return prefixes\n}\n\nexport class SyncCoordinator {\n private readonly inFlight = new Map<string, Promise<SyncRunSummary>>()\n\n constructor(\n private readonly peers: SyncPeerManager,\n private readonly state: SyncStateStore,\n private readonly nodes: SyncNodeService,\n private readonly transport: SyncSshTransport,\n private readonly now: () => number = Date.now,\n private readonly sessions?: SyncSessionService,\n ) {}\n\n run(peerId: string): Promise<SyncRunSummary> {\n const active = this.inFlight.get(peerId)\n if (active) return active\n const run = this.execute(peerId)\n this.inFlight.set(peerId, run)\n void run.then(() => this.clear(peerId, run), () => this.clear(peerId, run))\n return run\n }\n\n private async execute(peerId: string): Promise<SyncRunSummary> {\n const peer = await this.peers.get(peerId)\n if (!peer) throw new SyncCoordinatorError('peer-not-found', '同步节点已不存在')\n if (!peer.enabled) throw new SyncCoordinatorError('peer-disabled', '此同步节点已停用')\n\n const startedAt = new Date(this.now()).toISOString()\n this.peers.updateProgress(peer.id, {\n lastRunAt: startedAt,\n lastRunStatus: 'running',\n lastErrorCode: undefined,\n lastErrorMessage: undefined,\n })\n\n try {\n if (peer.syncSessions && !this.sessions) throw new SyncCoordinatorError('sessions-unavailable', '会话同步服务不可用')\n const remoteNodeManifest = await this.retryTransport(() => this.transport.fetchNodeManifest(peer.id))\n if (remoteNodeManifest.protocolVersion !== 1 || remoteNodeManifest.instanceId === this.state.instanceId()) {\n throw new SyncCoordinatorError('protocol-invalid', '同步 peer 协议版本或实例身份无效')\n }\n const remoteEntries = this.nodes.validateManifest(remoteNodeManifest.records)\n const localManifest = await this.nodes.exportManifest()\n const localEntries = this.nodes.validateManifest(localManifest.records)\n const plan = planNodeTransfers(peer.direction, localEntries, remoteEntries)\n\n const localResult = plan.toLocal.length > 0\n ? await this.nodes.applyManifest(plan.toLocal, peer.name, 'peer')\n : EMPTY_APPLY_RESULT\n const remoteResult = plan.toRemote.length > 0\n ? await this.retryTransport(() => this.transport.applyNodeManifest(peer.id, plan.toRemote, localManifest.instanceId))\n : EMPTY_APPLY_RESULT\n const localCounts = resultCount(localResult)\n const remoteCounts = resultCount(remoteResult)\n let importedSessions = 0\n let exportedSessions = 0\n let skippedSessions = 0\n let digest = manifestDigest(remoteEntries)\n if (peer.syncSessions) {\n const sessionService = this.sessions!\n let plan: ReturnType<typeof planSessionTransfers>\n let localBundleToApply: ReturnType<typeof makeSyncSessionBundle>\n let remoteBundleToApply: ReturnType<typeof makeSyncSessionBundle>\n let remoteSkipped = 0\n let remoteSessionDigest = ''\n let localSessionDigest = ''\n let localInstanceId = ''\n let modernManifests: { remote: SyncSessionManifest; local: SyncSessionManifest } | undefined\n if (remoteNodeManifest.capabilities?.includes(SYNC_SESSION_MANIFEST_CAPABILITY)) {\n const remoteManifest = validateSyncSessionManifest(\n await this.retryTransport(() => this.transport.fetchSessionManifest(peer.id)),\n )\n if (remoteManifest.instanceId !== remoteNodeManifest.instanceId) {\n throw new SyncCoordinatorError('protocol-invalid', '节点与会话 manifest 的实例身份不一致')\n }\n const localManifest = await sessionService.exportManifest()\n modernManifests = { remote: remoteManifest, local: localManifest }\n plan = planSessionTransfers(peer.direction, localManifest.entries, remoteManifest.entries)\n remoteSkipped = remoteManifest.skipped.length\n remoteSessionDigest = remoteManifest.digest\n localSessionDigest = localManifest.digest\n localInstanceId = localManifest.instanceId\n const useEventDeltas = remoteNodeManifest.capabilities.includes(SYNC_SESSION_EVENT_TAIL_CAPABILITY)\n const useSharedObjects = remoteNodeManifest.capabilities.includes(SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY)\n const toWireBundle = (wired: SyncSessionBundle, skippedReport: Array<{ sessionId: string; reason: string }>): SyncSessionBundle =>\n makeSyncSessionBundle(wired.instanceId, wired.entries, skippedReport, wired.objects)\n\n const remoteEntriesToApply = plan.toLocal.length > 0\n ? validateSyncSessionBundle(await this.retryTransport(() =>\n this.transport.fetchSessionEntries(\n peer.id,\n plan.toLocal.map(syncSessionEntryKey),\n remoteManifest.digest,\n useEventDeltas ? sessionEventPrefixesFor(plan.toLocal, localManifest.entries) : [],\n ),\n ))\n : makeSyncSessionBundle(remoteManifest.instanceId, [])\n if (remoteEntriesToApply.instanceId !== remoteManifest.instanceId) {\n throw new SyncCoordinatorError('protocol-invalid', '远端会话内容与 manifest 实例不一致')\n }\n assertSelectedSessionEntries(plan.toLocal, remoteEntriesToApply.entries, '远端')\n const localEntriesToApply = plan.toRemote.length > 0\n ? await sessionService.exportEntries(\n plan.toRemote.map(syncSessionEntryKey),\n localManifest.digest,\n useEventDeltas ? sessionEventPrefixesFor(plan.toRemote, remoteManifest.entries) : [],\n { sharedObjects: useSharedObjects },\n )\n : makeSyncSessionBundle(localManifest.instanceId, [])\n assertSelectedSessionEntries(plan.toRemote, localEntriesToApply.entries, '本地')\n const remoteSkippedReport = mergeSessionSkipped(remoteManifest.skipped, remoteEntriesToApply.skipped)\n const localSkippedReport = mergeSessionSkipped(localManifest.skipped, localEntriesToApply.skipped)\n remoteSkipped = remoteSkippedReport.length\n localBundleToApply = makeSyncSessionBundle(remoteManifest.instanceId, remoteEntriesToApply.entries, remoteSkippedReport)\n remoteBundleToApply = toWireBundle(localEntriesToApply, localSkippedReport)\n } else {\n const remoteBundle = validateSyncSessionBundle(await this.retryTransport(() => this.transport.fetchSessionBundle(peer.id)))\n if (remoteBundle.instanceId !== remoteNodeManifest.instanceId) {\n throw new SyncCoordinatorError('protocol-invalid', '节点与会话 manifest 的实例身份不一致')\n }\n const localBundle = await sessionService.exportBundle()\n plan = planSessionTransfers(peer.direction, localBundle.entries, remoteBundle.entries)\n remoteSkipped = remoteBundle.skipped.length\n remoteSessionDigest = remoteBundle.digest\n localSessionDigest = localBundle.digest\n localInstanceId = localBundle.instanceId\n localBundleToApply = makeSyncSessionBundle(remoteBundle.instanceId, plan.toLocal, remoteBundle.skipped)\n remoteBundleToApply = makeSyncSessionBundle(localBundle.instanceId, plan.toRemote, localBundle.skipped)\n }\n let sessionLocalResult = emptySessionResult()\n if (plan.toLocal.length > 0 || remoteSkipped > 0) {\n try {\n sessionLocalResult = await sessionService.applyBundle(localBundleToApply, peer.name, peer.id)\n } catch (error) {\n if (!(error instanceof SyncSessionError && error.code === 'session-delta-prefix') || !modernManifests) throw error\n const fullRemoteEntries = validateSyncSessionBundle(await this.retryTransport(() =>\n this.transport.fetchSessionEntries(\n peer.id,\n plan.toLocal.map(syncSessionEntryKey),\n modernManifests!.remote.digest,\n ),\n ))\n assertSelectedSessionEntries(plan.toLocal, fullRemoteEntries.entries, '远端')\n const skippedReport = mergeSessionSkipped(modernManifests.remote.skipped, fullRemoteEntries.skipped)\n remoteSkipped = skippedReport.length\n localBundleToApply = makeSyncSessionBundle(modernManifests.remote.instanceId, fullRemoteEntries.entries, skippedReport)\n sessionLocalResult = await sessionService.applyBundle(localBundleToApply, peer.name, peer.id)\n }\n }\n\n let sessionRemoteResult = emptySessionResult()\n if (plan.toRemote.length > 0) {\n try {\n sessionRemoteResult = await this.retryTransport(() =>\n this.transport.applySessionBundle(peer.id, remoteBundleToApply, localInstanceId),\n )\n } catch (error) {\n if (!(error instanceof SyncSessionDeltaPrefixError) || !modernManifests) throw error\n const fullLocalEntries = await sessionService.exportEntries(\n plan.toRemote.map(syncSessionEntryKey),\n modernManifests.local.digest,\n [],\n { sharedObjects: remoteBundleToApply?.objects !== undefined },\n )\n assertSelectedSessionEntries(plan.toRemote, fullLocalEntries.entries, '本地')\n const skippedReport = mergeSessionSkipped(modernManifests.local.skipped, fullLocalEntries.skipped)\n remoteBundleToApply = fullLocalEntries.objects !== undefined\n ? makeSyncSessionBundle(fullLocalEntries.instanceId, fullLocalEntries.entries, skippedReport, fullLocalEntries.objects)\n : makeSyncSessionBundle(fullLocalEntries.instanceId, fullLocalEntries.entries, skippedReport)\n sessionRemoteResult = await this.retryTransport(() =>\n this.transport.applySessionBundle(peer.id, remoteBundleToApply, localInstanceId),\n )\n }\n }\n importedSessions = sessionResultCount(sessionLocalResult)\n exportedSessions = sessionResultCount(sessionRemoteResult)\n skippedSessions = plan.skipped + sessionLocalResult.skipped + sessionRemoteResult.skipped\n digest = `sha256:${createHash('sha256').update(`${digest}\\0${remoteSessionDigest}\\0${localSessionDigest}`).digest('hex')}`\n }\n const finishedAt = new Date(this.now()).toISOString()\n const summary: SyncRunSummary = {\n peerId,\n direction: peer.direction,\n startedAt,\n finishedAt,\n remoteInstanceId: remoteNodeManifest.instanceId,\n imported: localCounts.applied,\n exported: remoteCounts.applied,\n skipped: plan.skipped + localCounts.skipped + remoteCounts.skipped + skippedSessions,\n pendingRemoval: localCounts.pendingRemoval + remoteCounts.pendingRemoval,\n importedSessions,\n exportedSessions,\n skippedSessions,\n }\n\n this.peers.updateProgress(peer.id, {\n lastRunAt: finishedAt,\n lastRunStatus: 'succeeded',\n lastSuccessAt: finishedAt,\n lastManifestDigest: digest,\n watermark: digest,\n lastErrorCode: undefined,\n lastErrorMessage: undefined,\n lastResult: {\n imported: summary.imported,\n exported: summary.exported,\n skipped: summary.skipped,\n pendingRemoval: summary.pendingRemoval,\n importedSessions,\n exportedSessions,\n skippedSessions,\n },\n })\n return summary\n } catch (error) {\n const failure = mapFailure(error)\n if (!(error instanceof SyncCoordinatorError) && !(error instanceof SyncSshProbeError) && !(error instanceof SyncSessionError) && !(error instanceof SyncNodeManifestError) && !(error instanceof SyncHostKeyChallengeError) && !(error instanceof SyncPeerNotFoundError)) {\n failure.message = `${failure.message}:${error instanceof Error ? error.message : String(error)}`\n }\n try {\n this.peers.updateProgress(peer.id, {\n lastRunAt: new Date(this.now()).toISOString(),\n lastRunStatus: 'failed',\n lastErrorCode: failure.code,\n lastErrorMessage: failure.message,\n })\n } catch {}\n throw failure\n }\n }\n\n private clear(peerId: string, run: Promise<SyncRunSummary>): void {\n if (this.inFlight.get(peerId) === run) this.inFlight.delete(peerId)\n }\n\n private async retryTransport<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation()\n } catch (error) {\n if (!(error instanceof SyncSshProbeError) || !['network', 'timeout', 'helper'].includes(error.category)) throw error\n await new Promise((resolve) => setTimeout(resolve, TRANSPORT_RETRY_DELAY_MS))\n return operation()\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"coordinator.js","names":[],"sources":["../../../src/core/sync/coordinator.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport { SyncNodeManifestError, type SyncNodeApplyResult, type SyncNodeEntry, type SyncNodeService } from './node-sync.js'\nimport { SyncHostKeyChallengeError } from './host-key.js'\nimport { compareSyncRevision } from './revision.js'\nimport { syncRecordKey, type SyncPeer } from './model.js'\nimport { SyncPeerManager, SyncPeerNotFoundError } from './peer-manager.js'\nimport type { SyncStateStore } from './stores.js'\nimport { SyncSessionDeltaPrefixError, SyncSshProbeError, SyncSshTransport } from './ssh-transport.js'\nimport {\n makeSyncSessionBundle,\n planSessionTransfers,\n SyncSessionError,\n SyncSessionService,\n sessionEventListDigest,\n syncSessionEntryKey,\n validateSyncSessionBundle,\n validateSyncSessionManifest,\n} from './session-sync.js'\nimport type {\n SyncSessionApplyResult,\n SyncSessionBundle,\n SyncSessionEntry,\n SyncSessionEventPrefix,\n SyncSessionObject,\n SyncSessionManifest,\n SyncSessionManifestEntry,\n} from './session-sync.js'\nimport { SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY, SYNC_SESSION_EVENT_TAIL_CAPABILITY, SYNC_SESSION_MANIFEST_CAPABILITY, SYNC_SESSION_OBJECT_STREAM_CAPABILITY } from './protocol.js'\n\nconst EMPTY_APPLY_RESULT: SyncNodeApplyResult = { created: 0, updated: 0, deleted: 0, pendingRemoval: 0, skipped: 0 }\nconst TRANSPORT_RETRY_DELAY_MS = 250\n// 会话分批交换的目标批体积与单批 key 数上限:目标值 ≈ JSON.parse 后对象图的\n// 堆占用仍远小于宿主堆上限(桌面端约 1.4GB);单批 key 上限限制批内的\n// 条目数,避免 manifest 条目本身过大。\nconst SESSION_CHUNK_TARGET_BYTES = 12 * 1024 * 1024\nconst SESSION_CHUNK_MAX_KEYS = 8\n\nexport interface NodeTransferPlan {\n toLocal: SyncNodeEntry[]\n toRemote: SyncNodeEntry[]\n skipped: number\n}\n\nexport interface SyncRunSummary {\n peerId: string\n direction: SyncPeer['direction']\n startedAt: string\n finishedAt: string\n remoteInstanceId: string\n imported: number\n exported: number\n skipped: number\n pendingRemoval: number\n importedSessions: number\n exportedSessions: number\n skippedSessions: number\n}\n\nexport class SyncCoordinatorError extends Error {\n constructor(readonly code: string, message: string) {\n super(message)\n this.name = 'SyncCoordinatorError'\n }\n}\n\nfunction addCount(result: SyncNodeApplyResult): number {\n return result.created + result.updated + result.deleted\n}\n\nfunction manifestDigest(entries: readonly SyncNodeEntry[]): string {\n const versions = entries\n .map((entry) => ({\n key: syncRecordKey('node', entry.metadata),\n revision: entry.metadata.revision,\n contentDigest: entry.metadata.contentDigest,\n deleted: Boolean(entry.metadata.deleted),\n }))\n .sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0)\n return `sha256:${createHash('sha256').update(JSON.stringify(versions)).digest('hex')}`\n}\n\nexport function planNodeTransfers(\n direction: SyncPeer['direction'],\n localEntries: readonly SyncNodeEntry[],\n remoteEntries: readonly SyncNodeEntry[],\n): NodeTransferPlan {\n const toLocal: SyncNodeEntry[] = []\n const toRemote: SyncNodeEntry[] = []\n if (direction === 'pull') return { toLocal: [...remoteEntries], toRemote, skipped: 0 }\n\n const localByKey = new Map(localEntries.map((entry) => [syncRecordKey('node', entry.metadata), entry]))\n const remoteByKey = new Map(remoteEntries.map((entry) => [syncRecordKey('node', entry.metadata), entry]))\n const keys = new Set([...localByKey.keys(), ...remoteByKey.keys()])\n let skipped = 0\n\n for (const key of keys) {\n const local = localByKey.get(key)\n const remote = remoteByKey.get(key)\n if (!local) {\n toLocal.push(remote!)\n continue\n }\n if (!remote) {\n toRemote.push(local)\n continue\n }\n\n const comparison = compareSyncRevision(local.metadata.revision, remote.metadata.revision)\n if (comparison > 0) {\n toRemote.push(local)\n } else if (comparison < 0) {\n toLocal.push(remote)\n } else if (\n local.metadata.contentDigest === remote.metadata.contentDigest &&\n Boolean(local.metadata.deleted) === Boolean(remote.metadata.deleted)\n ) {\n skipped += 1\n } else {\n throw new SyncCoordinatorError('revision-conflict', 'Equal node revisions contain different content')\n }\n }\n return { toLocal, toRemote, skipped }\n}\n\nfunction mapFailure(error: unknown): SyncCoordinatorError {\n if (error instanceof SyncCoordinatorError) return error\n if (error instanceof SyncPeerNotFoundError) return new SyncCoordinatorError('peer-not-found', '同步节点已不存在')\n if (error instanceof SyncHostKeyChallengeError) {\n const code = error.decision === 'changed' ? 'host-key-changed' : 'host-key-untrusted'\n return new SyncCoordinatorError(code, error.message)\n }\n if (error instanceof SyncSshProbeError) {\n const messages: Record<string, string> = {\n auth: 'SSH 认证失败',\n network: 'SSH 节点不可达',\n timeout: 'SSH 连接超时',\n hostkey: 'SSH 主机密钥校验失败',\n helper: '远端同步 helper 不可用',\n protocol: '同步协议不兼容',\n }\n const baseMsg = messages[error.category] ?? 'SSH 同步失败'\n const detail = (error as Error & { detail?: string }).detail\n return new SyncCoordinatorError(`ssh-${error.category}`, detail ? `${baseMsg}:${detail}` : baseMsg)\n }\n if (error instanceof SyncNodeManifestError) return new SyncCoordinatorError('manifest-invalid', '节点 manifest 校验失败')\n if (error instanceof SyncSessionError) {\n return new SyncCoordinatorError(error.code.startsWith('session-') ? error.code : `session-${error.code}`, error.message)\n }\n return new SyncCoordinatorError('sync-failed', '同步作业失败')\n}\n\nfunction resultCount(result: SyncNodeApplyResult): { applied: number; skipped: number; pendingRemoval: number } {\n return {\n applied: addCount(result),\n skipped: result.skipped,\n pendingRemoval: result.pendingRemoval,\n }\n}\n\nfunction emptySessionResult(): SyncSessionApplyResult {\n return { created: 0, updated: 0, archived: 0, deleted: 0, skipped: 0, unmapped: 0 }\n}\n\nfunction sessionResultCount(result: SyncSessionApplyResult): number {\n return result.created + result.updated + result.deleted\n}\n\nfunction addSessionResults(target: SyncSessionApplyResult, delta: SyncSessionApplyResult): SyncSessionApplyResult {\n return {\n created: target.created + delta.created,\n updated: target.updated + delta.updated,\n archived: target.archived + delta.archived,\n deleted: target.deleted + delta.deleted,\n skipped: target.skipped + delta.skipped,\n unmapped: target.unmapped + delta.unmapped,\n }\n}\n\nfunction mergeSessionSkipped(...reports: Array<readonly { sessionId: string; reason: string }[]>): Array<{ sessionId: string; reason: string }> {\n const merged = new Map<string, { sessionId: string; reason: string }>()\n for (const report of reports) {\n for (const entry of report) merged.set(JSON.stringify([entry.sessionId, entry.reason]), entry)\n }\n return [...merged.values()]\n}\n\nfunction assertSelectedSessionEntries(\n expectedEntries: readonly SyncSessionManifestEntry[],\n selectedEntries: readonly SyncSessionEntry[],\n side: string,\n): void {\n const expected = new Map(expectedEntries.map((entry) => [syncSessionEntryKey(entry), entry]))\n const selected = new Map(selectedEntries.map((entry) => [syncSessionEntryKey(entry), entry]))\n if (selected.size !== selectedEntries.length || selected.size !== expected.size) {\n throw new SyncCoordinatorError('session-manifest-stale', `${side}会话内容与 manifest 清单不一致`)\n }\n for (const [key, expectedEntry] of expected) {\n const metadata = expectedEntry.metadata\n const currentEntry = selected.get(key)\n const current = currentEntry?.metadata\n const eventSummary = currentEntry?.eventDelta\n ? {\n eventCount: currentEntry.eventDelta.fullEventCount,\n eventDigest: currentEntry.eventDelta.fullEventDigest,\n inheritedEventCount: currentEntry.inheritedEventCount,\n }\n : currentEntry?.events\n ? {\n eventCount: currentEntry.events.length,\n eventDigest: sessionEventListDigest(currentEntry.events),\n inheritedEventCount: currentEntry.inheritedEventCount,\n }\n : undefined\n const hasExpectedEventSummary = expectedEntry.eventCount !== undefined || expectedEntry.eventDigest !== undefined ||\n expectedEntry.inheritedEventCount !== undefined\n const expectedWorkspace = metadata.workspaceOrigin\n const currentWorkspace = current?.workspaceOrigin\n const mismatches: string[] = []\n if (!current) mismatches.push('entry-missing')\n else {\n if (current.localEntityId !== metadata.localEntityId) mismatches.push('localEntityId')\n if (compareSyncRevision(current.revision, metadata.revision) !== 0) mismatches.push('revision')\n if (current.contentDigest !== metadata.contentDigest) mismatches.push('contentDigest')\n if (current.stateDigest !== metadata.stateDigest) mismatches.push('stateDigest')\n if (Boolean(current.deleted) !== Boolean(metadata.deleted)) mismatches.push('deleted')\n if (expectedWorkspace?.originInstanceId !== currentWorkspace?.originInstanceId ||\n expectedWorkspace?.originEntityId !== currentWorkspace?.originEntityId) mismatches.push('workspaceOrigin')\n if (hasExpectedEventSummary && (!eventSummary || eventSummary.eventCount !== expectedEntry.eventCount ||\n eventSummary.eventDigest !== expectedEntry.eventDigest || eventSummary.inheritedEventCount !== expectedEntry.inheritedEventCount)) {\n mismatches.push('eventSummary')\n }\n }\n if (mismatches.length > 0) {\n const detail = current && hasExpectedEventSummary\n ? ` expected(eventCount=${expectedEntry.eventCount}, inherited=${expectedEntry.inheritedEventCount}, digest=${expectedEntry.eventDigest})` +\n ` actual(${eventSummary ? `eventCount=${eventSummary.eventCount}, inherited=${eventSummary.inheritedEventCount}, digest=${eventSummary.eventDigest}` : 'no-events'})`\n : ''\n throw new SyncCoordinatorError('session-manifest-stale', `${side}会话内容在 manifest 之后发生变化(key=${key},字段=${mismatches.join(',')})${detail}`)\n }\n }\n}\n\nfunction sessionEventPrefixesFor(\n selectedEntries: readonly SyncSessionManifestEntry[],\n receiverEntries: readonly SyncSessionManifestEntry[],\n): SyncSessionEventPrefix[] {\n const receiverByKey = new Map(receiverEntries.map((entry) => [syncSessionEntryKey(entry), entry]))\n const prefixes: SyncSessionEventPrefix[] = []\n for (const selected of selectedEntries) {\n if (selected.branchId || selected.metadata.deleted) continue\n const receiver = receiverByKey.get(syncSessionEntryKey(selected))\n if (!receiver || receiver.branchId || receiver.metadata.deleted || receiver.eventCount === undefined ||\n receiver.eventDigest === undefined || receiver.inheritedEventCount === undefined) continue\n prefixes.push({\n key: syncSessionEntryKey(selected),\n eventCount: receiver.eventCount,\n eventDigest: receiver.eventDigest,\n inheritedEventCount: receiver.inheritedEventCount,\n })\n }\n return prefixes\n}\n\nexport class SyncCoordinator {\n private readonly inFlight = new Map<string, Promise<SyncRunSummary>>()\n\n constructor(\n private readonly peers: SyncPeerManager,\n private readonly state: SyncStateStore,\n private readonly nodes: SyncNodeService,\n private readonly transport: SyncSshTransport,\n private readonly now: () => number = Date.now,\n private readonly sessions?: SyncSessionService,\n ) {}\n\n run(peerId: string): Promise<SyncRunSummary> {\n const active = this.inFlight.get(peerId)\n if (active) return active\n const run = this.execute(peerId)\n this.inFlight.set(peerId, run)\n void run.then(() => this.clear(peerId, run), () => this.clear(peerId, run))\n return run\n }\n\n private async execute(peerId: string): Promise<SyncRunSummary> {\n const peer = await this.peers.get(peerId)\n if (!peer) throw new SyncCoordinatorError('peer-not-found', '同步节点已不存在')\n if (!peer.enabled) throw new SyncCoordinatorError('peer-disabled', '此同步节点已停用')\n\n const startedAt = new Date(this.now()).toISOString()\n this.peers.updateProgress(peer.id, {\n lastRunAt: startedAt,\n lastRunStatus: 'running',\n lastErrorCode: undefined,\n lastErrorMessage: undefined,\n })\n\n try {\n if (peer.syncSessions && !this.sessions) throw new SyncCoordinatorError('sessions-unavailable', '会话同步服务不可用')\n const remoteNodeManifest = await this.retryTransport(() => this.transport.fetchNodeManifest(peer.id))\n if (remoteNodeManifest.protocolVersion !== 1 || remoteNodeManifest.instanceId === this.state.instanceId()) {\n throw new SyncCoordinatorError('protocol-invalid', '同步 peer 协议版本或实例身份无效')\n }\n const remoteEntries = this.nodes.validateManifest(remoteNodeManifest.records)\n const localManifest = await this.nodes.exportManifest()\n const localEntries = this.nodes.validateManifest(localManifest.records)\n const plan = planNodeTransfers(peer.direction, localEntries, remoteEntries)\n\n const localResult = plan.toLocal.length > 0\n ? await this.nodes.applyManifest(plan.toLocal, peer.name, 'peer')\n : EMPTY_APPLY_RESULT\n const remoteResult = plan.toRemote.length > 0\n ? await this.retryTransport(() => this.transport.applyNodeManifest(peer.id, plan.toRemote, localManifest.instanceId))\n : EMPTY_APPLY_RESULT\n const localCounts = resultCount(localResult)\n const remoteCounts = resultCount(remoteResult)\n let importedSessions = 0\n let exportedSessions = 0\n let skippedSessions = 0\n let digest = manifestDigest(remoteEntries)\n if (peer.syncSessions) {\n const sessionService = this.sessions!\n let plan: ReturnType<typeof planSessionTransfers>\n let remoteSkipped = 0\n let remoteSessionDigest = ''\n let localSessionDigest = ''\n let localInstanceId = ''\n let sessionLocalResult = emptySessionResult()\n let sessionRemoteResult = emptySessionResult()\n if (remoteNodeManifest.capabilities?.includes(SYNC_SESSION_MANIFEST_CAPABILITY)) {\n const remoteManifest = validateSyncSessionManifest(\n await this.retryTransport(() => this.transport.fetchSessionManifest(peer.id)),\n )\n if (remoteManifest.instanceId !== remoteNodeManifest.instanceId) {\n throw new SyncCoordinatorError('protocol-invalid', '节点与会话 manifest 的实例身份不一致')\n }\n const localManifest = await sessionService.exportManifest()\n plan = planSessionTransfers(peer.direction, localManifest.entries, remoteManifest.entries)\n remoteSessionDigest = remoteManifest.digest\n localSessionDigest = localManifest.digest\n localInstanceId = localManifest.instanceId\n const useEventDeltas = remoteNodeManifest.capabilities.includes(SYNC_SESSION_EVENT_TAIL_CAPABILITY)\n const useSharedObjects = remoteNodeManifest.capabilities.includes(SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY)\n const useObjectStream = remoteNodeManifest.capabilities.includes(SYNC_SESSION_OBJECT_STREAM_CAPABILITY)\n const remoteSkippedReports: Array<readonly { sessionId: string; reason: string }[]> = [remoteManifest.skipped]\n\n // 分批交换:首次同步(或长时间未同步后)plan 可能包含全部会话,一次性\n // fetch/export/apply 会让两端同时把整包内容驻留堆内(桌面端 1.4GB 堆\n // OOM 崩溃的根因,2026-11-01 崩溃日志)。按观测到的单 key 体积自适应\n // 分批:每批 fetch → 断言 → apply,批间释放内存;小计划(≤ 单批上限)\n // 仍只发一次请求,行为与旧实现一致。\n\n // 拉取方向(远端 → 本地)\n if (plan.toLocal.length > 0) {\n let perKeyBytes = 0\n let firstChunk = true\n let cursor = 0\n const keys = plan.toLocal.map(syncSessionEntryKey)\n while (cursor < keys.length) {\n const take = perKeyBytes > 0\n ? Math.max(1, Math.min(SESSION_CHUNK_MAX_KEYS, Math.floor(SESSION_CHUNK_TARGET_BYTES / perKeyBytes)))\n : SESSION_CHUNK_MAX_KEYS\n const chunkKeys = keys.slice(cursor, cursor + take)\n cursor += chunkKeys.length\n const chunkKeySet = new Set(chunkKeys)\n const chunkManifest = plan.toLocal.filter((entry) => chunkKeySet.has(syncSessionEntryKey(entry)))\n const prefixes = useEventDeltas ? sessionEventPrefixesFor(chunkManifest, localManifest.entries) : []\n let fetched = validateSyncSessionBundle(await this.retryTransport(() =>\n this.transport.fetchSessionEntries(peer.id, chunkKeys, remoteManifest.digest, prefixes, useObjectStream),\n ))\n if (fetched.instanceId !== remoteManifest.instanceId) {\n throw new SyncCoordinatorError('protocol-invalid', '远端会话内容与 manifest 实例不一致')\n }\n assertSelectedSessionEntries(chunkManifest, fetched.entries, '远端')\n remoteSkippedReports.push(fetched.skipped)\n if (useObjectStream) {\n // 带外对象:bundle 里 deferred 描述符的载荷经对象流 RPC 拉到本地\n // 收件箱(磁盘流式,内存 O(帧)),随后 applyBundle 按摘要水合。\n const deferredObjects = fetched.entries.flatMap((entry) => (entry.objects ?? []).filter((object) => object.deferred === true))\n if (deferredObjects.length > 0) {\n await this.retryTransport(async () => {\n await sessionService.ingestObjectStream(this.transport.fetchSessionObjects(peer.id, deferredObjects))\n })\n }\n }\n const applySkipped = firstChunk ? mergeSessionSkipped(remoteManifest.skipped, fetched.skipped) : fetched.skipped\n try {\n const applied = await sessionService.applyBundle(\n makeSyncSessionBundle(remoteManifest.instanceId, fetched.entries, applySkipped),\n peer.name,\n peer.id,\n )\n sessionLocalResult = addSessionResults(sessionLocalResult, applied)\n } catch (error) {\n if (!(error instanceof SyncSessionError && error.code === 'session-delta-prefix')) throw error\n // 回退拉全量内容:不带 deferred 标记,载荷内联(与既有回退语义一致,\n // 且不依赖已入库的收件箱对象)。\n const fullRemoteEntries = validateSyncSessionBundle(await this.retryTransport(() =>\n this.transport.fetchSessionEntries(peer.id, chunkKeys, remoteManifest.digest, undefined, false),\n ))\n if (fullRemoteEntries.instanceId !== remoteManifest.instanceId) {\n throw new SyncCoordinatorError('protocol-invalid', '远端会话内容与 manifest 实例不一致')\n }\n assertSelectedSessionEntries(chunkManifest, fullRemoteEntries.entries, '远端')\n remoteSkippedReports.push(fullRemoteEntries.skipped)\n const applied = await sessionService.applyBundle(\n makeSyncSessionBundle(\n remoteManifest.instanceId,\n fullRemoteEntries.entries,\n firstChunk ? mergeSessionSkipped(remoteManifest.skipped, fullRemoteEntries.skipped) : fullRemoteEntries.skipped,\n ),\n peer.name,\n peer.id,\n )\n sessionLocalResult = addSessionResults(sessionLocalResult, applied)\n fetched = fullRemoteEntries\n }\n const chunkBytes = Buffer.byteLength(JSON.stringify(fetched), 'utf8')\n if (chunkKeys.length > 0) perKeyBytes = Math.max(perKeyBytes, Math.ceil(chunkBytes / chunkKeys.length))\n firstChunk = false\n }\n } else if (remoteManifest.skipped.length > 0) {\n const applied = await sessionService.applyBundle(\n makeSyncSessionBundle(remoteManifest.instanceId, [], remoteManifest.skipped),\n peer.name,\n peer.id,\n )\n sessionLocalResult = addSessionResults(sessionLocalResult, applied)\n }\n remoteSkipped = mergeSessionSkipped(...remoteSkippedReports).length\n\n // 推送方向(本地 → 远端)\n if (plan.toRemote.length > 0) {\n const keys = plan.toRemote.map(syncSessionEntryKey)\n let perKeyBytes = 0\n let firstChunk = true\n let cursor = 0\n while (cursor < keys.length) {\n const take = perKeyBytes > 0\n ? Math.max(1, Math.min(SESSION_CHUNK_MAX_KEYS, Math.floor(SESSION_CHUNK_TARGET_BYTES / perKeyBytes)))\n : SESSION_CHUNK_MAX_KEYS\n const chunkKeys = keys.slice(cursor, cursor + take)\n cursor += chunkKeys.length\n const chunkKeySet = new Set(chunkKeys)\n const chunkManifest = plan.toRemote.filter((entry) => chunkKeySet.has(syncSessionEntryKey(entry)))\n const prefixes = useEventDeltas ? sessionEventPrefixesFor(chunkManifest, remoteManifest.entries) : []\n const exported = await sessionService.exportEntries(\n chunkKeys,\n localManifest.digest,\n prefixes,\n { sharedObjects: useSharedObjects, deferredObjects: useObjectStream },\n )\n assertSelectedSessionEntries(chunkManifest, exported.entries, '本地')\n const wireSkipped = firstChunk ? mergeSessionSkipped(localManifest.skipped, exported.skipped) : exported.skipped\n let wireBundle = exported.objects !== undefined\n ? makeSyncSessionBundle(exported.instanceId, exported.entries, wireSkipped, exported.objects)\n : makeSyncSessionBundle(exported.instanceId, exported.entries, wireSkipped)\n if (useObjectStream) {\n // 带外对象:先把 deferred 载荷推送进对端收件箱(磁盘流式,\n // 内存 O(帧)),再发 bundle;对端 apply 时按摘要水合。\n const deferredObjects = ((wireBundle.objects ?? wireBundle.entries.flatMap((entry) => entry.objects ?? [])) as SyncSessionObject[])\n .filter((object) => object.deferred === true)\n if (deferredObjects.length > 0) {\n await this.retryTransport(() =>\n this.transport.pushSessionObjects(peer.id, sessionService.streamObjects(deferredObjects)),\n )\n }\n }\n try {\n const applied = await this.retryTransport(() =>\n this.transport.applySessionBundle(peer.id, wireBundle, localInstanceId),\n )\n sessionRemoteResult = addSessionResults(sessionRemoteResult, applied)\n } catch (error) {\n if (!(error instanceof SyncSessionDeltaPrefixError)) throw error\n // 回退推全量内容:不带 deferred 标记,载荷内联。\n const fullLocalEntries = await sessionService.exportEntries(\n chunkKeys,\n localManifest.digest,\n [],\n { sharedObjects: wireBundle.objects !== undefined, deferredObjects: false },\n )\n assertSelectedSessionEntries(chunkManifest, fullLocalEntries.entries, '本地')\n const skippedReport = firstChunk ? mergeSessionSkipped(localManifest.skipped, fullLocalEntries.skipped) : fullLocalEntries.skipped\n wireBundle = fullLocalEntries.objects !== undefined\n ? makeSyncSessionBundle(fullLocalEntries.instanceId, fullLocalEntries.entries, skippedReport, fullLocalEntries.objects)\n : makeSyncSessionBundle(fullLocalEntries.instanceId, fullLocalEntries.entries, skippedReport)\n const applied = await this.retryTransport(() =>\n this.transport.applySessionBundle(peer.id, wireBundle, localInstanceId),\n )\n sessionRemoteResult = addSessionResults(sessionRemoteResult, applied)\n }\n const chunkBytes = Buffer.byteLength(JSON.stringify(wireBundle), 'utf8')\n if (chunkKeys.length > 0) perKeyBytes = Math.max(perKeyBytes, Math.ceil(chunkBytes / chunkKeys.length))\n firstChunk = false\n }\n }\n } else {\n const remoteBundle = validateSyncSessionBundle(await this.retryTransport(() => this.transport.fetchSessionBundle(peer.id)))\n if (remoteBundle.instanceId !== remoteNodeManifest.instanceId) {\n throw new SyncCoordinatorError('protocol-invalid', '节点与会话 manifest 的实例身份不一致')\n }\n const localBundle = await sessionService.exportBundle()\n plan = planSessionTransfers(peer.direction, localBundle.entries, remoteBundle.entries)\n remoteSkipped = remoteBundle.skipped.length\n remoteSessionDigest = remoteBundle.digest\n localSessionDigest = localBundle.digest\n localInstanceId = localBundle.instanceId\n if (plan.toLocal.length > 0 || remoteSkipped > 0) {\n sessionLocalResult = await sessionService.applyBundle(\n makeSyncSessionBundle(remoteBundle.instanceId, plan.toLocal, remoteBundle.skipped),\n peer.name,\n peer.id,\n )\n }\n if (plan.toRemote.length > 0) {\n sessionRemoteResult = await this.retryTransport(() =>\n this.transport.applySessionBundle(\n peer.id,\n makeSyncSessionBundle(localBundle.instanceId, plan.toRemote, localBundle.skipped),\n localInstanceId,\n ),\n )\n }\n }\n importedSessions = sessionResultCount(sessionLocalResult)\n exportedSessions = sessionResultCount(sessionRemoteResult)\n skippedSessions = plan.skipped + sessionLocalResult.skipped + sessionRemoteResult.skipped\n digest = `sha256:${createHash('sha256').update(`${digest}\\0${remoteSessionDigest}\\0${localSessionDigest}`).digest('hex')}`\n }\n const finishedAt = new Date(this.now()).toISOString()\n const summary: SyncRunSummary = {\n peerId,\n direction: peer.direction,\n startedAt,\n finishedAt,\n remoteInstanceId: remoteNodeManifest.instanceId,\n imported: localCounts.applied,\n exported: remoteCounts.applied,\n skipped: plan.skipped + localCounts.skipped + remoteCounts.skipped + skippedSessions,\n pendingRemoval: localCounts.pendingRemoval + remoteCounts.pendingRemoval,\n importedSessions,\n exportedSessions,\n skippedSessions,\n }\n\n this.peers.updateProgress(peer.id, {\n lastRunAt: finishedAt,\n lastRunStatus: 'succeeded',\n lastSuccessAt: finishedAt,\n lastManifestDigest: digest,\n watermark: digest,\n lastErrorCode: undefined,\n lastErrorMessage: undefined,\n lastResult: {\n imported: summary.imported,\n exported: summary.exported,\n skipped: summary.skipped,\n pendingRemoval: summary.pendingRemoval,\n importedSessions,\n exportedSessions,\n skippedSessions,\n },\n })\n return summary\n } catch (error) {\n const failure = mapFailure(error)\n if (!(error instanceof SyncCoordinatorError) && !(error instanceof SyncSshProbeError) && !(error instanceof SyncSessionError) && !(error instanceof SyncNodeManifestError) && !(error instanceof SyncHostKeyChallengeError) && !(error instanceof SyncPeerNotFoundError)) {\n failure.message = `${failure.message}:${error instanceof Error ? error.message : String(error)}`\n }\n try {\n this.peers.updateProgress(peer.id, {\n lastRunAt: new Date(this.now()).toISOString(),\n lastRunStatus: 'failed',\n lastErrorCode: failure.code,\n lastErrorMessage: failure.message,\n })\n } catch {}\n throw failure\n }\n }\n\n private clear(peerId: string, run: Promise<SyncRunSummary>): void {\n if (this.inFlight.get(peerId) === run) this.inFlight.delete(peerId)\n }\n\n private async retryTransport<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation()\n } catch (error) {\n if (!(error instanceof SyncSshProbeError) || !['network', 'timeout', 'helper'].includes(error.category)) throw error\n await new Promise((resolve) => setTimeout(resolve, TRANSPORT_RETRY_DELAY_MS))\n return operation()\n }\n }\n}\n"],"mappings":";;;;;;;;;;AA6BA,MAAM,qBAA0C;CAAE,SAAS;CAAG,SAAS;CAAG,SAAS;CAAG,gBAAgB;CAAG,SAAS;AAAE;AACpH,MAAM,2BAA2B;AAIjC,MAAM,6BAA6B;AACnC,MAAM,yBAAyB;AAuB/B,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,kBAAkB,QAAgC,OAAuD;CAChH,OAAO;EACL,SAAS,OAAO,UAAU,MAAM;EAChC,SAAS,OAAO,UAAU,MAAM;EAChC,UAAU,OAAO,WAAW,MAAM;EAClC,SAAS,OAAO,UAAU,MAAM;EAChC,SAAS,OAAO,UAAU,MAAM;EAChC,UAAU,OAAO,WAAW,MAAM;CACpC;AACF;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,gBAAgB;IACpB,IAAI,sBAAsB;IAC1B,IAAI,qBAAqB;IACzB,IAAI,kBAAkB;IACtB,IAAI,qBAAqB,mBAAmB;IAC5C,IAAI,sBAAsB,mBAAmB;IAC7C,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,OAAO,qBAAqB,KAAK,WAAW,cAAc,SAAS,eAAe,OAAO;KACzF,sBAAsB,eAAe;KACrC,qBAAqB,cAAc;KACnC,kBAAkB,cAAc;KAChC,MAAM,iBAAiB,mBAAmB,aAAa,SAAS,kCAAkC;KAClG,MAAM,mBAAmB,mBAAmB,aAAa,SAAS,sCAAsC;KACxG,MAAM,kBAAkB,mBAAmB,aAAa,SAAS,qCAAqC;KACtG,MAAM,uBAAgF,CAAC,eAAe,OAAO;KAS7G,IAAI,KAAK,QAAQ,SAAS,GAAG;MAC3B,IAAI,cAAc;MAClB,IAAI,aAAa;MACjB,IAAI,SAAS;MACb,MAAM,OAAO,KAAK,QAAQ,IAAI,mBAAmB;MACjD,OAAO,SAAS,KAAK,QAAQ;OAC3B,MAAM,OAAO,cAAc,IACvB,KAAK,IAAI,GAAG,KAAK,IAAI,wBAAwB,KAAK,MAAM,6BAA6B,WAAW,CAAC,CAAC,IAClG;OACJ,MAAM,YAAY,KAAK,MAAM,QAAQ,SAAS,IAAI;OAClD,UAAU,UAAU;OACpB,MAAM,cAAc,IAAI,IAAI,SAAS;OACrC,MAAM,gBAAgB,KAAK,QAAQ,QAAQ,UAAU,YAAY,IAAI,oBAAoB,KAAK,CAAC,CAAC;OAChG,MAAM,WAAW,iBAAiB,wBAAwB,eAAe,cAAc,OAAO,IAAI,CAAC;OACnG,IAAI,UAAU,0BAA0B,MAAM,KAAK,qBACjD,KAAK,UAAU,oBAAoB,KAAK,IAAI,WAAW,eAAe,QAAQ,UAAU,eAAe,CACzG,CAAC;OACD,IAAI,QAAQ,eAAe,eAAe,YACxC,MAAM,IAAI,qBAAqB,oBAAoB,wBAAwB;OAE7E,6BAA6B,eAAe,QAAQ,SAAS,IAAI;OACjE,qBAAqB,KAAK,QAAQ,OAAO;OACzC,IAAI,iBAAiB;QAGnB,MAAM,kBAAkB,QAAQ,QAAQ,SAAS,WAAW,MAAM,WAAW,CAAC,EAAA,CAAG,QAAQ,WAAW,OAAO,aAAa,IAAI,CAAC;QAC7H,IAAI,gBAAgB,SAAS,GAC3B,MAAM,KAAK,eAAe,YAAY;SACpC,MAAM,eAAe,mBAAmB,KAAK,UAAU,oBAAoB,KAAK,IAAI,eAAe,CAAC;QACtG,CAAC;OAEL;OACA,MAAM,eAAe,aAAa,oBAAoB,eAAe,SAAS,QAAQ,OAAO,IAAI,QAAQ;OACzG,IAAI;QACF,MAAM,UAAU,MAAM,eAAe,YACnC,sBAAsB,eAAe,YAAY,QAAQ,SAAS,YAAY,GAC9E,KAAK,MACL,KAAK,EACP;QACA,qBAAqB,kBAAkB,oBAAoB,OAAO;OACpE,SAAS,OAAO;QACd,IAAI,EAAE,iBAAiB,oBAAoB,MAAM,SAAS,yBAAyB,MAAM;QAGzF,MAAM,oBAAoB,0BAA0B,MAAM,KAAK,qBAC7D,KAAK,UAAU,oBAAoB,KAAK,IAAI,WAAW,eAAe,QAAQ,KAAA,GAAW,KAAK,CAChG,CAAC;QACD,IAAI,kBAAkB,eAAe,eAAe,YAClD,MAAM,IAAI,qBAAqB,oBAAoB,wBAAwB;QAE7E,6BAA6B,eAAe,kBAAkB,SAAS,IAAI;QAC3E,qBAAqB,KAAK,kBAAkB,OAAO;QACnD,MAAM,UAAU,MAAM,eAAe,YACnC,sBACE,eAAe,YACf,kBAAkB,SAClB,aAAa,oBAAoB,eAAe,SAAS,kBAAkB,OAAO,IAAI,kBAAkB,OAC1G,GACA,KAAK,MACL,KAAK,EACP;QACA,qBAAqB,kBAAkB,oBAAoB,OAAO;QAClE,UAAU;OACZ;OACA,MAAM,aAAa,OAAO,WAAW,KAAK,UAAU,OAAO,GAAG,MAAM;OACpE,IAAI,UAAU,SAAS,GAAG,cAAc,KAAK,IAAI,aAAa,KAAK,KAAK,aAAa,UAAU,MAAM,CAAC;OACtG,aAAa;MACf;KACF,OAAO,IAAI,eAAe,QAAQ,SAAS,GAAG;MAC5C,MAAM,UAAU,MAAM,eAAe,YACnC,sBAAsB,eAAe,YAAY,CAAC,GAAG,eAAe,OAAO,GAC3E,KAAK,MACL,KAAK,EACP;MACA,qBAAqB,kBAAkB,oBAAoB,OAAO;KACpE;KACA,gBAAgB,oBAAoB,GAAG,oBAAoB,CAAC,CAAC;KAG7D,IAAI,KAAK,SAAS,SAAS,GAAG;MAC5B,MAAM,OAAO,KAAK,SAAS,IAAI,mBAAmB;MAClD,IAAI,cAAc;MAClB,IAAI,aAAa;MACjB,IAAI,SAAS;MACb,OAAO,SAAS,KAAK,QAAQ;OAC3B,MAAM,OAAO,cAAc,IACvB,KAAK,IAAI,GAAG,KAAK,IAAI,wBAAwB,KAAK,MAAM,6BAA6B,WAAW,CAAC,CAAC,IAClG;OACJ,MAAM,YAAY,KAAK,MAAM,QAAQ,SAAS,IAAI;OAClD,UAAU,UAAU;OACpB,MAAM,cAAc,IAAI,IAAI,SAAS;OACrC,MAAM,gBAAgB,KAAK,SAAS,QAAQ,UAAU,YAAY,IAAI,oBAAoB,KAAK,CAAC,CAAC;OACjG,MAAM,WAAW,iBAAiB,wBAAwB,eAAe,eAAe,OAAO,IAAI,CAAC;OACpG,MAAM,WAAW,MAAM,eAAe,cACpC,WACA,cAAc,QACd,UACA;QAAE,eAAe;QAAkB,iBAAiB;OAAgB,CACtE;OACA,6BAA6B,eAAe,SAAS,SAAS,IAAI;OAClE,MAAM,cAAc,aAAa,oBAAoB,cAAc,SAAS,SAAS,OAAO,IAAI,SAAS;OACzG,IAAI,aAAa,SAAS,YAAY,KAAA,IAClC,sBAAsB,SAAS,YAAY,SAAS,SAAS,aAAa,SAAS,OAAO,IAC1F,sBAAsB,SAAS,YAAY,SAAS,SAAS,WAAW;OAC5E,IAAI,iBAAiB;QAGnB,MAAM,mBAAoB,WAAW,WAAW,WAAW,QAAQ,SAAS,UAAU,MAAM,WAAW,CAAC,CAAC,EAAA,CACtG,QAAQ,WAAW,OAAO,aAAa,IAAI;QAC9C,IAAI,gBAAgB,SAAS,GAC3B,MAAM,KAAK,qBACT,KAAK,UAAU,mBAAmB,KAAK,IAAI,eAAe,cAAc,eAAe,CAAC,CAC1F;OAEJ;OACA,IAAI;QACF,MAAM,UAAU,MAAM,KAAK,qBACzB,KAAK,UAAU,mBAAmB,KAAK,IAAI,YAAY,eAAe,CACxE;QACA,sBAAsB,kBAAkB,qBAAqB,OAAO;OACtE,SAAS,OAAO;QACd,IAAI,EAAE,iBAAiB,8BAA8B,MAAM;QAE3D,MAAM,mBAAmB,MAAM,eAAe,cAC5C,WACA,cAAc,QACd,CAAC,GACD;SAAE,eAAe,WAAW,YAAY,KAAA;SAAW,iBAAiB;QAAM,CAC5E;QACA,6BAA6B,eAAe,iBAAiB,SAAS,IAAI;QAC1E,MAAM,gBAAgB,aAAa,oBAAoB,cAAc,SAAS,iBAAiB,OAAO,IAAI,iBAAiB;QAC3H,aAAa,iBAAiB,YAAY,KAAA,IACtC,sBAAsB,iBAAiB,YAAY,iBAAiB,SAAS,eAAe,iBAAiB,OAAO,IACpH,sBAAsB,iBAAiB,YAAY,iBAAiB,SAAS,aAAa;QAC9F,MAAM,UAAU,MAAM,KAAK,qBACzB,KAAK,UAAU,mBAAmB,KAAK,IAAI,YAAY,eAAe,CACxE;QACA,sBAAsB,kBAAkB,qBAAqB,OAAO;OACtE;OACA,MAAM,aAAa,OAAO,WAAW,KAAK,UAAU,UAAU,GAAG,MAAM;OACvE,IAAI,UAAU,SAAS,GAAG,cAAc,KAAK,IAAI,aAAa,KAAK,KAAK,aAAa,UAAU,MAAM,CAAC;OACtG,aAAa;MACf;KACF;IACF,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,IAAI,KAAK,QAAQ,SAAS,KAAK,gBAAgB,GAC7C,qBAAqB,MAAM,eAAe,YACxC,sBAAsB,aAAa,YAAY,KAAK,SAAS,aAAa,OAAO,GACjF,KAAK,MACL,KAAK,EACP;KAEF,IAAI,KAAK,SAAS,SAAS,GACzB,sBAAsB,MAAM,KAAK,qBAC/B,KAAK,UAAU,mBACb,KAAK,IACL,sBAAsB,YAAY,YAAY,KAAK,UAAU,YAAY,OAAO,GAChF,eACF,CACF;IAEJ;IACA,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 { f as SyncSessionService } from "../../session-sync-C2j2rk9K.js";
3
+ import { h as SyncSessionService } from "../../session-sync-BuOJvFxK.js";
4
4
  import { Context } from "@deepseek-ai/cordis";
5
5
  //#region src/core/sync/internal-routes.d.ts
6
6
  declare function registerSyncInternalRoutes(ctx: Context, state: SyncStateStore, nodeSync?: SyncNodeService, sessionSync?: SyncSessionService): void;
@@ -1,9 +1,9 @@
1
1
  import { isLoopbackRequest } from "../../auth/loopback.js";
2
2
  import { SyncNodeManifestError } from "./node-sync.js";
3
- import { 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";
3
+ import { SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY, SYNC_SESSION_EVENT_TAIL_CAPABILITY, SYNC_SESSION_MANIFEST_CAPABILITY, SYNC_SESSION_OBJECT_STREAM_CAPABILITY } from "./protocol.js";
4
+ import { d as validateObjectPayload, n as SyncSessionError } from "../../session-sync-CIqjuWkh.js";
5
5
  //#region src/core/sync/internal-routes.ts
6
- const MAX_APPLY_BODY_BYTES = MAX_SYNC_TRANSFER_BYTES;
6
+ const MAX_APPLY_BODY_BYTES = 134217728;
7
7
  function sendJson(res, status, body) {
8
8
  res.writeHead(status, {
9
9
  "content-type": "application/json",
@@ -55,6 +55,77 @@ function readJsonBody(req) {
55
55
  req.on("error", fail);
56
56
  });
57
57
  }
58
+ /**
59
+ * 会话对象带外流端点(POST /api/dshb/sync/internal/sessions/objects):
60
+ * - 拉取模式(application/json 请求体 {objects: 描述符[]}):把本地对象编码为
61
+ * "JSON 头行 + 原始字节" 帧流响应,磁盘到网络流式读出,内存 O(帧)。
62
+ * - 推送模式(application/x-dshb-object-stream 请求体):请求帧流直接落盘
63
+ * 收件箱(边收边校验摘要),返回 JSON 摘要。
64
+ * 两种模式全程流式,单对象体积上限 MAX_SESSION_OBJECT_STREAM_BYTES(64GB),
65
+ * 不受 JSON bundle 传输上限约束。
66
+ */
67
+ async function handleSessionObjects(sessionSync, req, res) {
68
+ if (!isLoopbackRequest(req.socket.remoteAddress, req.headers.host)) {
69
+ sendJson(res, 403, {
70
+ ok: false,
71
+ error: "loopback access required"
72
+ });
73
+ return;
74
+ }
75
+ const contentType = req.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase();
76
+ if (contentType === "application/x-dshb-object-stream") {
77
+ try {
78
+ sendJson(res, 200, {
79
+ ok: true,
80
+ received: await sessionSync.ingestObjectStream(req)
81
+ });
82
+ } catch (error) {
83
+ sendJson(res, 422, {
84
+ ok: false,
85
+ code: error instanceof SyncSessionError ? error.code : "object-stream-failed",
86
+ error: error instanceof SyncSessionError ? error.message : "object stream ingest failed"
87
+ });
88
+ }
89
+ return;
90
+ }
91
+ if (contentType !== "application/json") {
92
+ sendJson(res, 400, {
93
+ ok: false,
94
+ code: "object-request-invalid",
95
+ error: "json or object-stream content type is required"
96
+ });
97
+ return;
98
+ }
99
+ const body = await readJsonBody(req);
100
+ if (!body || !Array.isArray(body.objects) || body.objects.length > 1e4 || body.objects.some((object) => {
101
+ try {
102
+ return validateObjectPayload(object).deferred !== true;
103
+ } catch {
104
+ return true;
105
+ }
106
+ })) {
107
+ sendJson(res, 400, {
108
+ ok: false,
109
+ code: "object-request-invalid",
110
+ error: "deferred session object descriptors are required"
111
+ });
112
+ return;
113
+ }
114
+ const objects = body.objects.map((object) => validateObjectPayload(object));
115
+ res.writeHead(200, {
116
+ "content-type": "application/x-dshb-object-stream",
117
+ "cache-control": "no-store"
118
+ });
119
+ try {
120
+ for await (const chunk of sessionSync.streamObjects(objects)) if (!res.write(chunk)) await new Promise((resolve, reject) => {
121
+ res.once("drain", resolve);
122
+ res.once("error", reject);
123
+ });
124
+ res.end();
125
+ } catch {
126
+ res.destroy();
127
+ }
128
+ }
58
129
  function registerSyncInternalRoutes(ctx, state, nodeSync, sessionSync) {
59
130
  const capabilities = [
60
131
  "peer-handshake",
@@ -63,7 +134,8 @@ function registerSyncInternalRoutes(ctx, state, nodeSync, sessionSync) {
63
134
  "session-sync",
64
135
  SYNC_SESSION_MANIFEST_CAPABILITY,
65
136
  SYNC_SESSION_EVENT_TAIL_CAPABILITY,
66
- SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY
137
+ SYNC_SESSION_BUNDLE_OBJECTS_CAPABILITY,
138
+ SYNC_SESSION_OBJECT_STREAM_CAPABILITY
67
139
  ] : []
68
140
  ];
69
141
  ctx.effect(() => ctx.webServer.register({
@@ -230,7 +302,7 @@ function registerSyncInternalRoutes(ctx, state, nodeSync, sessionSync) {
230
302
  try {
231
303
  sendJson(res, 200, {
232
304
  ok: true,
233
- bundle: await sessionSync.exportEntries(body.keys, body.manifestDigest, body.prefixes ?? [])
305
+ bundle: await sessionSync.exportEntries(body.keys, body.manifestDigest, body.prefixes ?? [], { deferredObjects: body.deferredObjects === true })
234
306
  });
235
307
  } catch (error) {
236
308
  const code = error instanceof SyncSessionError ? error.code : "session-export-failed";
@@ -242,6 +314,10 @@ function registerSyncInternalRoutes(ctx, state, nodeSync, sessionSync) {
242
314
  }
243
315
  return;
244
316
  }
317
+ if (req.method === "POST" && action === "objects") {
318
+ await handleSessionObjects(sessionSync, req, res);
319
+ return;
320
+ }
245
321
  if (req.method === "POST" && action === "apply") {
246
322
  const body = await readJsonBody(req);
247
323
  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)) {