@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
@@ -2,14 +2,14 @@ import { compareSyncRevision } from "./core/sync/revision.js";
2
2
  import { isNonNegativeSafeInteger, isRecord as isRecord$1, syncRecordKey, syncWorkspaceKey } from "./core/sync/model.js";
3
3
  import "./core/workspace-instructions.js";
4
4
  import { i as deepFreeze, r as brandString } from "./lib-gdtU_WKT.js";
5
+ import { MAX_SESSION_OBJECT_INLINE_BYTES, MAX_SESSION_OBJECT_STREAM_BYTES, MAX_SYNC_TRANSFER_BYTES } from "./core/sync/protocol.js";
5
6
  import { SyncSessionJournalStore } from "./core/sync/session-journal.js";
6
- import { MAX_SYNC_TRANSFER_BYTES } from "./core/sync/protocol.js";
7
7
  import { homedir, tmpdir } from "node:os";
8
- import { chmodSync, closeSync, constants, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
8
+ import { chmodSync, closeSync, constants, createReadStream, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
9
9
  import { basename, dirname, join, normalize, posix, resolve } from "node:path";
10
10
  import { createHash, randomBytes } from "node:crypto";
11
11
  import "@deepseek-ai/cordis";
12
- import { lstat, mkdir, open, realpath, unlink } from "node:fs/promises";
12
+ import { access, lstat, mkdir, open, readdir, realpath, rename, stat, unlink } from "node:fs/promises";
13
13
  //#region ../../node_modules/@deepseek-ai/dsh-session/lib/index.js
14
14
  /**
15
15
  * Brand a string as a {@link SessionId}.
@@ -488,6 +488,7 @@ var SyncSessionExportCacheStore = class {
488
488
  //#region src/core/sync/session-sync.ts
489
489
  const SESSION_READ_PAGE_SIZE = 256;
490
490
  const MAX_SYNC_SESSIONS = 1e4;
491
+ const MAX_SESSION_EXPORT_ENTRY_BYTES = 100663296;
491
492
  const MAX_MANIFEST_PAYLOAD_CACHE_BYTES = 4194304;
492
493
  const MAX_MANIFEST_PAYLOAD_CACHE_COUNT = 8;
493
494
  const SESSION_EXPORT_CACHE_VERSION = 1;
@@ -496,6 +497,156 @@ const SPILL_ARTIFACT_META_KEY = "@deepseek-ai/dsh-spill";
496
497
  const LEGACY_LOCAL_SPILL_HINT = "Use read with offset/limit, or grep this path to search within it.";
497
498
  const LEGACY_LOCAL_SPILL_MARKER = "Full formatted result stored at: ";
498
499
  const LEGACY_LOCAL_SPILL_SUFFIX = `. ${LEGACY_LOCAL_SPILL_HINT})`;
500
+ var SyncSessionObjectInbox = class {
501
+ root;
502
+ constructor(home) {
503
+ this.root = join(home ?? process.env.DSH_HOME ?? homedir(), "dshb", "sync-objects");
504
+ }
505
+ get path() {
506
+ return this.root;
507
+ }
508
+ fileFor(digest) {
509
+ const match = /^sha256:([a-f0-9]{64})$/.exec(digest);
510
+ if (!match) return void 0;
511
+ return join(this.root, match[1]);
512
+ }
513
+ /**
514
+ * 打开一个收件箱写入器:逐帧流式写盘 + 增量摘要校验,内存 O(帧)。
515
+ * commit() 校验字节数与 digest 后原子重命名;abort() 清理临时文件。
516
+ */
517
+ async writerFor(digest, expectedBytes) {
518
+ const file = this.fileFor(digest);
519
+ if (!file) throw new SyncSessionError("object-invalid", "session object digest is invalid");
520
+ await mkdir(this.root, {
521
+ recursive: true,
522
+ mode: 448
523
+ });
524
+ const temporary = `${file}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
525
+ const hash = createHash("sha256");
526
+ const handle = await open(temporary, "wx", 384);
527
+ let written = 0;
528
+ let closed = false;
529
+ const closeHandle = async () => {
530
+ if (closed) return;
531
+ closed = true;
532
+ await handle.close().catch(() => {});
533
+ };
534
+ return {
535
+ async write(chunk) {
536
+ if (closed) throw new SyncSessionError("object-invalid", "session object writer is closed");
537
+ written += chunk.byteLength;
538
+ if (written > expectedBytes) throw new SyncSessionError("object-integrity", "session object stream exceeds declared bytes");
539
+ hash.update(chunk);
540
+ let offset = 0;
541
+ while (offset < chunk.byteLength) {
542
+ const { bytesWritten } = await handle.write(chunk, offset, chunk.byteLength - offset, null);
543
+ if (bytesWritten <= 0) throw new SyncSessionError("object-integrity", "session object stream write made no progress");
544
+ offset += bytesWritten;
545
+ }
546
+ },
547
+ async commit() {
548
+ if (written !== expectedBytes || `sha256:${hash.digest("hex")}` !== digest) {
549
+ await closeHandle();
550
+ try {
551
+ await unlink(temporary);
552
+ } catch {}
553
+ throw new SyncSessionError("object-integrity", "session object stream digest mismatch");
554
+ }
555
+ await handle.sync();
556
+ await closeHandle();
557
+ await rename(temporary, file);
558
+ },
559
+ async abort() {
560
+ await closeHandle();
561
+ try {
562
+ await unlink(temporary);
563
+ } catch {}
564
+ }
565
+ };
566
+ }
567
+ /** 便捷写入:把对象流整段写进收件箱(测试与小对象用)。 */
568
+ async write(digest, bytes, stream) {
569
+ const writer = await this.writerFor(digest, bytes);
570
+ try {
571
+ for await (const chunk of stream) {
572
+ if (!(chunk instanceof Uint8Array) || chunk.byteLength === 0) throw new SyncSessionError("object-invalid", "session object stream contains an invalid chunk");
573
+ await writer.write(chunk);
574
+ }
575
+ await writer.commit();
576
+ } catch (error) {
577
+ await writer.abort();
578
+ throw error;
579
+ }
580
+ }
581
+ /** 打开收件箱文件的只读流(apply 水合用);调用方负责消费完毕。 */
582
+ async read(digest) {
583
+ const file = this.fileFor(digest);
584
+ if (!file) throw new SyncSessionError("object-missing", "session object is not in the inbox");
585
+ try {
586
+ await access(file);
587
+ } catch {
588
+ throw new SyncSessionError("object-missing", "session object is not in the inbox");
589
+ }
590
+ return createReadStream(file);
591
+ }
592
+ async delete(digest) {
593
+ const file = this.fileFor(digest);
594
+ if (!file) return;
595
+ try {
596
+ await unlink(file);
597
+ } catch {}
598
+ }
599
+ /** TTL 清扫:删除超过 maxAgeMs 未修改的孤儿文件。 */
600
+ async sweep(maxAgeMs = 864e5) {
601
+ let entries;
602
+ try {
603
+ entries = await readdir(this.root, { withFileTypes: true });
604
+ } catch {
605
+ return 0;
606
+ }
607
+ const cutoff = Date.now() - maxAgeMs;
608
+ let removed = 0;
609
+ for (const entry of entries) {
610
+ if (!entry.isFile() || !/^[a-f0-9]{64}(\.|$)/.test(entry.name)) continue;
611
+ const file = join(this.root, entry.name);
612
+ try {
613
+ if ((await stat(file)).mtimeMs < cutoff) {
614
+ await unlink(file);
615
+ removed += 1;
616
+ }
617
+ } catch {}
618
+ }
619
+ return removed;
620
+ }
621
+ };
622
+ /**
623
+ * 导出期附件对象缓存:跨会话去重同一附件(同 id 只读一次存储、只算一次摘要)。
624
+ * 附件对象携带 base64 数据,若整轮导出无上限持有(此前是普通 Map),多图会话的
625
+ * 首次同步会把堆吃满(桌面端 OOM 崩溃的直接推手之一)。这里按原始字节数做
626
+ * LRU 上限,超限淘汰最早条目;被淘汰的附件在后续会话中按 cache miss 重新读取。
627
+ */
628
+ const MAX_EXPORT_OBJECT_CACHE_BYTES = 67108864;
629
+ var ExportObjectCache = class {
630
+ entries = /* @__PURE__ */ new Map();
631
+ bytes = 0;
632
+ get(key) {
633
+ return this.entries.get(key);
634
+ }
635
+ set(key, entry) {
636
+ const existing = this.entries.get(key);
637
+ if (existing) this.bytes -= existing.object.bytes;
638
+ this.entries.delete(key);
639
+ this.entries.set(key, entry);
640
+ this.bytes += entry.object.bytes;
641
+ while (this.bytes > MAX_EXPORT_OBJECT_CACHE_BYTES && this.entries.size > 1) {
642
+ const oldest = this.entries.keys().next().value;
643
+ if (oldest === void 0) break;
644
+ const removed = this.entries.get(oldest);
645
+ this.entries.delete(oldest);
646
+ if (removed) this.bytes -= removed.object.bytes;
647
+ }
648
+ }
649
+ };
499
650
  var SyncSessionError = class extends Error {
500
651
  code;
501
652
  constructor(code, message, cause) {
@@ -668,16 +819,30 @@ async function writeLocalSpillTransfer(root, input) {
668
819
  return ref;
669
820
  }
670
821
  async function saveSpillTransfer(store, input) {
671
- if (store.saveTextStream) return store.saveTextStream({
672
- owner: input.owner,
673
- source: input.source,
674
- suggestedName: input.suggestedName,
675
- transferId: input.transferId,
676
- bytes: input.bytes,
677
- digest: input.digest,
678
- content: oneChunk(input.data)
822
+ if (store.saveTextStream) {
823
+ if (input.content !== void 0) return store.saveTextStream({
824
+ owner: input.owner,
825
+ source: input.source,
826
+ suggestedName: input.suggestedName,
827
+ transferId: input.transferId,
828
+ bytes: input.bytes,
829
+ digest: input.digest,
830
+ content: input.content
831
+ });
832
+ return store.saveTextStream({
833
+ owner: input.owner,
834
+ source: input.source,
835
+ suggestedName: input.suggestedName,
836
+ transferId: input.transferId,
837
+ bytes: input.bytes,
838
+ digest: input.digest,
839
+ content: oneChunk(input.data)
840
+ });
841
+ }
842
+ if (typeof store.root === "string" && input.content === void 0 && input.data !== void 0) return writeLocalSpillTransfer(store.root, {
843
+ ...input,
844
+ data: input.data
679
845
  });
680
- if (typeof store.root === "string") return writeLocalSpillTransfer(store.root, input);
681
846
  throw new SyncSessionError("spill-transfer-unavailable", "目标实例的 spill provider 不支持安全写入");
682
847
  }
683
848
  function canonicalJson(value) {
@@ -865,30 +1030,43 @@ function sessionEventDigest(header, inheritedEventCount, events, workspace, inst
865
1030
  return output;
866
1031
  };
867
1032
  const { id: _id, cwd: _cwd, parentSession: _parent, ...stableHeader } = header;
868
- return sha256(canonicalJson({
869
- header: stableHeader,
870
- inheritedEventCount,
871
- events: events.map(normalize),
872
- workspace: {
873
- originInstanceId: workspace.originInstanceId,
874
- originWorkspaceId: workspace.originWorkspaceId,
875
- nodeOrigin: workspace.nodeOrigin,
876
- remotePath: workspace.remotePath
877
- },
878
- parentOrigin,
879
- instructionSnapshot: instructionSnapshot ? {
880
- text: instructionSnapshot.text,
881
- capturedAt: instructionSnapshot.capturedAt,
882
- workspaceOrigin: instructionSnapshot.workspaceOrigin,
883
- workspaceRemotePath: instructionSnapshot.workspaceRemotePath,
884
- workspaceRevision: instructionSnapshot.workspaceRevision,
885
- digest: instructionSnapshot.digest,
886
- inheritedFromOrigin: instructionSnapshot.inheritedFromOrigin
887
- } : void 0
888
- }));
1033
+ const hash = createHash("sha256");
1034
+ hash.update("{\"events\":[");
1035
+ for (let index = 0; index < events.length; index += 1) {
1036
+ if (index > 0) hash.update(",");
1037
+ hash.update(canonicalJson(normalize(events[index])));
1038
+ }
1039
+ hash.update("]");
1040
+ hash.update(`,"header":${canonicalJson(stableHeader)}`);
1041
+ hash.update(`,"inheritedEventCount":${canonicalJson(inheritedEventCount)}`);
1042
+ hash.update(`,"instructionSnapshot":${instructionSnapshot ? canonicalJson({
1043
+ text: instructionSnapshot.text,
1044
+ capturedAt: instructionSnapshot.capturedAt,
1045
+ workspaceOrigin: instructionSnapshot.workspaceOrigin,
1046
+ workspaceRemotePath: instructionSnapshot.workspaceRemotePath,
1047
+ workspaceRevision: instructionSnapshot.workspaceRevision,
1048
+ digest: instructionSnapshot.digest,
1049
+ inheritedFromOrigin: instructionSnapshot.inheritedFromOrigin
1050
+ }) : "undefined"}`);
1051
+ hash.update(`,"parentOrigin":${parentOrigin ? canonicalJson(parentOrigin) : "undefined"}`);
1052
+ hash.update(`,"workspace":${canonicalJson({
1053
+ originInstanceId: workspace.originInstanceId,
1054
+ originWorkspaceId: workspace.originWorkspaceId,
1055
+ nodeOrigin: workspace.nodeOrigin,
1056
+ remotePath: workspace.remotePath
1057
+ })}`);
1058
+ hash.update("}");
1059
+ return `sha256:${hash.digest("hex")}`;
889
1060
  }
890
1061
  function sessionEventListDigest(events) {
891
- return sha256(canonicalJson(events));
1062
+ const hash = createHash("sha256");
1063
+ hash.update("[");
1064
+ for (let index = 0; index < events.length; index += 1) {
1065
+ if (index > 0) hash.update(",");
1066
+ hash.update(canonicalJson(events[index]));
1067
+ }
1068
+ hash.update("]");
1069
+ return `sha256:${hash.digest("hex")}`;
892
1070
  }
893
1071
  function sessionStateDigest(workspace, workspacePosition, archived, pinned) {
894
1072
  return sha256(canonicalJson({
@@ -1033,6 +1211,52 @@ async function collectBytes(stream, maxBytes = MAX_SYNC_TRANSFER_BYTES) {
1033
1211
  }
1034
1212
  return Buffer.concat(chunks, size);
1035
1213
  }
1214
+ /** 流式计算对象摘要:内存 O(单块),供超过内联阈值的带外对象测长 + 摘要。 */
1215
+ async function hashStream(stream, maxBytes) {
1216
+ const hash = createHash("sha256");
1217
+ let total = 0;
1218
+ for await (const chunk of stream) {
1219
+ if (!(chunk instanceof Uint8Array)) throw new SyncSessionError("object-invalid", "session object stream contains a non-byte chunk");
1220
+ total += chunk.byteLength;
1221
+ if (total > maxBytes) throw new SyncSessionError("bundle-too-large", "session object exceeds the transfer limit");
1222
+ hash.update(chunk);
1223
+ }
1224
+ return {
1225
+ bytes: total,
1226
+ digest: `sha256:${hash.digest("hex")}`
1227
+ };
1228
+ }
1229
+ /**
1230
+ * 带外水合的内容流:边消费边校验字节数与摘要,全部消费完才放行
1231
+ * (摘要不符在流末尾抛出,内存 O(单块))。
1232
+ */
1233
+ async function* verifiedObjectStream(stream, expectedBytes, expectedDigest) {
1234
+ const hash = createHash("sha256");
1235
+ let total = 0;
1236
+ for await (const chunk of stream) {
1237
+ if (!(chunk instanceof Uint8Array)) throw new SyncSessionError("object-invalid", "session object stream contains a non-byte chunk");
1238
+ total += chunk.byteLength;
1239
+ if (total > expectedBytes) throw new SyncSessionError("object-integrity", "session object content exceeds declared bytes");
1240
+ hash.update(chunk);
1241
+ yield chunk;
1242
+ }
1243
+ if (total !== expectedBytes || `sha256:${hash.digest("hex")}` !== expectedDigest) throw new SyncSessionError("object-integrity", "session object content failed digest validation");
1244
+ }
1245
+ /** 校验带外对象帧头:{"id":"...","bytes":N,"digest":"sha256:..."} */
1246
+ function parseObjectStreamHeader(line) {
1247
+ let parsed;
1248
+ try {
1249
+ parsed = JSON.parse(line.toString("utf8"));
1250
+ } catch {
1251
+ throw new SyncSessionError("object-invalid", "object stream header is not valid JSON");
1252
+ }
1253
+ if (!isRecord$1(parsed) || typeof parsed.id !== "string" || !parsed.id || !isNonNegativeSafeInteger(parsed.bytes) || parsed.bytes > 68719476736 || typeof parsed.digest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(parsed.digest)) throw new SyncSessionError("object-invalid", "object stream header is invalid");
1254
+ return {
1255
+ id: parsed.id,
1256
+ bytes: parsed.bytes,
1257
+ digest: parsed.digest
1258
+ };
1259
+ }
1036
1260
  async function* oneChunk(data) {
1037
1261
  yield data;
1038
1262
  }
@@ -1104,7 +1328,9 @@ function workspaceCandidateKey(workspace) {
1104
1328
  }
1105
1329
  function validateObjectPayload(object) {
1106
1330
  if (!isRecord$1(object) || typeof object.id !== "string" || !object.id || typeof object.digest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(object.digest) || !isNonNegativeSafeInteger(object.bytes) || typeof object.data !== "string") throw new SyncSessionError("object-invalid", "session object metadata is invalid");
1107
- if (!(object.kind === "spill" && object.unavailable === true && object.bytes === 0 && object.data === "")) {
1331
+ const isDeferred = object.deferred === true && object.data === "";
1332
+ const isUnavailableSpill = object.kind === "spill" && object.unavailable === true && object.bytes === 0 && object.data === "";
1333
+ if (!isDeferred && !isUnavailableSpill) {
1108
1334
  const bytes = Buffer.from(object.data, "base64");
1109
1335
  if (bytes.toString("base64") !== object.data || bytes.byteLength !== object.bytes || sha256(bytes) !== object.digest) throw new SyncSessionError("object-invalid", "session object encoding or digest is invalid");
1110
1336
  }
@@ -1292,7 +1518,7 @@ function validateSyncSessionBundle(value) {
1292
1518
  digest: value.digest
1293
1519
  };
1294
1520
  if (bundleDigest(bundle) !== bundle.digest) throw new SyncSessionError("manifest-digest", "session bundle digest does not match");
1295
- if (Buffer.byteLength(JSON.stringify(bundle), "utf8") > 107374182400) throw new SyncSessionError("bundle-too-large", "session bundle exceeds the transfer limit");
1521
+ if (Buffer.byteLength(JSON.stringify(bundle), "utf8") > 134217728) throw new SyncSessionError("bundle-too-large", "session bundle exceeds the transfer limit");
1296
1522
  return bundle;
1297
1523
  }
1298
1524
  function makeSyncSessionBundle(instanceId, entries, skipped = [], sharedObjects) {
@@ -1314,7 +1540,7 @@ function makeSyncSessionBundle(instanceId, entries, skipped = [], sharedObjects)
1314
1540
  digest: ""
1315
1541
  };
1316
1542
  bundle.digest = bundleDigest(bundle);
1317
- if (Buffer.byteLength(JSON.stringify(bundle), "utf8") > 107374182400) throw new SyncSessionError("bundle-too-large", "session bundle exceeds the transfer limit");
1543
+ if (Buffer.byteLength(JSON.stringify(bundle), "utf8") > 134217728) throw new SyncSessionError("bundle-too-large", "session bundle exceeds the transfer limit");
1318
1544
  return bundle;
1319
1545
  }
1320
1546
  const bundle = {
@@ -1325,7 +1551,7 @@ function makeSyncSessionBundle(instanceId, entries, skipped = [], sharedObjects)
1325
1551
  digest: ""
1326
1552
  };
1327
1553
  bundle.digest = bundleDigest(bundle);
1328
- if (Buffer.byteLength(JSON.stringify(bundle), "utf8") > 107374182400) throw new SyncSessionError("bundle-too-large", "session bundle exceeds the transfer limit");
1554
+ if (Buffer.byteLength(JSON.stringify(bundle), "utf8") > 134217728) throw new SyncSessionError("bundle-too-large", "session bundle exceeds the transfer limit");
1329
1555
  return bundle;
1330
1556
  }
1331
1557
  function syncSessionEntryKey(entry) {
@@ -1380,7 +1606,7 @@ function makeSyncSessionManifest(instanceId, entries, skipped = []) {
1380
1606
  digest: ""
1381
1607
  };
1382
1608
  manifest.digest = sessionManifestDigest(manifest);
1383
- if (Buffer.byteLength(JSON.stringify(manifest), "utf8") > 107374182400) throw new SyncSessionError("bundle-too-large", "session manifest exceeds the transfer limit");
1609
+ if (Buffer.byteLength(JSON.stringify(manifest), "utf8") > 134217728) throw new SyncSessionError("bundle-too-large", "session manifest exceeds the transfer limit");
1384
1610
  return manifest;
1385
1611
  }
1386
1612
  function validateSyncSessionManifest(value) {
@@ -1399,7 +1625,7 @@ function validateSyncSessionManifest(value) {
1399
1625
  digest: value.digest
1400
1626
  };
1401
1627
  if (sessionManifestDigest(manifest) !== manifest.digest) throw new SyncSessionError("manifest-digest", "session manifest digest does not match");
1402
- if (Buffer.byteLength(JSON.stringify(manifest), "utf8") > 107374182400) throw new SyncSessionError("bundle-too-large", "session manifest exceeds the transfer limit");
1628
+ if (Buffer.byteLength(JSON.stringify(manifest), "utf8") > 134217728) throw new SyncSessionError("bundle-too-large", "session manifest exceeds the transfer limit");
1403
1629
  return manifest;
1404
1630
  }
1405
1631
  function emptyApplyResult() {
@@ -1501,7 +1727,7 @@ function instructionsForSession(store, sessionId, stateIndex, preferredWorkspace
1501
1727
  ...inheritedFromOrigin ? { inheritedFromOrigin } : {}
1502
1728
  };
1503
1729
  }
1504
- var SyncSessionService = class {
1730
+ var SyncSessionService = class SyncSessionService {
1505
1731
  state;
1506
1732
  nodes;
1507
1733
  registry;
@@ -1518,6 +1744,8 @@ var SyncSessionService = class {
1518
1744
  exportCacheStore;
1519
1745
  manifestPayloadCache = /* @__PURE__ */ new Map();
1520
1746
  manifestPayloadCacheBytes = 0;
1747
+ /** 带外对象收件箱:对象流 RPC 的落盘暂存区,apply 水合后删除。 */
1748
+ objectInbox;
1521
1749
  constructor(state, nodes, registry, bindings, getWorkspaces, getPersistence, getAttachments, getSpills, instructions, journal = new SyncSessionJournalStore(), now = Date.now, home) {
1522
1750
  this.state = state;
1523
1751
  this.nodes = nodes;
@@ -1531,6 +1759,7 @@ var SyncSessionService = class {
1531
1759
  this.journal = journal;
1532
1760
  this.now = now;
1533
1761
  this.exportCacheStore = new SyncSessionExportCacheStore(home);
1762
+ this.objectInbox = new SyncSessionObjectInbox(home);
1534
1763
  for (const [key, entry] of this.exportCacheStore.entries()) if (!this.exportCache.has(key)) this.exportCache.set(key, entry);
1535
1764
  }
1536
1765
  exportBundle() {
@@ -1575,7 +1804,8 @@ var SyncSessionService = class {
1575
1804
  bundle = makeSyncSessionBundle(payloadCache.instanceId, [...cachedEntries, ...additional.entries], additional.skipped);
1576
1805
  } else bundle = await this.exportBundleNow({
1577
1806
  selectedKeys,
1578
- sharedObjects: options.sharedObjects === true
1807
+ sharedObjects: options.sharedObjects === true,
1808
+ deferredObjects: options.deferredObjects === true
1579
1809
  });
1580
1810
  const withTails = bundle.entries.map((entry) => withEventTail(entry, prefixesByKey.get(syncSessionEntryKey(entry))));
1581
1811
  if (bundle.objects !== void 0) return makeSyncSessionBundle(bundle.instanceId, withTails, bundle.skipped, bundle.objects);
@@ -1709,9 +1939,10 @@ var SyncSessionService = class {
1709
1939
  const entries = [];
1710
1940
  const skipped = [];
1711
1941
  const eligibleLocalIds = /* @__PURE__ */ new Set();
1942
+ const preservedLocalIds = /* @__PURE__ */ new Set();
1712
1943
  const workspaceById = /* @__PURE__ */ new Map();
1713
1944
  const archivedBranchSessionIds = new Set(Object.values(stateIndex.state.archives).map((archive) => archive.localSessionId));
1714
- const objectCache = /* @__PURE__ */ new Map();
1945
+ const objectCache = new ExportObjectCache();
1715
1946
  const descriptorCache = /* @__PURE__ */ new Map();
1716
1947
  const getWorkspaceDescriptor = (workspace, binding, nodeOrigin, preferredOrigin) => {
1717
1948
  const key = canonicalJson({
@@ -1764,7 +1995,18 @@ var SyncSessionService = class {
1764
1995
  continue;
1765
1996
  }
1766
1997
  }
1767
- const exported = await this.exportSessionEntry(persistence, workspace, descriptor, position, SessionId(sessionId), snapshot, archivedIds.has(sessionId), pinnedIds.has(sessionId), void 0, stateIndex, objectCache);
1998
+ let exported;
1999
+ try {
2000
+ exported = await this.exportSessionEntry(persistence, workspace, descriptor, position, SessionId(sessionId), snapshot, archivedIds.has(sessionId), pinnedIds.has(sessionId), void 0, stateIndex, objectCache, options.deferredObjects === true);
2001
+ } catch (error) {
2002
+ if (!(error instanceof SyncSessionError && error.code === "session-too-large")) throw error;
2003
+ skipped.push({
2004
+ sessionId,
2005
+ reason: "session-payload-too-large"
2006
+ });
2007
+ preservedLocalIds.add(sessionId);
2008
+ continue;
2009
+ }
1768
2010
  if (exported) {
1769
2011
  const { entry } = exported;
1770
2012
  if (manifestOnly && options.payloadCache) {
@@ -1801,6 +2043,7 @@ var SyncSessionService = class {
1801
2043
  continue;
1802
2044
  }
1803
2045
  if (eligibleLocalIds.has(metadata.localEntityId)) continue;
2046
+ if (preservedLocalIds.has(metadata.localEntityId)) continue;
1804
2047
  const tombstone = {
1805
2048
  ...metadata,
1806
2049
  revision: this.state.nextRevision(this.now()),
@@ -1828,7 +2071,18 @@ var SyncSessionService = class {
1828
2071
  const descriptor = getWorkspaceDescriptor(workspace, binding, nodeOrigin, stateIndex.archivesByLocalSessionId.get(archive.localSessionId)?.workspaceOrigin);
1829
2072
  const snapshot = await persistence.stat(SessionId(archive.localSessionId));
1830
2073
  if (!snapshot) continue;
1831
- const exported = await this.exportSessionEntry(persistence, workspace, descriptor, archive.workspacePosition, SessionId(archive.localSessionId), snapshot, true, false, archive, stateIndex, objectCache);
2074
+ let exported;
2075
+ try {
2076
+ exported = await this.exportSessionEntry(persistence, workspace, descriptor, archive.workspacePosition, SessionId(archive.localSessionId), snapshot, true, false, archive, stateIndex, objectCache, options.deferredObjects === true);
2077
+ } catch (error) {
2078
+ if (!(error instanceof SyncSessionError && error.code === "session-too-large")) throw error;
2079
+ skipped.push({
2080
+ sessionId: archive.localSessionId,
2081
+ reason: "session-payload-too-large"
2082
+ });
2083
+ preservedLocalIds.add(archive.localSessionId);
2084
+ continue;
2085
+ }
1832
2086
  if (exported) {
1833
2087
  const { entry } = exported;
1834
2088
  if (manifestOnly && options.payloadCache) {
@@ -1887,6 +2141,7 @@ var SyncSessionService = class {
1887
2141
  const batchId = pendingBatch?.batchId ?? this.journal.beginWithBundle(peerId, safeSourceName, bundle.digest, recordKeys, bundle, new Date(this.now()).toISOString(), resolvedImportedVia, allowSameInstance);
1888
2142
  const result = emptyApplyResult();
1889
2143
  const attachmentImportCache = /* @__PURE__ */ new Map();
2144
+ const inboxConsumed = /* @__PURE__ */ new Set();
1890
2145
  result.skipped += bundle.skipped.length;
1891
2146
  result.unmapped += bundle.skipped.length;
1892
2147
  const workspaceOrders = /* @__PURE__ */ new Map();
@@ -1992,7 +2247,7 @@ var SyncSessionService = class {
1992
2247
  let prepared;
1993
2248
  if (preparedValue === void 0) {
1994
2249
  try {
1995
- prepared = await this.prepareEntry(entry, safeSourceName, plannedSessionIds, entryWorkspace, resolvedImportedVia, attachmentImportCache);
2250
+ prepared = await this.prepareEntry(entry, safeSourceName, plannedSessionIds, entryWorkspace, resolvedImportedVia, attachmentImportCache, inboxConsumed);
1996
2251
  } catch (error) {
1997
2252
  if (error instanceof SyncSessionError && error.code === "workspace-unmapped") {
1998
2253
  result.skipped += 1;
@@ -2018,6 +2273,7 @@ var SyncSessionService = class {
2018
2273
  await this.orderWorkspaceSessions(workspaceOrders);
2019
2274
  this.journal.complete(batchId);
2020
2275
  this.exportCacheStore.flush();
2276
+ if (inboxConsumed.size > 0) await this.deleteInboxedObjects(inboxConsumed);
2021
2277
  return result;
2022
2278
  } catch (error) {
2023
2279
  if (error instanceof SyncSessionError) throw error;
@@ -2052,7 +2308,7 @@ var SyncSessionService = class {
2052
2308
  }
2053
2309
  return validateSyncSessionBundle(makeSyncSessionBundle(bundle.instanceId, entries, bundle.skipped));
2054
2310
  }
2055
- async prepareEntry(entry, sourceName, plannedSessionIds, workspace, importedVia, attachmentImportCache) {
2311
+ async prepareEntry(entry, sourceName, plannedSessionIds, workspace, importedVia, attachmentImportCache, inboxConsumed) {
2056
2312
  if (!entry.header || !entry.events || !entry.workspace || !entry.objects) throw new SyncSessionError("manifest-invalid", "active session entry is incomplete");
2057
2313
  const sessionId = entry.branchId ? archivedSessionId(entry.branchId) : importedSessionId(entry.metadata);
2058
2314
  const sourceHeader = entry.header;
@@ -2077,7 +2333,7 @@ var SyncSessionService = class {
2077
2333
  await handle.close();
2078
2334
  }
2079
2335
  }
2080
- const objectMap = await this.importObjects(entry.objects, sessionId, attachmentImportCache);
2336
+ const objectMap = await this.importObjects(entry.objects, sessionId, attachmentImportCache, inboxConsumed);
2081
2337
  const validatedEvents = validateStoredEvents(header, this.rewriteEvents(entry.events, entry.objects, objectMap, sessionId, String(entry.header.id)));
2082
2338
  assertContiguous(sessionId, validatedEvents, 0);
2083
2339
  const instructionWorkspaceId = entry.instructionSnapshot?.workspaceOrigin ? this.localWorkspaceIdForOrigin(entry.instructionSnapshot.workspaceOrigin, entry.instructionSnapshot.workspaceRemotePath) : void 0;
@@ -2163,7 +2419,7 @@ var SyncSessionService = class {
2163
2419
  if (!committedWorkspace) throw new SyncSessionError("workspace-unmapped", "目标实例没有对应的可用远程节点");
2164
2420
  return committedWorkspace;
2165
2421
  }
2166
- async importObjects(objects, sessionId, attachmentImportCache) {
2422
+ async importObjects(objects, sessionId, attachmentImportCache, inboxConsumed) {
2167
2423
  const attachments = objects.some((object) => object.kind === "image" || object.kind === "file") ? this.getAttachments() : void 0;
2168
2424
  const spills = objects.some((object) => object.kind === "spill" && !object.unavailable) ? this.getSpills() : void 0;
2169
2425
  if (objects.some((object) => object.kind === "image" || object.kind === "file") && !attachments) throw new SyncSessionError("attachments-unavailable", "目标实例的附件存储服务不可用");
@@ -2178,6 +2434,54 @@ var SyncSessionService = class {
2178
2434
  continue;
2179
2435
  }
2180
2436
  }
2437
+ if (object.deferred === true) {
2438
+ inboxConsumed?.add(object.digest);
2439
+ if (object.kind === "image") {
2440
+ const data = await collectBytes(await this.objectInbox.read(object.digest), MAX_SESSION_OBJECT_INLINE_BYTES * 256);
2441
+ if (data.byteLength !== object.bytes || sha256(data) !== object.digest) throw new SyncSessionError("object-integrity", "session object failed digest validation");
2442
+ const ref = await attachments.saveImage({
2443
+ data,
2444
+ mediaType: object.ref.mediaType,
2445
+ ...object.ref.name === void 0 ? {} : { name: object.ref.name }
2446
+ });
2447
+ output.set(object.id, ref);
2448
+ attachmentImportCache?.set(object.id, ref);
2449
+ } else if (object.kind === "file") {
2450
+ const stream = await this.objectInbox.read(object.digest);
2451
+ const ref = await attachments.saveFileStream({
2452
+ data: verifiedObjectStream(stream, object.bytes, object.digest),
2453
+ name: object.ref.name
2454
+ });
2455
+ output.set(object.id, ref);
2456
+ attachmentImportCache?.set(object.id, ref);
2457
+ } else if (spills.saveTextStream) {
2458
+ const stream = await this.objectInbox.read(object.digest);
2459
+ const ref = await saveSpillTransfer(spills, {
2460
+ owner: { sessionId },
2461
+ source: object.reference.source,
2462
+ suggestedName: object.reference.suggestedName,
2463
+ transferId: object.id,
2464
+ bytes: object.bytes,
2465
+ digest: object.digest,
2466
+ content: verifiedObjectStream(stream, object.bytes, object.digest)
2467
+ });
2468
+ output.set(object.id, ref);
2469
+ } else {
2470
+ const data = await collectBytes(await this.objectInbox.read(object.digest), MAX_SESSION_OBJECT_INLINE_BYTES * 1024);
2471
+ if (data.byteLength !== object.bytes || sha256(data) !== object.digest) throw new SyncSessionError("object-integrity", "session object failed digest validation");
2472
+ const ref = await saveSpillTransfer(spills, {
2473
+ owner: { sessionId },
2474
+ source: object.reference.source,
2475
+ suggestedName: object.reference.suggestedName,
2476
+ transferId: object.id,
2477
+ bytes: object.bytes,
2478
+ digest: object.digest,
2479
+ data
2480
+ });
2481
+ output.set(object.id, ref);
2482
+ }
2483
+ continue;
2484
+ }
2181
2485
  const data = arrayBuffer(object.data);
2182
2486
  if (data.byteLength !== object.bytes || sha256(data) !== object.digest) throw new SyncSessionError("object-integrity", "session object failed digest validation");
2183
2487
  if (object.kind === "image") {
@@ -2627,7 +2931,7 @@ var SyncSessionService = class {
2627
2931
  contentDigest: digest
2628
2932
  };
2629
2933
  }
2630
- async exportSessionEntry(persistence, workspace, descriptor, workspacePosition, sessionId, firstSnapshot, archived, pinned, archive, stateIndex, objectCache) {
2934
+ async exportSessionEntry(persistence, workspace, descriptor, workspacePosition, sessionId, firstSnapshot, archived, pinned, archive, stateIndex, objectCache, deferredObjects = false) {
2631
2935
  const { header, inheritedEventCount, events, revision: sourceRevision } = await this.readStableSession(persistence, sessionId, firstSnapshot);
2632
2936
  const localSessionId = String(sessionId);
2633
2937
  const cacheKey = sessionExportCacheKey(localSessionId);
@@ -2648,7 +2952,7 @@ var SyncSessionService = class {
2648
2952
  originEntityId: descriptor.originWorkspaceId
2649
2953
  });
2650
2954
  const eventDigest = sessionEventListDigest(events);
2651
- const objects = await this.exportObjects(events, localSessionId, objectCache);
2955
+ const objects = await this.exportObjects(events, localSessionId, objectCache, deferredObjects);
2652
2956
  const digest = sessionEventDigest(header, inheritedEventCount, events, descriptor, syncInstructions, objects, parentOrigin);
2653
2957
  const current = archive ? void 0 : index.recordsByLocalId.get(localSessionId) ?? state.records[syncRecordKey("session", {
2654
2958
  originInstanceId: this.state.instanceId(),
@@ -2711,6 +3015,28 @@ var SyncSessionService = class {
2711
3015
  });
2712
3016
  indexSessionRecord(index, metadata);
2713
3017
  }
3018
+ const wireEntry = {
3019
+ metadata,
3020
+ ...outputBranchId ? { branchId: outputBranchId } : {},
3021
+ sourceLocalSessionId: localSessionId,
3022
+ header,
3023
+ inheritedEventCount,
3024
+ ...parentOrigin ? { parentOrigin } : {},
3025
+ workspace: descriptor,
3026
+ workspacePosition,
3027
+ archived,
3028
+ pinned,
3029
+ ...syncInstructions ? { instructionSnapshot: syncInstructions } : {},
3030
+ events: [...events],
3031
+ objects,
3032
+ eventCount: events.length,
3033
+ eventDigest
3034
+ };
3035
+ if (Buffer.byteLength(JSON.stringify(wireEntry), "utf8") > MAX_SESSION_EXPORT_ENTRY_BYTES) {
3036
+ this.exportCache.delete(cacheKey);
3037
+ this.exportCacheStore.delete(cacheKey);
3038
+ throw new SyncSessionError("session-too-large", "会话内容超过单会话传输上限");
3039
+ }
2714
3040
  if (!archive && !objects.some((object) => object.kind === "spill")) {
2715
3041
  const fingerprint = sessionExportFingerprint({
2716
3042
  revision: sourceRevision,
@@ -2739,23 +3065,7 @@ var SyncSessionService = class {
2739
3065
  this.exportCacheStore.delete(cacheKey);
2740
3066
  }
2741
3067
  return {
2742
- entry: {
2743
- metadata,
2744
- ...outputBranchId ? { branchId: outputBranchId } : {},
2745
- sourceLocalSessionId: localSessionId,
2746
- header,
2747
- inheritedEventCount,
2748
- ...parentOrigin ? { parentOrigin } : {},
2749
- workspace: descriptor,
2750
- workspacePosition,
2751
- archived,
2752
- pinned,
2753
- ...syncInstructions ? { instructionSnapshot: syncInstructions } : {},
2754
- events: [...events],
2755
- objects,
2756
- eventCount: events.length,
2757
- eventDigest
2758
- },
3068
+ entry: wireEntry,
2759
3069
  eventCount: events.length,
2760
3070
  eventDigest,
2761
3071
  inheritedEventCount
@@ -2794,7 +3104,7 @@ var SyncSessionService = class {
2794
3104
  }
2795
3105
  throw new SyncSessionError("session-changing", "会话在导出期间持续变化,请稍后重试");
2796
3106
  }
2797
- async exportObjects(events, sessionId, objectCache) {
3107
+ async exportObjects(events, sessionId, objectCache, deferredObjects = false) {
2798
3108
  const attachments = attachmentReferences(events);
2799
3109
  const spills = spillReferences(events);
2800
3110
  const structuredSpills = new Set(spills.map(spillKey));
@@ -2802,6 +3112,11 @@ var SyncSessionService = class {
2802
3112
  const payloads = /* @__PURE__ */ new Map();
2803
3113
  const attachmentStore = attachments.images.length + attachments.files.length > 0 ? this.getAttachments() : void 0;
2804
3114
  if (attachments.images.length + attachments.files.length > 0 && !attachmentStore) throw new SyncSessionError("attachments-unavailable", "附件存储服务不可用");
3115
+ const deferredDescriptor = (object) => ({
3116
+ ...object,
3117
+ data: "",
3118
+ deferred: true
3119
+ });
2805
3120
  for (const ref of attachments.images) {
2806
3121
  const key = attachmentKey("image", ref);
2807
3122
  const cached = objectCache?.get(key)?.object;
@@ -2811,7 +3126,7 @@ var SyncSessionService = class {
2811
3126
  }
2812
3127
  const stored = await attachmentStore.readImage(ref);
2813
3128
  const digest = sha256(stored.data);
2814
- const object = {
3129
+ let object = {
2815
3130
  id: key,
2816
3131
  kind: "image",
2817
3132
  ref: stored.ref,
@@ -2819,6 +3134,7 @@ var SyncSessionService = class {
2819
3134
  digest,
2820
3135
  data: Buffer.from(stored.data).toString("base64")
2821
3136
  };
3137
+ if (deferredObjects && object.bytes > 262144) object = deferredDescriptor(object);
2822
3138
  payloads.set(key, object);
2823
3139
  objectCache?.set(key, { object });
2824
3140
  }
@@ -2829,7 +3145,30 @@ var SyncSessionService = class {
2829
3145
  payloads.set(key, cached);
2830
3146
  continue;
2831
3147
  }
2832
- const data = await collectBytes(attachmentStore.readFileStream(ref));
3148
+ let data;
3149
+ const inlineCap = deferredObjects ? MAX_SESSION_OBJECT_INLINE_BYTES : MAX_SYNC_TRANSFER_BYTES;
3150
+ try {
3151
+ data = await collectBytes(attachmentStore.readFileStream(ref), inlineCap);
3152
+ } catch (error) {
3153
+ if (!(error instanceof SyncSessionError && error.code === "bundle-too-large")) throw error;
3154
+ data = void 0;
3155
+ }
3156
+ if (data === void 0) {
3157
+ if (!deferredObjects) throw new SyncSessionError("session-too-large", "会话文件附件超过传输大小限制");
3158
+ const measured = await hashStream(attachmentStore.readFileStream(ref), MAX_SESSION_OBJECT_STREAM_BYTES);
3159
+ const object = {
3160
+ id: key,
3161
+ kind: "file",
3162
+ ref,
3163
+ bytes: measured.bytes,
3164
+ digest: measured.digest,
3165
+ data: "",
3166
+ deferred: true
3167
+ };
3168
+ payloads.set(key, object);
3169
+ objectCache?.set(key, { object });
3170
+ continue;
3171
+ }
2833
3172
  const object = {
2834
3173
  id: key,
2835
3174
  kind: "file",
@@ -2856,7 +3195,37 @@ var SyncSessionService = class {
2856
3195
  continue;
2857
3196
  }
2858
3197
  try {
2859
- const data = await collectBytes(readSpillStream(store, reference));
3198
+ let data;
3199
+ const inlineCap = deferredObjects ? MAX_SESSION_OBJECT_INLINE_BYTES : MAX_SYNC_TRANSFER_BYTES;
3200
+ try {
3201
+ data = await collectBytes(readSpillStream(store, reference), inlineCap);
3202
+ } catch (error) {
3203
+ if (!(error instanceof SyncSessionError && error.code === "bundle-too-large")) throw error;
3204
+ data = void 0;
3205
+ }
3206
+ if (data === void 0) {
3207
+ if (!deferredObjects) {
3208
+ untransferred.push(reference);
3209
+ continue;
3210
+ }
3211
+ const measured = await hashStream(readSpillStream(store, reference), MAX_SESSION_OBJECT_STREAM_BYTES);
3212
+ if (reference.ref.bytes !== void 0 && measured.bytes !== reference.ref.bytes) {
3213
+ untransferred.push(reference);
3214
+ continue;
3215
+ }
3216
+ const object = {
3217
+ id: key,
3218
+ kind: "spill",
3219
+ reference,
3220
+ bytes: measured.bytes,
3221
+ digest: measured.digest,
3222
+ data: "",
3223
+ deferred: true
3224
+ };
3225
+ payloads.set(key, object);
3226
+ objectCache?.set(key, { object });
3227
+ continue;
3228
+ }
2860
3229
  if (reference.ref.bytes !== void 0 && data.byteLength !== reference.ref.bytes) {
2861
3230
  untransferred.push(reference);
2862
3231
  continue;
@@ -2901,6 +3270,111 @@ var SyncSessionService = class {
2901
3270
  if (!persistence) throw new SyncSessionError("session-persistence-unavailable", "会话持久化服务不可用");
2902
3271
  return persistence;
2903
3272
  }
3273
+ /**
3274
+ * 把 deferred 对象编码为带外帧流:每对象一行 JSON 头 + 原始字节。
3275
+ * 内容从本地存储流式读取(spill/文件流式;图片体积有限走缓冲),
3276
+ * 逐块校验字节数与摘要,内存 O(单块)。
3277
+ */
3278
+ async *streamObjects(objects) {
3279
+ for (const object of objects) {
3280
+ yield Buffer.from(`${JSON.stringify({
3281
+ id: object.id,
3282
+ bytes: object.bytes,
3283
+ digest: object.digest
3284
+ })}\n`, "utf8");
3285
+ if (object.deferred !== true) {
3286
+ const data = arrayBuffer(object.data);
3287
+ if (data.byteLength !== object.bytes || sha256(data) !== object.digest) throw new SyncSessionError("object-integrity", "session object failed digest validation");
3288
+ yield data;
3289
+ continue;
3290
+ }
3291
+ const hash = createHash("sha256");
3292
+ let verified = 0;
3293
+ const source = await this.openObjectContent(object);
3294
+ for await (const chunk of source) {
3295
+ if (!(chunk instanceof Uint8Array)) throw new SyncSessionError("object-invalid", "session object stream contains a non-byte chunk");
3296
+ verified += chunk.byteLength;
3297
+ if (verified > object.bytes) throw new SyncSessionError("object-integrity", "session object content exceeds declared bytes");
3298
+ hash.update(chunk);
3299
+ yield chunk;
3300
+ }
3301
+ if (verified !== object.bytes || `sha256:${hash.digest("hex")}` !== object.digest) throw new SyncSessionError("object-integrity", "session object content failed digest validation");
3302
+ }
3303
+ }
3304
+ async openObjectContent(object) {
3305
+ if (object.kind === "spill") {
3306
+ const store = this.getSpills();
3307
+ if (!store) throw new SyncSessionError("spill-transfer-unavailable", "目标实例没有 owner-scoped spill transfer API");
3308
+ return readSpillStream(store, object.reference);
3309
+ }
3310
+ const attachments = this.getAttachments();
3311
+ if (!attachments) throw new SyncSessionError("attachments-unavailable", "附件存储服务不可用");
3312
+ if (object.kind === "file") return attachments.readFileStream(object.ref);
3313
+ return oneChunk((await attachments.readImage(object.ref)).data);
3314
+ }
3315
+ /** 单帧写入上限:超过即认为流失控。 */
3316
+ static OBJECT_STREAM_HEADER_MAX_BYTES = 65536;
3317
+ /**
3318
+ * 解析并落盘对端推送的对象帧流:逐帧写收件箱,边写边校验摘要,
3319
+ * 内存 O(单帧)。返回接收的对象数。
3320
+ */
3321
+ async ingestObjectStream(source) {
3322
+ let buffer = Buffer.alloc(0);
3323
+ const reader = source[Symbol.asyncIterator]();
3324
+ const nextChunk = async () => {
3325
+ const { value, done } = await reader.next();
3326
+ return done ? void 0 : Buffer.from(value);
3327
+ };
3328
+ let count = 0;
3329
+ for (;;) {
3330
+ let newline = buffer.indexOf(10);
3331
+ while (newline < 0) {
3332
+ const chunk = await nextChunk();
3333
+ if (chunk === void 0) {
3334
+ if (buffer.length === 0) return count;
3335
+ throw new SyncSessionError("object-invalid", "object stream ended inside a header");
3336
+ }
3337
+ buffer = Buffer.concat([buffer, chunk]);
3338
+ if (buffer.length > SyncSessionService.OBJECT_STREAM_HEADER_MAX_BYTES) throw new SyncSessionError("object-invalid", "object stream header exceeds size limit");
3339
+ newline = buffer.indexOf(10);
3340
+ }
3341
+ const header = parseObjectStreamHeader(buffer.subarray(0, newline));
3342
+ buffer = buffer.subarray(newline + 1);
3343
+ const writer = await this.objectInbox.writerFor(header.digest, header.bytes);
3344
+ try {
3345
+ const writePart = async (part) => {
3346
+ if (part.byteLength > 0) await writer.write(part);
3347
+ };
3348
+ let remaining = header.bytes;
3349
+ for (;;) {
3350
+ if (buffer.length > 0) {
3351
+ const take = Math.min(buffer.length, remaining);
3352
+ await writePart(buffer.subarray(0, take));
3353
+ remaining -= take;
3354
+ buffer = buffer.subarray(take);
3355
+ if (remaining === 0) break;
3356
+ continue;
3357
+ }
3358
+ const chunk = await nextChunk();
3359
+ if (chunk === void 0) throw new SyncSessionError("object-invalid", "object stream ended inside a payload");
3360
+ const take = Math.min(chunk.length, remaining);
3361
+ await writePart(chunk.subarray(0, take));
3362
+ remaining -= take;
3363
+ if (take < chunk.length) buffer = Buffer.concat([buffer, chunk.subarray(take)]);
3364
+ if (remaining === 0) break;
3365
+ }
3366
+ await writer.commit();
3367
+ } catch (error) {
3368
+ await writer.abort();
3369
+ throw error;
3370
+ }
3371
+ count += 1;
3372
+ }
3373
+ }
3374
+ /** apply 成功后删除已水合对象的收件箱文件;失败保留供 journal 回放。 */
3375
+ async deleteInboxedObjects(digests) {
3376
+ for (const digest of digests) await this.objectInbox.delete(digest);
3377
+ }
2904
3378
  serializeMutation(operation) {
2905
3379
  const result = this.operationTail.then(operation);
2906
3380
  this.operationTail = result.then(() => void 0, () => void 0);
@@ -2908,6 +3382,6 @@ var SyncSessionService = class {
2908
3382
  }
2909
3383
  };
2910
3384
  //#endregion
2911
- export { planSessionTransfers as a, validateSyncSessionBundle as c, makeSyncSessionManifest as i, validateSyncSessionManifest as l, SyncSessionService as n, sessionEventListDigest as o, makeSyncSessionBundle as r, syncSessionEntryKey as s, SyncSessionError as t };
3385
+ export { estimateJsonBytes as a, planSessionTransfers as c, validateObjectPayload as d, validateSyncSessionBundle as f, SyncSessionService as i, sessionEventListDigest as l, SyncSessionError as n, makeSyncSessionBundle as o, validateSyncSessionManifest as p, SyncSessionObjectInbox as r, makeSyncSessionManifest as s, ExportObjectCache as t, syncSessionEntryKey as u };
2912
3386
 
2913
- //# sourceMappingURL=session-sync-C0uoizyB.js.map
3387
+ //# sourceMappingURL=session-sync-CIqjuWkh.js.map