@artenx/dshb 0.1.31 → 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 +23 -6
- 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 +80 -4
- package/lib/core/sync/internal-routes.js.map +1 -1
- package/lib/core/sync/offline-archive.d.ts +1 -1
- package/lib/core/sync/offline-archive.js +1 -1
- package/lib/core/sync/offline-routes.js +1 -1
- package/lib/core/sync/protocol.d.ts +2 -2
- package/lib/core/sync/protocol.js +4 -1
- package/lib/core/sync/protocol.js.map +1 -1
- package/lib/core/sync/remote-helper.js +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 +100 -2
- 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-Zfo1Q-qT.d.ts → session-sync-BuOJvFxK.d.ts} +54 -2
- package/lib/{session-sync-BQt3HXkp.js → session-sync-CIqjuWkh.js} +455 -48
- package/lib/session-sync-CIqjuWkh.js.map +1 -0
- package/lib/{ssh-transport-Cfb_nwkY.d.ts → ssh-transport-B2oNDs0D.d.ts} +14 -4
- package/package.json +1 -1
- package/lib/session-sync-BQt3HXkp.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,128 @@ 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
|
+
};
|
|
499
622
|
/**
|
|
500
623
|
* 导出期附件对象缓存:跨会话去重同一附件(同 id 只读一次存储、只算一次摘要)。
|
|
501
624
|
* 附件对象携带 base64 数据,若整轮导出无上限持有(此前是普通 Map),多图会话的
|
|
@@ -696,16 +819,30 @@ async function writeLocalSpillTransfer(root, input) {
|
|
|
696
819
|
return ref;
|
|
697
820
|
}
|
|
698
821
|
async function saveSpillTransfer(store, input) {
|
|
699
|
-
if (store.saveTextStream)
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
|
707
845
|
});
|
|
708
|
-
if (typeof store.root === "string") return writeLocalSpillTransfer(store.root, input);
|
|
709
846
|
throw new SyncSessionError("spill-transfer-unavailable", "目标实例的 spill provider 不支持安全写入");
|
|
710
847
|
}
|
|
711
848
|
function canonicalJson(value) {
|
|
@@ -1074,6 +1211,52 @@ async function collectBytes(stream, maxBytes = MAX_SYNC_TRANSFER_BYTES) {
|
|
|
1074
1211
|
}
|
|
1075
1212
|
return Buffer.concat(chunks, size);
|
|
1076
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
|
+
}
|
|
1077
1260
|
async function* oneChunk(data) {
|
|
1078
1261
|
yield data;
|
|
1079
1262
|
}
|
|
@@ -1145,7 +1328,9 @@ function workspaceCandidateKey(workspace) {
|
|
|
1145
1328
|
}
|
|
1146
1329
|
function validateObjectPayload(object) {
|
|
1147
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");
|
|
1148
|
-
|
|
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) {
|
|
1149
1334
|
const bytes = Buffer.from(object.data, "base64");
|
|
1150
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");
|
|
1151
1336
|
}
|
|
@@ -1542,7 +1727,7 @@ function instructionsForSession(store, sessionId, stateIndex, preferredWorkspace
|
|
|
1542
1727
|
...inheritedFromOrigin ? { inheritedFromOrigin } : {}
|
|
1543
1728
|
};
|
|
1544
1729
|
}
|
|
1545
|
-
var SyncSessionService = class {
|
|
1730
|
+
var SyncSessionService = class SyncSessionService {
|
|
1546
1731
|
state;
|
|
1547
1732
|
nodes;
|
|
1548
1733
|
registry;
|
|
@@ -1559,6 +1744,8 @@ var SyncSessionService = class {
|
|
|
1559
1744
|
exportCacheStore;
|
|
1560
1745
|
manifestPayloadCache = /* @__PURE__ */ new Map();
|
|
1561
1746
|
manifestPayloadCacheBytes = 0;
|
|
1747
|
+
/** 带外对象收件箱:对象流 RPC 的落盘暂存区,apply 水合后删除。 */
|
|
1748
|
+
objectInbox;
|
|
1562
1749
|
constructor(state, nodes, registry, bindings, getWorkspaces, getPersistence, getAttachments, getSpills, instructions, journal = new SyncSessionJournalStore(), now = Date.now, home) {
|
|
1563
1750
|
this.state = state;
|
|
1564
1751
|
this.nodes = nodes;
|
|
@@ -1572,6 +1759,7 @@ var SyncSessionService = class {
|
|
|
1572
1759
|
this.journal = journal;
|
|
1573
1760
|
this.now = now;
|
|
1574
1761
|
this.exportCacheStore = new SyncSessionExportCacheStore(home);
|
|
1762
|
+
this.objectInbox = new SyncSessionObjectInbox(home);
|
|
1575
1763
|
for (const [key, entry] of this.exportCacheStore.entries()) if (!this.exportCache.has(key)) this.exportCache.set(key, entry);
|
|
1576
1764
|
}
|
|
1577
1765
|
exportBundle() {
|
|
@@ -1616,7 +1804,8 @@ var SyncSessionService = class {
|
|
|
1616
1804
|
bundle = makeSyncSessionBundle(payloadCache.instanceId, [...cachedEntries, ...additional.entries], additional.skipped);
|
|
1617
1805
|
} else bundle = await this.exportBundleNow({
|
|
1618
1806
|
selectedKeys,
|
|
1619
|
-
sharedObjects: options.sharedObjects === true
|
|
1807
|
+
sharedObjects: options.sharedObjects === true,
|
|
1808
|
+
deferredObjects: options.deferredObjects === true
|
|
1620
1809
|
});
|
|
1621
1810
|
const withTails = bundle.entries.map((entry) => withEventTail(entry, prefixesByKey.get(syncSessionEntryKey(entry))));
|
|
1622
1811
|
if (bundle.objects !== void 0) return makeSyncSessionBundle(bundle.instanceId, withTails, bundle.skipped, bundle.objects);
|
|
@@ -1750,6 +1939,7 @@ var SyncSessionService = class {
|
|
|
1750
1939
|
const entries = [];
|
|
1751
1940
|
const skipped = [];
|
|
1752
1941
|
const eligibleLocalIds = /* @__PURE__ */ new Set();
|
|
1942
|
+
const preservedLocalIds = /* @__PURE__ */ new Set();
|
|
1753
1943
|
const workspaceById = /* @__PURE__ */ new Map();
|
|
1754
1944
|
const archivedBranchSessionIds = new Set(Object.values(stateIndex.state.archives).map((archive) => archive.localSessionId));
|
|
1755
1945
|
const objectCache = new ExportObjectCache();
|
|
@@ -1807,13 +1997,14 @@ var SyncSessionService = class {
|
|
|
1807
1997
|
}
|
|
1808
1998
|
let exported;
|
|
1809
1999
|
try {
|
|
1810
|
-
exported = await this.exportSessionEntry(persistence, workspace, descriptor, position, SessionId(sessionId), snapshot, archivedIds.has(sessionId), pinnedIds.has(sessionId), void 0, stateIndex, objectCache);
|
|
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);
|
|
1811
2001
|
} catch (error) {
|
|
1812
2002
|
if (!(error instanceof SyncSessionError && error.code === "session-too-large")) throw error;
|
|
1813
2003
|
skipped.push({
|
|
1814
2004
|
sessionId,
|
|
1815
2005
|
reason: "session-payload-too-large"
|
|
1816
2006
|
});
|
|
2007
|
+
preservedLocalIds.add(sessionId);
|
|
1817
2008
|
continue;
|
|
1818
2009
|
}
|
|
1819
2010
|
if (exported) {
|
|
@@ -1852,6 +2043,7 @@ var SyncSessionService = class {
|
|
|
1852
2043
|
continue;
|
|
1853
2044
|
}
|
|
1854
2045
|
if (eligibleLocalIds.has(metadata.localEntityId)) continue;
|
|
2046
|
+
if (preservedLocalIds.has(metadata.localEntityId)) continue;
|
|
1855
2047
|
const tombstone = {
|
|
1856
2048
|
...metadata,
|
|
1857
2049
|
revision: this.state.nextRevision(this.now()),
|
|
@@ -1881,13 +2073,14 @@ var SyncSessionService = class {
|
|
|
1881
2073
|
if (!snapshot) continue;
|
|
1882
2074
|
let exported;
|
|
1883
2075
|
try {
|
|
1884
|
-
exported = await this.exportSessionEntry(persistence, workspace, descriptor, archive.workspacePosition, SessionId(archive.localSessionId), snapshot, true, false, archive, stateIndex, objectCache);
|
|
2076
|
+
exported = await this.exportSessionEntry(persistence, workspace, descriptor, archive.workspacePosition, SessionId(archive.localSessionId), snapshot, true, false, archive, stateIndex, objectCache, options.deferredObjects === true);
|
|
1885
2077
|
} catch (error) {
|
|
1886
2078
|
if (!(error instanceof SyncSessionError && error.code === "session-too-large")) throw error;
|
|
1887
2079
|
skipped.push({
|
|
1888
2080
|
sessionId: archive.localSessionId,
|
|
1889
2081
|
reason: "session-payload-too-large"
|
|
1890
2082
|
});
|
|
2083
|
+
preservedLocalIds.add(archive.localSessionId);
|
|
1891
2084
|
continue;
|
|
1892
2085
|
}
|
|
1893
2086
|
if (exported) {
|
|
@@ -1948,6 +2141,7 @@ var SyncSessionService = class {
|
|
|
1948
2141
|
const batchId = pendingBatch?.batchId ?? this.journal.beginWithBundle(peerId, safeSourceName, bundle.digest, recordKeys, bundle, new Date(this.now()).toISOString(), resolvedImportedVia, allowSameInstance);
|
|
1949
2142
|
const result = emptyApplyResult();
|
|
1950
2143
|
const attachmentImportCache = /* @__PURE__ */ new Map();
|
|
2144
|
+
const inboxConsumed = /* @__PURE__ */ new Set();
|
|
1951
2145
|
result.skipped += bundle.skipped.length;
|
|
1952
2146
|
result.unmapped += bundle.skipped.length;
|
|
1953
2147
|
const workspaceOrders = /* @__PURE__ */ new Map();
|
|
@@ -2053,7 +2247,7 @@ var SyncSessionService = class {
|
|
|
2053
2247
|
let prepared;
|
|
2054
2248
|
if (preparedValue === void 0) {
|
|
2055
2249
|
try {
|
|
2056
|
-
prepared = await this.prepareEntry(entry, safeSourceName, plannedSessionIds, entryWorkspace, resolvedImportedVia, attachmentImportCache);
|
|
2250
|
+
prepared = await this.prepareEntry(entry, safeSourceName, plannedSessionIds, entryWorkspace, resolvedImportedVia, attachmentImportCache, inboxConsumed);
|
|
2057
2251
|
} catch (error) {
|
|
2058
2252
|
if (error instanceof SyncSessionError && error.code === "workspace-unmapped") {
|
|
2059
2253
|
result.skipped += 1;
|
|
@@ -2079,6 +2273,7 @@ var SyncSessionService = class {
|
|
|
2079
2273
|
await this.orderWorkspaceSessions(workspaceOrders);
|
|
2080
2274
|
this.journal.complete(batchId);
|
|
2081
2275
|
this.exportCacheStore.flush();
|
|
2276
|
+
if (inboxConsumed.size > 0) await this.deleteInboxedObjects(inboxConsumed);
|
|
2082
2277
|
return result;
|
|
2083
2278
|
} catch (error) {
|
|
2084
2279
|
if (error instanceof SyncSessionError) throw error;
|
|
@@ -2113,7 +2308,7 @@ var SyncSessionService = class {
|
|
|
2113
2308
|
}
|
|
2114
2309
|
return validateSyncSessionBundle(makeSyncSessionBundle(bundle.instanceId, entries, bundle.skipped));
|
|
2115
2310
|
}
|
|
2116
|
-
async prepareEntry(entry, sourceName, plannedSessionIds, workspace, importedVia, attachmentImportCache) {
|
|
2311
|
+
async prepareEntry(entry, sourceName, plannedSessionIds, workspace, importedVia, attachmentImportCache, inboxConsumed) {
|
|
2117
2312
|
if (!entry.header || !entry.events || !entry.workspace || !entry.objects) throw new SyncSessionError("manifest-invalid", "active session entry is incomplete");
|
|
2118
2313
|
const sessionId = entry.branchId ? archivedSessionId(entry.branchId) : importedSessionId(entry.metadata);
|
|
2119
2314
|
const sourceHeader = entry.header;
|
|
@@ -2138,7 +2333,7 @@ var SyncSessionService = class {
|
|
|
2138
2333
|
await handle.close();
|
|
2139
2334
|
}
|
|
2140
2335
|
}
|
|
2141
|
-
const objectMap = await this.importObjects(entry.objects, sessionId, attachmentImportCache);
|
|
2336
|
+
const objectMap = await this.importObjects(entry.objects, sessionId, attachmentImportCache, inboxConsumed);
|
|
2142
2337
|
const validatedEvents = validateStoredEvents(header, this.rewriteEvents(entry.events, entry.objects, objectMap, sessionId, String(entry.header.id)));
|
|
2143
2338
|
assertContiguous(sessionId, validatedEvents, 0);
|
|
2144
2339
|
const instructionWorkspaceId = entry.instructionSnapshot?.workspaceOrigin ? this.localWorkspaceIdForOrigin(entry.instructionSnapshot.workspaceOrigin, entry.instructionSnapshot.workspaceRemotePath) : void 0;
|
|
@@ -2224,7 +2419,7 @@ var SyncSessionService = class {
|
|
|
2224
2419
|
if (!committedWorkspace) throw new SyncSessionError("workspace-unmapped", "目标实例没有对应的可用远程节点");
|
|
2225
2420
|
return committedWorkspace;
|
|
2226
2421
|
}
|
|
2227
|
-
async importObjects(objects, sessionId, attachmentImportCache) {
|
|
2422
|
+
async importObjects(objects, sessionId, attachmentImportCache, inboxConsumed) {
|
|
2228
2423
|
const attachments = objects.some((object) => object.kind === "image" || object.kind === "file") ? this.getAttachments() : void 0;
|
|
2229
2424
|
const spills = objects.some((object) => object.kind === "spill" && !object.unavailable) ? this.getSpills() : void 0;
|
|
2230
2425
|
if (objects.some((object) => object.kind === "image" || object.kind === "file") && !attachments) throw new SyncSessionError("attachments-unavailable", "目标实例的附件存储服务不可用");
|
|
@@ -2239,6 +2434,54 @@ var SyncSessionService = class {
|
|
|
2239
2434
|
continue;
|
|
2240
2435
|
}
|
|
2241
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
|
+
}
|
|
2242
2485
|
const data = arrayBuffer(object.data);
|
|
2243
2486
|
if (data.byteLength !== object.bytes || sha256(data) !== object.digest) throw new SyncSessionError("object-integrity", "session object failed digest validation");
|
|
2244
2487
|
if (object.kind === "image") {
|
|
@@ -2688,7 +2931,7 @@ var SyncSessionService = class {
|
|
|
2688
2931
|
contentDigest: digest
|
|
2689
2932
|
};
|
|
2690
2933
|
}
|
|
2691
|
-
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) {
|
|
2692
2935
|
const { header, inheritedEventCount, events, revision: sourceRevision } = await this.readStableSession(persistence, sessionId, firstSnapshot);
|
|
2693
2936
|
const localSessionId = String(sessionId);
|
|
2694
2937
|
const cacheKey = sessionExportCacheKey(localSessionId);
|
|
@@ -2709,7 +2952,7 @@ var SyncSessionService = class {
|
|
|
2709
2952
|
originEntityId: descriptor.originWorkspaceId
|
|
2710
2953
|
});
|
|
2711
2954
|
const eventDigest = sessionEventListDigest(events);
|
|
2712
|
-
const objects = await this.exportObjects(events, localSessionId, objectCache);
|
|
2955
|
+
const objects = await this.exportObjects(events, localSessionId, objectCache, deferredObjects);
|
|
2713
2956
|
const digest = sessionEventDigest(header, inheritedEventCount, events, descriptor, syncInstructions, objects, parentOrigin);
|
|
2714
2957
|
const current = archive ? void 0 : index.recordsByLocalId.get(localSessionId) ?? state.records[syncRecordKey("session", {
|
|
2715
2958
|
originInstanceId: this.state.instanceId(),
|
|
@@ -2772,6 +3015,28 @@ var SyncSessionService = class {
|
|
|
2772
3015
|
});
|
|
2773
3016
|
indexSessionRecord(index, metadata);
|
|
2774
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
|
+
}
|
|
2775
3040
|
if (!archive && !objects.some((object) => object.kind === "spill")) {
|
|
2776
3041
|
const fingerprint = sessionExportFingerprint({
|
|
2777
3042
|
revision: sourceRevision,
|
|
@@ -2800,23 +3065,7 @@ var SyncSessionService = class {
|
|
|
2800
3065
|
this.exportCacheStore.delete(cacheKey);
|
|
2801
3066
|
}
|
|
2802
3067
|
return {
|
|
2803
|
-
entry:
|
|
2804
|
-
metadata,
|
|
2805
|
-
...outputBranchId ? { branchId: outputBranchId } : {},
|
|
2806
|
-
sourceLocalSessionId: localSessionId,
|
|
2807
|
-
header,
|
|
2808
|
-
inheritedEventCount,
|
|
2809
|
-
...parentOrigin ? { parentOrigin } : {},
|
|
2810
|
-
workspace: descriptor,
|
|
2811
|
-
workspacePosition,
|
|
2812
|
-
archived,
|
|
2813
|
-
pinned,
|
|
2814
|
-
...syncInstructions ? { instructionSnapshot: syncInstructions } : {},
|
|
2815
|
-
events: [...events],
|
|
2816
|
-
objects,
|
|
2817
|
-
eventCount: events.length,
|
|
2818
|
-
eventDigest
|
|
2819
|
-
},
|
|
3068
|
+
entry: wireEntry,
|
|
2820
3069
|
eventCount: events.length,
|
|
2821
3070
|
eventDigest,
|
|
2822
3071
|
inheritedEventCount
|
|
@@ -2855,7 +3104,7 @@ var SyncSessionService = class {
|
|
|
2855
3104
|
}
|
|
2856
3105
|
throw new SyncSessionError("session-changing", "会话在导出期间持续变化,请稍后重试");
|
|
2857
3106
|
}
|
|
2858
|
-
async exportObjects(events, sessionId, objectCache) {
|
|
3107
|
+
async exportObjects(events, sessionId, objectCache, deferredObjects = false) {
|
|
2859
3108
|
const attachments = attachmentReferences(events);
|
|
2860
3109
|
const spills = spillReferences(events);
|
|
2861
3110
|
const structuredSpills = new Set(spills.map(spillKey));
|
|
@@ -2863,6 +3112,11 @@ var SyncSessionService = class {
|
|
|
2863
3112
|
const payloads = /* @__PURE__ */ new Map();
|
|
2864
3113
|
const attachmentStore = attachments.images.length + attachments.files.length > 0 ? this.getAttachments() : void 0;
|
|
2865
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
|
+
});
|
|
2866
3120
|
for (const ref of attachments.images) {
|
|
2867
3121
|
const key = attachmentKey("image", ref);
|
|
2868
3122
|
const cached = objectCache?.get(key)?.object;
|
|
@@ -2872,7 +3126,7 @@ var SyncSessionService = class {
|
|
|
2872
3126
|
}
|
|
2873
3127
|
const stored = await attachmentStore.readImage(ref);
|
|
2874
3128
|
const digest = sha256(stored.data);
|
|
2875
|
-
|
|
3129
|
+
let object = {
|
|
2876
3130
|
id: key,
|
|
2877
3131
|
kind: "image",
|
|
2878
3132
|
ref: stored.ref,
|
|
@@ -2880,6 +3134,7 @@ var SyncSessionService = class {
|
|
|
2880
3134
|
digest,
|
|
2881
3135
|
data: Buffer.from(stored.data).toString("base64")
|
|
2882
3136
|
};
|
|
3137
|
+
if (deferredObjects && object.bytes > 262144) object = deferredDescriptor(object);
|
|
2883
3138
|
payloads.set(key, object);
|
|
2884
3139
|
objectCache?.set(key, { object });
|
|
2885
3140
|
}
|
|
@@ -2891,11 +3146,28 @@ var SyncSessionService = class {
|
|
|
2891
3146
|
continue;
|
|
2892
3147
|
}
|
|
2893
3148
|
let data;
|
|
3149
|
+
const inlineCap = deferredObjects ? MAX_SESSION_OBJECT_INLINE_BYTES : MAX_SYNC_TRANSFER_BYTES;
|
|
2894
3150
|
try {
|
|
2895
|
-
data = await collectBytes(attachmentStore.readFileStream(ref));
|
|
3151
|
+
data = await collectBytes(attachmentStore.readFileStream(ref), inlineCap);
|
|
2896
3152
|
} catch (error) {
|
|
2897
|
-
if (error instanceof SyncSessionError && error.code === "bundle-too-large") throw
|
|
2898
|
-
|
|
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;
|
|
2899
3171
|
}
|
|
2900
3172
|
const object = {
|
|
2901
3173
|
id: key,
|
|
@@ -2923,7 +3195,37 @@ var SyncSessionService = class {
|
|
|
2923
3195
|
continue;
|
|
2924
3196
|
}
|
|
2925
3197
|
try {
|
|
2926
|
-
|
|
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
|
+
}
|
|
2927
3229
|
if (reference.ref.bytes !== void 0 && data.byteLength !== reference.ref.bytes) {
|
|
2928
3230
|
untransferred.push(reference);
|
|
2929
3231
|
continue;
|
|
@@ -2968,6 +3270,111 @@ var SyncSessionService = class {
|
|
|
2968
3270
|
if (!persistence) throw new SyncSessionError("session-persistence-unavailable", "会话持久化服务不可用");
|
|
2969
3271
|
return persistence;
|
|
2970
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
|
+
}
|
|
2971
3378
|
serializeMutation(operation) {
|
|
2972
3379
|
const result = this.operationTail.then(operation);
|
|
2973
3380
|
this.operationTail = result.then(() => void 0, () => void 0);
|
|
@@ -2975,6 +3382,6 @@ var SyncSessionService = class {
|
|
|
2975
3382
|
}
|
|
2976
3383
|
};
|
|
2977
3384
|
//#endregion
|
|
2978
|
-
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 };
|
|
2979
3386
|
|
|
2980
|
-
//# sourceMappingURL=session-sync-
|
|
3387
|
+
//# sourceMappingURL=session-sync-CIqjuWkh.js.map
|