@artenx/dshb 0.1.35 → 0.1.36

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.
@@ -48,6 +48,8 @@ declare class SyncCoordinator {
48
48
  /** 是否已手动停止:错误分类与收尾路径共用。 */
49
49
  private stopped;
50
50
  private throwIfStopped;
51
+ /** 进度错误消息必须满足校验器限制(≤256、无控制字符),否则收尾写失败会卡住状态。 */
52
+ private progressMessage;
51
53
  private execute;
52
54
  private clear;
53
55
  private retryTransport;
@@ -230,6 +230,10 @@ var SyncCoordinator = class {
230
230
  throwIfStopped(signal) {
231
231
  if (signal.aborted) throw new SyncCoordinatorError("stopped", "同步已被手动停止");
232
232
  }
233
+ /** 进度错误消息必须满足校验器限制(≤256、无控制字符),否则收尾写失败会卡住状态。 */
234
+ progressMessage(text) {
235
+ return text.replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 256);
236
+ }
233
237
  async execute(peerId, signal) {
234
238
  const peer = await this.peers.get(peerId);
235
239
  if (!peer) throw new SyncCoordinatorError("peer-not-found", "同步节点已不存在");
@@ -405,24 +409,26 @@ var SyncCoordinator = class {
405
409
  exportedSessions,
406
410
  skippedSessions
407
411
  };
408
- this.peers.updateProgress(peer.id, {
409
- lastRunAt: finishedAt,
410
- lastRunStatus: "succeeded",
411
- lastSuccessAt: finishedAt,
412
- lastManifestDigest: digest,
413
- watermark: digest,
414
- lastErrorCode: void 0,
415
- lastErrorMessage: void 0,
416
- lastResult: {
417
- imported: summary.imported,
418
- exported: summary.exported,
419
- skipped: summary.skipped,
420
- pendingRemoval: summary.pendingRemoval,
421
- importedSessions,
422
- exportedSessions,
423
- skippedSessions
424
- }
425
- });
412
+ try {
413
+ this.peers.updateProgress(peer.id, {
414
+ lastRunAt: finishedAt,
415
+ lastRunStatus: "succeeded",
416
+ lastSuccessAt: finishedAt,
417
+ lastManifestDigest: digest,
418
+ watermark: digest,
419
+ lastErrorCode: void 0,
420
+ lastErrorMessage: void 0,
421
+ lastResult: {
422
+ imported: summary.imported,
423
+ exported: summary.exported,
424
+ skipped: summary.skipped,
425
+ pendingRemoval: summary.pendingRemoval,
426
+ importedSessions,
427
+ exportedSessions,
428
+ skippedSessions
429
+ }
430
+ });
431
+ } catch {}
426
432
  return summary;
427
433
  } catch (error) {
428
434
  if (this.stopped(error)) {
@@ -461,7 +467,7 @@ var SyncCoordinator = class {
461
467
  lastRunAt: new Date(this.now()).toISOString(),
462
468
  lastRunStatus: "failed",
463
469
  lastErrorCode: failure.code,
464
- lastErrorMessage: failure.message
470
+ lastErrorMessage: this.progressMessage(failure.message)
465
471
  });
466
472
  } catch {}
467
473
  throw failure;
@@ -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 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: Promise<SyncRunSummary>; controller: AbortController }>()\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.promise\n const controller = new AbortController()\n const run = this.execute(peerId, controller.signal)\n this.inFlight.set(peerId, { promise: run, controller })\n void run.then(() => this.clear(peerId, run), () => this.clear(peerId, run))\n return run\n }\n\n /**\n * 手动停止运行中的同步:在最近的安全边界(批次之间)中止。\n * 返回是否确有运行中的作业。中止后丢弃本对端未完成的 journal 批次\n * (未应用条目不落地、重启不回放),水位保持不变,状态记为 stopped。\n */\n stop(peerId: string): boolean {\n const active = this.inFlight.get(peerId)\n if (!active) return false\n active.controller.abort()\n // 立即关闭该对端的活跃 SSH 通道:等待中的 readFrames 随即返回,\n // 不必等空闲超时(沙箱代理冻结场景下可能长达 60s)。\n try {\n this.transport.stopActiveSessions(peerId)\n } catch {}\n return true\n }\n\n /** 是否已手动停止:错误分类与收尾路径共用。 */\n private stopped(error: unknown): boolean {\n return error instanceof SyncCoordinatorError && error.code === 'stopped'\n }\n\n private throwIfStopped(signal: AbortSignal): void {\n if (signal.aborted) throw new SyncCoordinatorError('stopped', '同步已被手动停止')\n }\n\n private async execute(peerId: string, signal: AbortSignal): 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 if (signal.aborted) throw new SyncCoordinatorError('stopped', '同步已被手动停止')\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(signal, () => 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(signal, () => 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(signal, () => 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 this.throwIfStopped(signal)\n let fetched = validateSyncSessionBundle(await this.retryTransport(signal, () =>\n this.transport.fetchSessionEntries(peer.id, chunkKeys, remoteManifest.digest, prefixes, useObjectStream),\n ))\n this.throwIfStopped(signal)\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(signal, 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(signal, () =>\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 this.throwIfStopped(signal)\n const exported = await sessionService.exportEntries(\n chunkKeys,\n localManifest.digest,\n prefixes,\n { sharedObjects: useSharedObjects, deferredObjects: useObjectStream },\n )\n this.throwIfStopped(signal)\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(signal, () =>\n this.transport.pushSessionObjects(peer.id, sessionService.streamObjects(deferredObjects)),\n )\n }\n }\n try {\n const applied = await this.retryTransport(signal, () =>\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(signal, () =>\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 this.throwIfStopped(signal)\n const remoteBundle = validateSyncSessionBundle(await this.retryTransport(signal, () => this.transport.fetchSessionBundle(peer.id)))\n this.throwIfStopped(signal)\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(signal, () =>\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 if (this.stopped(error)) {\n // 手动停止:丢弃本对端未完成的 journal 批次(进行中的批不再落地、\n // 重启不回放),水位保持不变,状态记为 stopped。\n let discarded = 0\n try {\n discarded = this.sessions?.discardPendingBatches(peerId) ?? 0\n } catch {}\n const finishedAt = new Date(this.now()).toISOString()\n // 进度写失败不得遮蔽停止本身(校验器曾拒绝 stopped 值导致状态卡 running)。\n try {\n this.peers.updateProgress(peer.id, {\n lastRunAt: finishedAt,\n lastRunStatus: 'stopped',\n lastErrorCode: 'stopped',\n lastErrorMessage: discarded > 0 ? `已停止:丢弃 ${discarded} 个未完成批次,已完成的会话保留` : '已停止:已完成的会话保留',\n })\n } catch {}\n const summary: SyncRunSummary = {\n peerId,\n direction: peer.direction,\n startedAt,\n finishedAt,\n remoteInstanceId: '',\n imported: 0,\n exported: 0,\n skipped: 0,\n pendingRemoval: 0,\n importedSessions: 0,\n exportedSessions: 0,\n skippedSessions: 0,\n }\n return summary\n }\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)?.promise === run) this.inFlight.delete(peerId)\n }\n\n private async retryTransport<T>(signal: AbortSignal, operation: () => Promise<T>): Promise<T> {\n this.throwIfStopped(signal)\n try {\n return await operation()\n } catch (error) {\n if (signal.aborted) throw new SyncCoordinatorError('stopped', '同步已被手动停止')\n if (!(error instanceof SyncSshProbeError) || !['network', 'timeout', 'helper'].includes(error.category)) throw error\n this.throwIfStopped(signal)\n await new Promise((resolve) => setTimeout(resolve, TRANSPORT_RETRY_DELAY_MS))\n this.throwIfStopped(signal)\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: Promise<SyncRunSummary>; controller: AbortController }>()\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.promise\n const controller = new AbortController()\n const run = this.execute(peerId, controller.signal)\n this.inFlight.set(peerId, { promise: run, controller })\n void run.then(() => this.clear(peerId, run), () => this.clear(peerId, run))\n return run\n }\n\n /**\n * 手动停止运行中的同步:在最近的安全边界(批次之间)中止。\n * 返回是否确有运行中的作业。中止后丢弃本对端未完成的 journal 批次\n * (未应用条目不落地、重启不回放),水位保持不变,状态记为 stopped。\n */\n stop(peerId: string): boolean {\n const active = this.inFlight.get(peerId)\n if (!active) return false\n active.controller.abort()\n // 立即关闭该对端的活跃 SSH 通道:等待中的 readFrames 随即返回,\n // 不必等空闲超时(沙箱代理冻结场景下可能长达 60s)。\n try {\n this.transport.stopActiveSessions(peerId)\n } catch {}\n return true\n }\n\n /** 是否已手动停止:错误分类与收尾路径共用。 */\n private stopped(error: unknown): boolean {\n return error instanceof SyncCoordinatorError && error.code === 'stopped'\n }\n\n private throwIfStopped(signal: AbortSignal): void {\n if (signal.aborted) throw new SyncCoordinatorError('stopped', '同步已被手动停止')\n }\n\n /** 进度错误消息必须满足校验器限制(≤256、无控制字符),否则收尾写失败会卡住状态。 */\n private progressMessage(text: string): string {\n return text.replace(/[\\u0000-\\u001f\\u007f]/g, ' ').slice(0, 256)\n }\n\n private async execute(peerId: string, signal: AbortSignal): 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 if (signal.aborted) throw new SyncCoordinatorError('stopped', '同步已被手动停止')\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(signal, () => 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(signal, () => 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(signal, () => 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 this.throwIfStopped(signal)\n let fetched = validateSyncSessionBundle(await this.retryTransport(signal, () =>\n this.transport.fetchSessionEntries(peer.id, chunkKeys, remoteManifest.digest, prefixes, useObjectStream),\n ))\n this.throwIfStopped(signal)\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(signal, 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(signal, () =>\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 this.throwIfStopped(signal)\n const exported = await sessionService.exportEntries(\n chunkKeys,\n localManifest.digest,\n prefixes,\n { sharedObjects: useSharedObjects, deferredObjects: useObjectStream },\n )\n this.throwIfStopped(signal)\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(signal, () =>\n this.transport.pushSessionObjects(peer.id, sessionService.streamObjects(deferredObjects)),\n )\n }\n }\n try {\n const applied = await this.retryTransport(signal, () =>\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(signal, () =>\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 this.throwIfStopped(signal)\n const remoteBundle = validateSyncSessionBundle(await this.retryTransport(signal, () => this.transport.fetchSessionBundle(peer.id)))\n this.throwIfStopped(signal)\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(signal, () =>\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 // 成功路径的进度写也不得抛出(否则 promise 拒绝被路由吞掉,状态卡 running)。\n try {\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 } catch {}\n return summary\n } catch (error) {\n if (this.stopped(error)) {\n // 手动停止:丢弃本对端未完成的 journal 批次(进行中的批不再落地、\n // 重启不回放),水位保持不变,状态记为 stopped。\n let discarded = 0\n try {\n discarded = this.sessions?.discardPendingBatches(peerId) ?? 0\n } catch {}\n const finishedAt = new Date(this.now()).toISOString()\n // 进度写失败不得遮蔽停止本身(校验器曾拒绝 stopped 值导致状态卡 running)。\n try {\n this.peers.updateProgress(peer.id, {\n lastRunAt: finishedAt,\n lastRunStatus: 'stopped',\n lastErrorCode: 'stopped',\n lastErrorMessage: discarded > 0 ? `已停止:丢弃 ${discarded} 个未完成批次,已完成的会话保留` : '已停止:已完成的会话保留',\n })\n } catch {}\n const summary: SyncRunSummary = {\n peerId,\n direction: peer.direction,\n startedAt,\n finishedAt,\n remoteInstanceId: '',\n imported: 0,\n exported: 0,\n skipped: 0,\n pendingRemoval: 0,\n importedSessions: 0,\n exportedSessions: 0,\n skippedSessions: 0,\n }\n return summary\n }\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: this.progressMessage(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)?.promise === run) this.inFlight.delete(peerId)\n }\n\n private async retryTransport<T>(signal: AbortSignal, operation: () => Promise<T>): Promise<T> {\n this.throwIfStopped(signal)\n try {\n return await operation()\n } catch (error) {\n if (signal.aborted) throw new SyncCoordinatorError('stopped', '同步已被手动停止')\n if (!(error instanceof SyncSshProbeError) || !['network', 'timeout', 'helper'].includes(error.category)) throw error\n this.throwIfStopped(signal)\n await new Promise((resolve) => setTimeout(resolve, TRANSPORT_RETRY_DELAY_MS))\n this.throwIfStopped(signal)\n return operation()\n }\n }\n}\n"],"mappings":"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"}
@@ -114,12 +114,25 @@ var SyncPeerManager = class {
114
114
  }
115
115
  updateProgress(id, patch) {
116
116
  if (!this.peers.get(id)) throw new SyncPeerNotFoundError(id);
117
- this.state.update((state) => {
118
- state.peers[id] = {
119
- ...state.peers[id],
120
- ...patch
117
+ try {
118
+ this.state.update((state) => {
119
+ state.peers[id] = {
120
+ ...state.peers[id],
121
+ ...patch
122
+ };
123
+ });
124
+ } catch (error) {
125
+ const fallback = {
126
+ ...patch,
127
+ lastErrorMessage: patch.lastErrorMessage === void 0 ? void 0 : ("进度详情省略:" + patch.lastErrorMessage).replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 200)
121
128
  };
122
- });
129
+ this.state.update((state) => {
130
+ state.peers[id] = {
131
+ ...state.peers[id],
132
+ ...fallback
133
+ };
134
+ });
135
+ }
123
136
  }
124
137
  async get(id) {
125
138
  const peer = this.peers.get(id);
@@ -1 +1 @@
1
- {"version":3,"file":"peer-manager.js","names":[],"sources":["../../../src/core/sync/peer-manager.ts"],"sourcesContent":["import { credentialKey, type CredentialKey, type CredentialRecord } from '@deepseek-ai/dsh-credentials'\nimport { parseSyncPeer, parseSyncPeerDraft, type SyncPeer, type SyncPeerDraft, type SyncPeerProgress } from './model.js'\nimport { SyncPeerStore, SyncStateStore } from './stores.js'\n\nexport interface SyncPeerSecrets {\n password?: string\n privateKey?: string\n passphrase?: string\n}\n\nexport interface SyncPeerView extends SyncPeer {\n hasSecret: { hasPassword: boolean; hasKey: boolean; hasPassphrase: boolean }\n progress?: SyncPeerProgress\n}\n\nexport type SyncPeerChangeListener = (peerId: string, peer: SyncPeer | undefined) => void\n\ninterface CredentialsLike {\n readRecord(key: CredentialKey): Promise<unknown>\n modifyRecord(key: CredentialKey, mutate: (current: unknown) => Promise<unknown>): Promise<unknown>\n deleteRecord(key: CredentialKey): Promise<void>\n}\n\nconst SECRET_FIELDS = new Set(['password', 'privateKey', 'passphrase'])\nconst PATCH_FIELDS = new Set([\n 'name',\n 'host',\n 'port',\n 'username',\n 'authKind',\n 'remoteDshHome',\n 'remoteProfile',\n 'remoteWebPort',\n 'direction',\n 'schedule',\n 'syncSessions',\n 'enabled',\n])\nconst MAX_SECRET_BYTES = 64 * 1024\n\nexport class SyncPeerInputError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'SyncPeerInputError'\n }\n}\n\nexport class SyncPeerNotFoundError extends Error {\n constructor(id: string) {\n super(`sync peer ${id} not found`)\n this.name = 'SyncPeerNotFoundError'\n }\n}\n\nfunction asRecord(value: unknown, label: string): Record<string, unknown> {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new SyncPeerInputError(`${label} must be an object`)\n }\n return value as Record<string, unknown>\n}\n\nfunction parseSecrets(value: unknown): SyncPeerSecrets | undefined {\n if (value === undefined) return undefined\n const input = asRecord(value, 'secrets')\n for (const key of Object.keys(input)) {\n if (!SECRET_FIELDS.has(key)) throw new SyncPeerInputError(`unsupported secret field: ${key}`)\n }\n\n const output: SyncPeerSecrets = {}\n for (const field of ['password', 'privateKey', 'passphrase'] as const) {\n const secret = input[field]\n if (secret === undefined || secret === '') continue\n if (typeof secret !== 'string' || Buffer.byteLength(secret, 'utf8') > MAX_SECRET_BYTES || secret.includes('\\0')) {\n throw new SyncPeerInputError(`${field} is invalid`)\n }\n output[field] = secret\n }\n return Object.keys(output).length === 0 ? undefined : output\n}\n\nfunction payloadFromRecord(value: unknown): SyncPeerSecrets {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return {}\n const payload = (value as { payload?: unknown }).payload\n if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return {}\n const source = payload as Record<string, unknown>\n return {\n ...(typeof source.password === 'string' ? { password: source.password } : {}),\n ...(typeof source.privateKey === 'string' ? { privateKey: source.privateKey } : {}),\n ...(typeof source.passphrase === 'string' ? { passphrase: source.passphrase } : {}),\n }\n}\n\nfunction toView(peer: SyncPeer, secrets: SyncPeerSecrets): SyncPeerView {\n return {\n ...peer,\n hasSecret: {\n hasPassword: Boolean(secrets.password),\n hasKey: Boolean(secrets.privateKey),\n hasPassphrase: Boolean(secrets.passphrase),\n },\n }\n}\n\nfunction validateAuthSecrets(\n authKind: SyncPeer['authKind'],\n supplied: SyncPeerSecrets | undefined,\n effective: SyncPeerSecrets,\n): void {\n if (authKind === 'password') {\n if (supplied?.privateKey !== undefined || supplied?.passphrase !== undefined) {\n throw new SyncPeerInputError('key credentials do not match password authentication')\n }\n if (!effective.password) throw new SyncPeerInputError('password authentication requires a saved password')\n return\n }\n if (supplied?.password !== undefined) throw new SyncPeerInputError('password does not match key authentication')\n if (!effective.privateKey) throw new SyncPeerInputError('key authentication requires a saved private key')\n}\n\nfunction secretsForAuth(authKind: SyncPeer['authKind'], secrets: SyncPeerSecrets): SyncPeerSecrets {\n return authKind === 'password'\n ? (secrets.password ? { password: secrets.password } : {})\n : {\n ...(secrets.privateKey ? { privateKey: secrets.privateKey } : {}),\n ...(secrets.passphrase ? { passphrase: secrets.passphrase } : {}),\n }\n}\n\nfunction asInputError(error: unknown): SyncPeerInputError {\n return error instanceof SyncPeerInputError\n ? error\n : new SyncPeerInputError(error instanceof Error ? error.message : 'invalid sync peer')\n}\n\nexport class SyncPeerManager {\n private readonly listeners = new Set<SyncPeerChangeListener>()\n\n constructor(\n private readonly peers: SyncPeerStore,\n private readonly state: SyncStateStore,\n private readonly credentials: CredentialsLike,\n ) {}\n\n async list(): Promise<SyncPeerView[]> {\n return Promise.all(this.peers.list().map((peer) => this.view(peer)))\n }\n\n subscribe(listener: SyncPeerChangeListener): () => void {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n updateProgress(id: string, patch: SyncPeerProgress): void {\n if (!this.peers.get(id)) throw new SyncPeerNotFoundError(id)\n this.state.update((state) => {\n state.peers[id] = { ...state.peers[id], ...patch }\n })\n }\n\n async get(id: string): Promise<SyncPeerView | undefined> {\n const peer = this.peers.get(id)\n return peer ? this.view(peer) : undefined\n }\n\n async connectionData(id: string): Promise<{ peer: SyncPeer; secrets: SyncPeerSecrets }> {\n const peer = this.peers.get(id)\n if (!peer) throw new SyncPeerNotFoundError(id)\n const record = await this.credentials.readRecord(this.credentialKey(id))\n return { peer, secrets: payloadFromRecord(record) }\n }\n\n async pinHostKey(id: string, fingerprint: string): Promise<SyncPeerView> {\n if (!/^SHA256:[A-Za-z0-9+/]{43}$/.test(fingerprint)) {\n throw new SyncPeerInputError('invalid SSH host-key fingerprint')\n }\n const peer = this.peers.update(id, { hostKeyFingerprint: fingerprint })\n return this.view(peer)\n }\n\n async create(draftValue: unknown, secretsValue?: unknown): Promise<SyncPeerView> {\n let draft: SyncPeerDraft\n let secrets: SyncPeerSecrets | undefined\n try {\n draft = parseSyncPeerDraft(draftValue)\n secrets = parseSecrets(secretsValue)\n validateAuthSecrets(draft.authKind, secrets, secrets ?? {})\n } catch (error) {\n throw asInputError(error)\n }\n\n const peer = this.peers.create(draft)\n let attemptedSecrets = false\n try {\n if (secrets) {\n attemptedSecrets = true\n await this.writeSecrets(peer.id, secretsForAuth(peer.authKind, secrets))\n }\n const view = await this.view(peer)\n this.notify(peer.id, peer)\n return view\n } catch (error) {\n this.peers.remove(peer.id)\n if (attemptedSecrets) {\n try {\n await this.credentials.deleteRecord(this.credentialKey(peer.id))\n } catch {}\n }\n throw error\n }\n }\n\n async update(id: string, patchValue: unknown, secretsValue?: unknown): Promise<SyncPeerView> {\n const current = this.peers.get(id)\n if (!current) throw new SyncPeerNotFoundError(id)\n\n let patch: Partial<Omit<SyncPeer, 'id' | 'hostKeyFingerprint'>>\n let secrets: SyncPeerSecrets | undefined\n try {\n const input = asRecord(patchValue, 'sync peer patch')\n for (const key of Object.keys(input)) {\n if (!PATCH_FIELDS.has(key)) throw new SyncPeerInputError(`unsupported peer field: ${key}`)\n }\n const validated = parseSyncPeer({ ...current, ...input, id })\n const { id: _id, hostKeyFingerprint: _fingerprint, ...validatedPatch } = validated\n void _id\n void _fingerprint\n patch = validatedPatch\n secrets = parseSecrets(secretsValue)\n } catch (error) {\n throw asInputError(error)\n }\n\n const credentialKeyValue = this.credentialKey(id)\n const previousRecord = await this.credentials.readRecord(credentialKeyValue)\n const effectiveSecrets = { ...payloadFromRecord(previousRecord), ...secrets }\n const authKind = patch.authKind ?? current.authKind\n validateAuthSecrets(authKind, secrets, effectiveSecrets)\n if (secrets || authKind !== current.authKind) {\n try {\n await this.writeSecrets(id, secretsForAuth(authKind, effectiveSecrets))\n } catch (error) {\n await this.restoreCredentialRecord(credentialKeyValue, previousRecord)\n throw error\n }\n }\n\n let updated: SyncPeer\n try {\n updated = this.peers.update(id, patch)\n } catch (error) {\n await this.restoreCredentialRecord(credentialKeyValue, previousRecord)\n throw error\n }\n this.notify(id, updated)\n return this.view(updated)\n }\n\n async remove(id: string): Promise<boolean> {\n if (!this.peers.get(id)) return false\n this.peers.remove(id)\n this.notify(id, undefined)\n await this.credentials.deleteRecord(this.credentialKey(id))\n this.state.update((state) => {\n delete state.peers[id]\n })\n return true\n }\n\n private async view(peer: SyncPeer): Promise<SyncPeerView> {\n const record = await this.credentials.readRecord(this.credentialKey(peer.id))\n const progress = this.state.read().peers[peer.id]\n return { ...toView(peer, payloadFromRecord(record)), ...(progress ? { progress } : {}) }\n }\n\n private notify(peerId: string, peer: SyncPeer | undefined): void {\n for (const listener of this.listeners) {\n try {\n listener(peerId, peer)\n } catch {}\n }\n }\n\n private async writeSecrets(id: string, secrets: SyncPeerSecrets): Promise<void> {\n const key = this.credentialKey(id)\n await this.credentials.modifyRecord(key, async (_current) => ({\n kind: 'grant',\n payload: secrets,\n }) as CredentialRecord)\n }\n\n private async restoreCredentialRecord(key: CredentialKey, previous: unknown): Promise<void> {\n try {\n if (previous === undefined) await this.credentials.deleteRecord(key)\n else await this.credentials.modifyRecord(key, async () => previous)\n } catch {}\n }\n\n private credentialKey(id: string): CredentialKey {\n try {\n return credentialKey('dshb-core', id)\n } catch {\n throw new Error('credentials service unavailable')\n }\n }\n}\n"],"mappings":";;;AAuBA,MAAM,gCAAgB,IAAI,IAAI;CAAC;CAAY;CAAc;AAAY,CAAC;AACtE,MAAM,+BAAe,IAAI,IAAI;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AACD,MAAM,mBAAmB;AAEzB,IAAa,qBAAb,cAAwC,MAAM;CAC5C,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;AAEA,IAAa,wBAAb,cAA2C,MAAM;CAC/C,YAAY,IAAY;EACtB,MAAM,aAAa,GAAG,WAAW;EACjC,KAAK,OAAO;CACd;AACF;AAEA,SAAS,SAAS,OAAgB,OAAwC;CACxE,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GACpE,MAAM,IAAI,mBAAmB,GAAG,MAAM,mBAAmB;CAE3D,OAAO;AACT;AAEA,SAAS,aAAa,OAA6C;CACjE,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;CAChC,MAAM,QAAQ,SAAS,OAAO,SAAS;CACvC,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GACjC,IAAI,CAAC,cAAc,IAAI,GAAG,GAAG,MAAM,IAAI,mBAAmB,6BAA6B,KAAK;CAG9F,MAAM,SAA0B,CAAC;CACjC,KAAK,MAAM,SAAS;EAAC;EAAY;EAAc;CAAY,GAAY;EACrE,MAAM,SAAS,MAAM;EACrB,IAAI,WAAW,KAAA,KAAa,WAAW,IAAI;EAC3C,IAAI,OAAO,WAAW,YAAY,OAAO,WAAW,QAAQ,MAAM,IAAI,oBAAoB,OAAO,SAAS,IAAI,GAC5G,MAAM,IAAI,mBAAmB,GAAG,MAAM,YAAY;EAEpD,OAAO,SAAS;CAClB;CACA,OAAO,OAAO,KAAK,MAAM,CAAC,CAAC,WAAW,IAAI,KAAA,IAAY;AACxD;AAEA,SAAS,kBAAkB,OAAiC;CAC1D,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG,OAAO,CAAC;CACjF,MAAM,UAAW,MAAgC;CACjD,IAAI,YAAY,QAAQ,OAAO,YAAY,YAAY,MAAM,QAAQ,OAAO,GAAG,OAAO,CAAC;CACvF,MAAM,SAAS;CACf,OAAO;EACL,GAAI,OAAO,OAAO,aAAa,WAAW,EAAE,UAAU,OAAO,SAAS,IAAI,CAAC;EAC3E,GAAI,OAAO,OAAO,eAAe,WAAW,EAAE,YAAY,OAAO,WAAW,IAAI,CAAC;EACjF,GAAI,OAAO,OAAO,eAAe,WAAW,EAAE,YAAY,OAAO,WAAW,IAAI,CAAC;CACnF;AACF;AAEA,SAAS,OAAO,MAAgB,SAAwC;CACtE,OAAO;EACL,GAAG;EACH,WAAW;GACT,aAAa,QAAQ,QAAQ,QAAQ;GACrC,QAAQ,QAAQ,QAAQ,UAAU;GAClC,eAAe,QAAQ,QAAQ,UAAU;EAC3C;CACF;AACF;AAEA,SAAS,oBACP,UACA,UACA,WACM;CACN,IAAI,aAAa,YAAY;EAC3B,IAAI,UAAU,eAAe,KAAA,KAAa,UAAU,eAAe,KAAA,GACjE,MAAM,IAAI,mBAAmB,sDAAsD;EAErF,IAAI,CAAC,UAAU,UAAU,MAAM,IAAI,mBAAmB,mDAAmD;EACzG;CACF;CACA,IAAI,UAAU,aAAa,KAAA,GAAW,MAAM,IAAI,mBAAmB,4CAA4C;CAC/G,IAAI,CAAC,UAAU,YAAY,MAAM,IAAI,mBAAmB,iDAAiD;AAC3G;AAEA,SAAS,eAAe,UAAgC,SAA2C;CACjG,OAAO,aAAa,aACf,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC,IACtD;EACE,GAAI,QAAQ,aAAa,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;EAC/D,GAAI,QAAQ,aAAa,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;CACjE;AACN;AAEA,SAAS,aAAa,OAAoC;CACxD,OAAO,iBAAiB,qBACpB,QACA,IAAI,mBAAmB,iBAAiB,QAAQ,MAAM,UAAU,mBAAmB;AACzF;AAEA,IAAa,kBAAb,MAA6B;CAIR;CACA;CACA;CALnB,4BAA6B,IAAI,IAA4B;CAE7D,YACE,OACA,OACA,aACA;EAHiB,KAAA,QAAA;EACA,KAAA,QAAA;EACA,KAAA,cAAA;CAChB;CAEH,MAAM,OAAgC;EACpC,OAAO,QAAQ,IAAI,KAAK,MAAM,KAAK,CAAC,CAAC,KAAK,SAAS,KAAK,KAAK,IAAI,CAAC,CAAC;CACrE;CAEA,UAAU,UAA8C;EACtD,KAAK,UAAU,IAAI,QAAQ;EAC3B,aAAa;GAAE,KAAK,UAAU,OAAO,QAAQ;EAAE;CACjD;CAEA,eAAe,IAAY,OAA+B;EACxD,IAAI,CAAC,KAAK,MAAM,IAAI,EAAE,GAAG,MAAM,IAAI,sBAAsB,EAAE;EAC3D,KAAK,MAAM,QAAQ,UAAU;GAC3B,MAAM,MAAM,MAAM;IAAE,GAAG,MAAM,MAAM;IAAK,GAAG;GAAM;EACnD,CAAC;CACH;CAEA,MAAM,IAAI,IAA+C;EACvD,MAAM,OAAO,KAAK,MAAM,IAAI,EAAE;EAC9B,OAAO,OAAO,KAAK,KAAK,IAAI,IAAI,KAAA;CAClC;CAEA,MAAM,eAAe,IAAmE;EACtF,MAAM,OAAO,KAAK,MAAM,IAAI,EAAE;EAC9B,IAAI,CAAC,MAAM,MAAM,IAAI,sBAAsB,EAAE;EAE7C,OAAO;GAAE;GAAM,SAAS,kBAAkB,MADrB,KAAK,YAAY,WAAW,KAAK,cAAc,EAAE,CAAC,CACvB;EAAE;CACpD;CAEA,MAAM,WAAW,IAAY,aAA4C;EACvE,IAAI,CAAC,6BAA6B,KAAK,WAAW,GAChD,MAAM,IAAI,mBAAmB,kCAAkC;EAEjE,MAAM,OAAO,KAAK,MAAM,OAAO,IAAI,EAAE,oBAAoB,YAAY,CAAC;EACtE,OAAO,KAAK,KAAK,IAAI;CACvB;CAEA,MAAM,OAAO,YAAqB,cAA+C;EAC/E,IAAI;EACJ,IAAI;EACJ,IAAI;GACF,QAAQ,mBAAmB,UAAU;GACrC,UAAU,aAAa,YAAY;GACnC,oBAAoB,MAAM,UAAU,SAAS,WAAW,CAAC,CAAC;EAC5D,SAAS,OAAO;GACd,MAAM,aAAa,KAAK;EAC1B;EAEA,MAAM,OAAO,KAAK,MAAM,OAAO,KAAK;EACpC,IAAI,mBAAmB;EACvB,IAAI;GACF,IAAI,SAAS;IACX,mBAAmB;IACnB,MAAM,KAAK,aAAa,KAAK,IAAI,eAAe,KAAK,UAAU,OAAO,CAAC;GACzE;GACA,MAAM,OAAO,MAAM,KAAK,KAAK,IAAI;GACjC,KAAK,OAAO,KAAK,IAAI,IAAI;GACzB,OAAO;EACT,SAAS,OAAO;GACd,KAAK,MAAM,OAAO,KAAK,EAAE;GACzB,IAAI,kBACF,IAAI;IACF,MAAM,KAAK,YAAY,aAAa,KAAK,cAAc,KAAK,EAAE,CAAC;GACjE,QAAQ,CAAC;GAEX,MAAM;EACR;CACF;CAEA,MAAM,OAAO,IAAY,YAAqB,cAA+C;EAC3F,MAAM,UAAU,KAAK,MAAM,IAAI,EAAE;EACjC,IAAI,CAAC,SAAS,MAAM,IAAI,sBAAsB,EAAE;EAEhD,IAAI;EACJ,IAAI;EACJ,IAAI;GACF,MAAM,QAAQ,SAAS,YAAY,iBAAiB;GACpD,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GACjC,IAAI,CAAC,aAAa,IAAI,GAAG,GAAG,MAAM,IAAI,mBAAmB,2BAA2B,KAAK;GAG3F,MAAM,EAAE,IAAI,KAAK,oBAAoB,cAAc,GAAG,mBADpC,cAAc;IAAE,GAAG;IAAS,GAAG;IAAO;GAAG,CACsB;GAGjF,QAAQ;GACR,UAAU,aAAa,YAAY;EACrC,SAAS,OAAO;GACd,MAAM,aAAa,KAAK;EAC1B;EAEA,MAAM,qBAAqB,KAAK,cAAc,EAAE;EAChD,MAAM,iBAAiB,MAAM,KAAK,YAAY,WAAW,kBAAkB;EAC3E,MAAM,mBAAmB;GAAE,GAAG,kBAAkB,cAAc;GAAG,GAAG;EAAQ;EAC5E,MAAM,WAAW,MAAM,YAAY,QAAQ;EAC3C,oBAAoB,UAAU,SAAS,gBAAgB;EACvD,IAAI,WAAW,aAAa,QAAQ,UAClC,IAAI;GACF,MAAM,KAAK,aAAa,IAAI,eAAe,UAAU,gBAAgB,CAAC;EACxE,SAAS,OAAO;GACd,MAAM,KAAK,wBAAwB,oBAAoB,cAAc;GACrE,MAAM;EACR;EAGF,IAAI;EACJ,IAAI;GACF,UAAU,KAAK,MAAM,OAAO,IAAI,KAAK;EACvC,SAAS,OAAO;GACd,MAAM,KAAK,wBAAwB,oBAAoB,cAAc;GACrE,MAAM;EACR;EACA,KAAK,OAAO,IAAI,OAAO;EACvB,OAAO,KAAK,KAAK,OAAO;CAC1B;CAEA,MAAM,OAAO,IAA8B;EACzC,IAAI,CAAC,KAAK,MAAM,IAAI,EAAE,GAAG,OAAO;EAChC,KAAK,MAAM,OAAO,EAAE;EACpB,KAAK,OAAO,IAAI,KAAA,CAAS;EACzB,MAAM,KAAK,YAAY,aAAa,KAAK,cAAc,EAAE,CAAC;EAC1D,KAAK,MAAM,QAAQ,UAAU;GAC3B,OAAO,MAAM,MAAM;EACrB,CAAC;EACD,OAAO;CACT;CAEA,MAAc,KAAK,MAAuC;EACxD,MAAM,SAAS,MAAM,KAAK,YAAY,WAAW,KAAK,cAAc,KAAK,EAAE,CAAC;EAC5E,MAAM,WAAW,KAAK,MAAM,KAAK,CAAC,CAAC,MAAM,KAAK;EAC9C,OAAO;GAAE,GAAG,OAAO,MAAM,kBAAkB,MAAM,CAAC;GAAG,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EAAG;CACzF;CAEA,OAAe,QAAgB,MAAkC;EAC/D,KAAK,MAAM,YAAY,KAAK,WAC1B,IAAI;GACF,SAAS,QAAQ,IAAI;EACvB,QAAQ,CAAC;CAEb;CAEA,MAAc,aAAa,IAAY,SAAyC;EAC9E,MAAM,MAAM,KAAK,cAAc,EAAE;EACjC,MAAM,KAAK,YAAY,aAAa,KAAK,OAAO,cAAc;GAC5D,MAAM;GACN,SAAS;EACX,EAAsB;CACxB;CAEA,MAAc,wBAAwB,KAAoB,UAAkC;EAC1F,IAAI;GACF,IAAI,aAAa,KAAA,GAAW,MAAM,KAAK,YAAY,aAAa,GAAG;QAC9D,MAAM,KAAK,YAAY,aAAa,KAAK,YAAY,QAAQ;EACpE,QAAQ,CAAC;CACX;CAEA,cAAsB,IAA2B;EAC/C,IAAI;GACF,OAAO,cAAc,aAAa,EAAE;EACtC,QAAQ;GACN,MAAM,IAAI,MAAM,iCAAiC;EACnD;CACF;AACF"}
1
+ {"version":3,"file":"peer-manager.js","names":[],"sources":["../../../src/core/sync/peer-manager.ts"],"sourcesContent":["import { credentialKey, type CredentialKey, type CredentialRecord } from '@deepseek-ai/dsh-credentials'\nimport { parseSyncPeer, parseSyncPeerDraft, type SyncPeer, type SyncPeerDraft, type SyncPeerProgress } from './model.js'\nimport { SyncPeerStore, SyncStateStore } from './stores.js'\n\nexport interface SyncPeerSecrets {\n password?: string\n privateKey?: string\n passphrase?: string\n}\n\nexport interface SyncPeerView extends SyncPeer {\n hasSecret: { hasPassword: boolean; hasKey: boolean; hasPassphrase: boolean }\n progress?: SyncPeerProgress\n}\n\nexport type SyncPeerChangeListener = (peerId: string, peer: SyncPeer | undefined) => void\n\ninterface CredentialsLike {\n readRecord(key: CredentialKey): Promise<unknown>\n modifyRecord(key: CredentialKey, mutate: (current: unknown) => Promise<unknown>): Promise<unknown>\n deleteRecord(key: CredentialKey): Promise<void>\n}\n\nconst SECRET_FIELDS = new Set(['password', 'privateKey', 'passphrase'])\nconst PATCH_FIELDS = new Set([\n 'name',\n 'host',\n 'port',\n 'username',\n 'authKind',\n 'remoteDshHome',\n 'remoteProfile',\n 'remoteWebPort',\n 'direction',\n 'schedule',\n 'syncSessions',\n 'enabled',\n])\nconst MAX_SECRET_BYTES = 64 * 1024\n\nexport class SyncPeerInputError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'SyncPeerInputError'\n }\n}\n\nexport class SyncPeerNotFoundError extends Error {\n constructor(id: string) {\n super(`sync peer ${id} not found`)\n this.name = 'SyncPeerNotFoundError'\n }\n}\n\nfunction asRecord(value: unknown, label: string): Record<string, unknown> {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new SyncPeerInputError(`${label} must be an object`)\n }\n return value as Record<string, unknown>\n}\n\nfunction parseSecrets(value: unknown): SyncPeerSecrets | undefined {\n if (value === undefined) return undefined\n const input = asRecord(value, 'secrets')\n for (const key of Object.keys(input)) {\n if (!SECRET_FIELDS.has(key)) throw new SyncPeerInputError(`unsupported secret field: ${key}`)\n }\n\n const output: SyncPeerSecrets = {}\n for (const field of ['password', 'privateKey', 'passphrase'] as const) {\n const secret = input[field]\n if (secret === undefined || secret === '') continue\n if (typeof secret !== 'string' || Buffer.byteLength(secret, 'utf8') > MAX_SECRET_BYTES || secret.includes('\\0')) {\n throw new SyncPeerInputError(`${field} is invalid`)\n }\n output[field] = secret\n }\n return Object.keys(output).length === 0 ? undefined : output\n}\n\nfunction payloadFromRecord(value: unknown): SyncPeerSecrets {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return {}\n const payload = (value as { payload?: unknown }).payload\n if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return {}\n const source = payload as Record<string, unknown>\n return {\n ...(typeof source.password === 'string' ? { password: source.password } : {}),\n ...(typeof source.privateKey === 'string' ? { privateKey: source.privateKey } : {}),\n ...(typeof source.passphrase === 'string' ? { passphrase: source.passphrase } : {}),\n }\n}\n\nfunction toView(peer: SyncPeer, secrets: SyncPeerSecrets): SyncPeerView {\n return {\n ...peer,\n hasSecret: {\n hasPassword: Boolean(secrets.password),\n hasKey: Boolean(secrets.privateKey),\n hasPassphrase: Boolean(secrets.passphrase),\n },\n }\n}\n\nfunction validateAuthSecrets(\n authKind: SyncPeer['authKind'],\n supplied: SyncPeerSecrets | undefined,\n effective: SyncPeerSecrets,\n): void {\n if (authKind === 'password') {\n if (supplied?.privateKey !== undefined || supplied?.passphrase !== undefined) {\n throw new SyncPeerInputError('key credentials do not match password authentication')\n }\n if (!effective.password) throw new SyncPeerInputError('password authentication requires a saved password')\n return\n }\n if (supplied?.password !== undefined) throw new SyncPeerInputError('password does not match key authentication')\n if (!effective.privateKey) throw new SyncPeerInputError('key authentication requires a saved private key')\n}\n\nfunction secretsForAuth(authKind: SyncPeer['authKind'], secrets: SyncPeerSecrets): SyncPeerSecrets {\n return authKind === 'password'\n ? (secrets.password ? { password: secrets.password } : {})\n : {\n ...(secrets.privateKey ? { privateKey: secrets.privateKey } : {}),\n ...(secrets.passphrase ? { passphrase: secrets.passphrase } : {}),\n }\n}\n\nfunction asInputError(error: unknown): SyncPeerInputError {\n return error instanceof SyncPeerInputError\n ? error\n : new SyncPeerInputError(error instanceof Error ? error.message : 'invalid sync peer')\n}\n\nexport class SyncPeerManager {\n private readonly listeners = new Set<SyncPeerChangeListener>()\n\n constructor(\n private readonly peers: SyncPeerStore,\n private readonly state: SyncStateStore,\n private readonly credentials: CredentialsLike,\n ) {}\n\n async list(): Promise<SyncPeerView[]> {\n return Promise.all(this.peers.list().map((peer) => this.view(peer)))\n }\n\n subscribe(listener: SyncPeerChangeListener): () => void {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n updateProgress(id: string, patch: SyncPeerProgress): void {\n if (!this.peers.get(id)) throw new SyncPeerNotFoundError(id)\n try {\n this.state.update((state) => {\n state.peers[id] = { ...state.peers[id], ...patch }\n })\n } catch (error) {\n // 进度写失败(校验拒绝、长消息等)绝不能让运行收尾丢失状态:\n // 降级为最小合法补丁(截断错误消息),保证状态机总能离开 running。\n const fallback: SyncPeerProgress = {\n ...patch,\n lastErrorMessage: patch.lastErrorMessage === undefined\n ? undefined\n : ('进度详情省略:' + patch.lastErrorMessage).replace(/[\\u0000-\\u001f\\u007f]/g, ' ').slice(0, 200),\n }\n this.state.update((state) => {\n state.peers[id] = { ...state.peers[id], ...fallback }\n })\n void error\n }\n }\n\n async get(id: string): Promise<SyncPeerView | undefined> {\n const peer = this.peers.get(id)\n return peer ? this.view(peer) : undefined\n }\n\n async connectionData(id: string): Promise<{ peer: SyncPeer; secrets: SyncPeerSecrets }> {\n const peer = this.peers.get(id)\n if (!peer) throw new SyncPeerNotFoundError(id)\n const record = await this.credentials.readRecord(this.credentialKey(id))\n return { peer, secrets: payloadFromRecord(record) }\n }\n\n async pinHostKey(id: string, fingerprint: string): Promise<SyncPeerView> {\n if (!/^SHA256:[A-Za-z0-9+/]{43}$/.test(fingerprint)) {\n throw new SyncPeerInputError('invalid SSH host-key fingerprint')\n }\n const peer = this.peers.update(id, { hostKeyFingerprint: fingerprint })\n return this.view(peer)\n }\n\n async create(draftValue: unknown, secretsValue?: unknown): Promise<SyncPeerView> {\n let draft: SyncPeerDraft\n let secrets: SyncPeerSecrets | undefined\n try {\n draft = parseSyncPeerDraft(draftValue)\n secrets = parseSecrets(secretsValue)\n validateAuthSecrets(draft.authKind, secrets, secrets ?? {})\n } catch (error) {\n throw asInputError(error)\n }\n\n const peer = this.peers.create(draft)\n let attemptedSecrets = false\n try {\n if (secrets) {\n attemptedSecrets = true\n await this.writeSecrets(peer.id, secretsForAuth(peer.authKind, secrets))\n }\n const view = await this.view(peer)\n this.notify(peer.id, peer)\n return view\n } catch (error) {\n this.peers.remove(peer.id)\n if (attemptedSecrets) {\n try {\n await this.credentials.deleteRecord(this.credentialKey(peer.id))\n } catch {}\n }\n throw error\n }\n }\n\n async update(id: string, patchValue: unknown, secretsValue?: unknown): Promise<SyncPeerView> {\n const current = this.peers.get(id)\n if (!current) throw new SyncPeerNotFoundError(id)\n\n let patch: Partial<Omit<SyncPeer, 'id' | 'hostKeyFingerprint'>>\n let secrets: SyncPeerSecrets | undefined\n try {\n const input = asRecord(patchValue, 'sync peer patch')\n for (const key of Object.keys(input)) {\n if (!PATCH_FIELDS.has(key)) throw new SyncPeerInputError(`unsupported peer field: ${key}`)\n }\n const validated = parseSyncPeer({ ...current, ...input, id })\n const { id: _id, hostKeyFingerprint: _fingerprint, ...validatedPatch } = validated\n void _id\n void _fingerprint\n patch = validatedPatch\n secrets = parseSecrets(secretsValue)\n } catch (error) {\n throw asInputError(error)\n }\n\n const credentialKeyValue = this.credentialKey(id)\n const previousRecord = await this.credentials.readRecord(credentialKeyValue)\n const effectiveSecrets = { ...payloadFromRecord(previousRecord), ...secrets }\n const authKind = patch.authKind ?? current.authKind\n validateAuthSecrets(authKind, secrets, effectiveSecrets)\n if (secrets || authKind !== current.authKind) {\n try {\n await this.writeSecrets(id, secretsForAuth(authKind, effectiveSecrets))\n } catch (error) {\n await this.restoreCredentialRecord(credentialKeyValue, previousRecord)\n throw error\n }\n }\n\n let updated: SyncPeer\n try {\n updated = this.peers.update(id, patch)\n } catch (error) {\n await this.restoreCredentialRecord(credentialKeyValue, previousRecord)\n throw error\n }\n this.notify(id, updated)\n return this.view(updated)\n }\n\n async remove(id: string): Promise<boolean> {\n if (!this.peers.get(id)) return false\n this.peers.remove(id)\n this.notify(id, undefined)\n await this.credentials.deleteRecord(this.credentialKey(id))\n this.state.update((state) => {\n delete state.peers[id]\n })\n return true\n }\n\n private async view(peer: SyncPeer): Promise<SyncPeerView> {\n const record = await this.credentials.readRecord(this.credentialKey(peer.id))\n const progress = this.state.read().peers[peer.id]\n return { ...toView(peer, payloadFromRecord(record)), ...(progress ? { progress } : {}) }\n }\n\n private notify(peerId: string, peer: SyncPeer | undefined): void {\n for (const listener of this.listeners) {\n try {\n listener(peerId, peer)\n } catch {}\n }\n }\n\n private async writeSecrets(id: string, secrets: SyncPeerSecrets): Promise<void> {\n const key = this.credentialKey(id)\n await this.credentials.modifyRecord(key, async (_current) => ({\n kind: 'grant',\n payload: secrets,\n }) as CredentialRecord)\n }\n\n private async restoreCredentialRecord(key: CredentialKey, previous: unknown): Promise<void> {\n try {\n if (previous === undefined) await this.credentials.deleteRecord(key)\n else await this.credentials.modifyRecord(key, async () => previous)\n } catch {}\n }\n\n private credentialKey(id: string): CredentialKey {\n try {\n return credentialKey('dshb-core', id)\n } catch {\n throw new Error('credentials service unavailable')\n }\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artenx/dshb",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "description": "Routes DSH filesystem, shell, subprocess, and PTY operations to local, SSH, and Docker execution nodes, with a node registry, an authentication gate, a mobile-aware management UI, and per-workspace custom project instructions.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",