@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.
- package/lib/core/index.js +6 -1
- package/lib/core/index.js.map +1 -1
- package/lib/core/sync/coordinator.d.ts +2 -2
- package/lib/core/sync/coordinator.js +108 -46
- package/lib/core/sync/coordinator.js.map +1 -1
- package/lib/core/sync/internal-routes.d.ts +1 -1
- package/lib/core/sync/internal-routes.js +81 -5
- package/lib/core/sync/internal-routes.js.map +1 -1
- package/lib/core/sync/offline-archive.d.ts +1 -1
- package/lib/core/sync/offline-archive.js +1 -1
- package/lib/core/sync/offline-routes.js +1 -1
- package/lib/core/sync/protocol.d.ts +2 -2
- package/lib/core/sync/protocol.js +5 -2
- package/lib/core/sync/protocol.js.map +1 -1
- package/lib/core/sync/remote-helper.js +107 -76
- package/lib/core/sync/remote-helper.js.map +1 -1
- package/lib/core/sync/routes.d.ts +1 -1
- package/lib/core/sync/session-routes.d.ts +1 -1
- package/lib/core/sync/session-routes.js +1 -1
- package/lib/core/sync/session-sync.d.ts +2 -2
- package/lib/core/sync/session-sync.js +2 -2
- package/lib/core/sync/ssh-transport.d.ts +1 -1
- package/lib/core/sync/ssh-transport.js +104 -6
- package/lib/core/sync/ssh-transport.js.map +1 -1
- package/lib/{protocol-KaWZArY8.d.ts → protocol-DJ0zyVlB.d.ts} +5 -2
- package/lib/{session-sync-C2j2rk9K.d.ts → session-sync-BuOJvFxK.d.ts} +64 -2
- package/lib/{session-sync-C0uoizyB.js → session-sync-CIqjuWkh.js} +548 -74
- package/lib/session-sync-CIqjuWkh.js.map +1 -0
- package/lib/{ssh-transport-uK9Ep0yp.d.ts → ssh-transport-B2oNDs0D.d.ts} +14 -4
- package/package.json +1 -1
- 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)
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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") >
|
|
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") >
|
|
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") >
|
|
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") >
|
|
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") >
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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-
|
|
3387
|
+
//# sourceMappingURL=session-sync-CIqjuWkh.js.map
|