@artenx/dshb 0.1.21 → 0.1.23
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/README.md +1 -1
- package/lib/auth/index.js +22 -11
- package/lib/auth/index.js.map +1 -1
- package/lib/client.js +1 -1
- package/lib/client.js.map +1 -1
- package/lib/core/index.js +2 -2
- package/lib/core/index.js.map +1 -1
- package/lib/core/sync/coordinator.d.ts +2 -2
- package/lib/core/sync/coordinator.js +121 -15
- 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 +58 -9
- 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 +10 -4
- 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 +2 -2
- package/lib/core/sync/ssh-transport.js +97 -104
- package/lib/core/sync/ssh-transport.js.map +1 -1
- package/lib/{protocol-CV7LGE44.d.ts → protocol-KaWZArY8.d.ts} +5 -2
- package/lib/{session-sync-EdfwUON5.js → session-sync-C0uoizyB.js} +834 -98
- package/lib/session-sync-C0uoizyB.js.map +1 -0
- package/lib/{session-sync-g6qKZzjV.d.ts → session-sync-C2j2rk9K.d.ts} +56 -4
- package/lib/{ssh-transport-BcIiLxsX.d.ts → ssh-transport-DGkQh-HA.d.ts} +14 -5
- package/package.json +1 -1
- package/lib/session-sync-EdfwUON5.js.map +0 -1
|
@@ -4,10 +4,10 @@ import "./core/workspace-instructions.js";
|
|
|
4
4
|
import { i as deepFreeze, r as brandString } from "./lib-gdtU_WKT.js";
|
|
5
5
|
import { SyncSessionJournalStore } from "./core/sync/session-journal.js";
|
|
6
6
|
import { MAX_SYNC_TRANSFER_BYTES } from "./core/sync/protocol.js";
|
|
7
|
-
import { tmpdir } from "node:os";
|
|
8
|
-
import { constants, mkdirSync } from "node:fs";
|
|
7
|
+
import { homedir, tmpdir } from "node:os";
|
|
8
|
+
import { chmodSync, closeSync, constants, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { basename, dirname, join, normalize, posix, resolve } from "node:path";
|
|
10
|
-
import { createHash } from "node:crypto";
|
|
10
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
11
11
|
import "@deepseek-ai/cordis";
|
|
12
12
|
import { lstat, mkdir, open, realpath, unlink } from "node:fs/promises";
|
|
13
13
|
//#region ../../node_modules/@deepseek-ai/dsh-session/lib/index.js
|
|
@@ -392,9 +392,105 @@ function assertContiguous(id, events, cursor) {
|
|
|
392
392
|
for (const [index, event] of events.entries()) if (event.seq !== cursor + index) throw new Error(`append seq mismatch for "${id}": expected ${cursor + index} at index ${index}, got ${event.seq}`);
|
|
393
393
|
}
|
|
394
394
|
//#endregion
|
|
395
|
+
//#region src/core/sync/export-cache.ts
|
|
396
|
+
const MAX_EXPORT_CACHE_FILE_BYTES = 8388608;
|
|
397
|
+
function cacheHome(home) {
|
|
398
|
+
return home ?? process.env.DSH_HOME ?? join(homedir(), ".dsh");
|
|
399
|
+
}
|
|
400
|
+
function atomicWrite(file, content) {
|
|
401
|
+
const dir = dirname(file);
|
|
402
|
+
mkdirSync(dir, {
|
|
403
|
+
recursive: true,
|
|
404
|
+
mode: 448
|
|
405
|
+
});
|
|
406
|
+
const temporary = `${file}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
|
|
407
|
+
let fd;
|
|
408
|
+
try {
|
|
409
|
+
fd = openSync(temporary, "wx", 384);
|
|
410
|
+
writeFileSync(fd, content, "utf8");
|
|
411
|
+
fsyncSync(fd);
|
|
412
|
+
closeSync(fd);
|
|
413
|
+
fd = void 0;
|
|
414
|
+
renameSync(temporary, file);
|
|
415
|
+
chmodSync(file, 384);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
if (fd !== void 0) closeSync(fd);
|
|
418
|
+
try {
|
|
419
|
+
unlinkSync(temporary);
|
|
420
|
+
} catch {}
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function validRevision(value) {
|
|
425
|
+
return isRecord$1(value) && isNonNegativeSafeInteger(value.physicalTimeMs) && isNonNegativeSafeInteger(value.logicalCounter) && typeof value.writerInstanceId === "string" && !!value.writerInstanceId;
|
|
426
|
+
}
|
|
427
|
+
function validEntry(value) {
|
|
428
|
+
if (!isRecord$1(value) || typeof value.fingerprint !== "string" || !/^sha256:[a-f0-9]{64}$/.test(value.fingerprint) || !isRecord$1(value.metadata) || typeof value.metadata.contentDigest !== "string" || !validRevision(value.metadata.revision) || !isNonNegativeSafeInteger(value.eventCount) || typeof value.eventDigest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(value.eventDigest) || !isNonNegativeSafeInteger(value.inheritedEventCount)) return false;
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
var SyncSessionExportCacheStore = class {
|
|
432
|
+
filePath;
|
|
433
|
+
data;
|
|
434
|
+
dirty = false;
|
|
435
|
+
constructor(home) {
|
|
436
|
+
this.filePath = join(cacheHome(home), "dshb", "sync-export-cache.json");
|
|
437
|
+
}
|
|
438
|
+
get(key) {
|
|
439
|
+
return this.load().entries[key];
|
|
440
|
+
}
|
|
441
|
+
set(key, entry) {
|
|
442
|
+
if (!validEntry(entry)) return;
|
|
443
|
+
this.load().entries[key] = entry;
|
|
444
|
+
this.dirty = true;
|
|
445
|
+
}
|
|
446
|
+
delete(key) {
|
|
447
|
+
if (!(key in this.load().entries)) return;
|
|
448
|
+
delete this.load().entries[key];
|
|
449
|
+
this.dirty = true;
|
|
450
|
+
}
|
|
451
|
+
flush() {
|
|
452
|
+
if (!this.dirty || !this.data) return;
|
|
453
|
+
this.dirty = false;
|
|
454
|
+
try {
|
|
455
|
+
atomicWrite(this.filePath, `${JSON.stringify(this.data)}\n`);
|
|
456
|
+
} catch {
|
|
457
|
+
this.dirty = true;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
entries() {
|
|
461
|
+
return Object.entries(this.load().entries);
|
|
462
|
+
}
|
|
463
|
+
load() {
|
|
464
|
+
if (this.data) return this.data;
|
|
465
|
+
let restored = {
|
|
466
|
+
version: 1,
|
|
467
|
+
entries: {}
|
|
468
|
+
};
|
|
469
|
+
try {
|
|
470
|
+
const raw = readFileSync(this.filePath, "utf8");
|
|
471
|
+
if (raw.length <= MAX_EXPORT_CACHE_FILE_BYTES) {
|
|
472
|
+
const parsed = JSON.parse(raw);
|
|
473
|
+
if (isRecord$1(parsed) && parsed.version === 1 && isRecord$1(parsed.entries)) {
|
|
474
|
+
const entries = {};
|
|
475
|
+
for (const [key, entry] of Object.entries(parsed.entries)) if (/^\[".+?",(null|".*?")\]$/.test(key) && validEntry(entry)) entries[key] = entry;
|
|
476
|
+
restored = {
|
|
477
|
+
version: 1,
|
|
478
|
+
entries
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
} catch {}
|
|
483
|
+
this.data = restored;
|
|
484
|
+
return restored;
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
//#endregion
|
|
395
488
|
//#region src/core/sync/session-sync.ts
|
|
396
489
|
const SESSION_READ_PAGE_SIZE = 256;
|
|
397
490
|
const MAX_SYNC_SESSIONS = 1e4;
|
|
491
|
+
const MAX_MANIFEST_PAYLOAD_CACHE_BYTES = 4194304;
|
|
492
|
+
const MAX_MANIFEST_PAYLOAD_CACHE_COUNT = 8;
|
|
493
|
+
const SESSION_EXPORT_CACHE_VERSION = 1;
|
|
398
494
|
const SPILL_META_VERSION = 1;
|
|
399
495
|
const SPILL_ARTIFACT_META_KEY = "@deepseek-ai/dsh-spill";
|
|
400
496
|
const LEGACY_LOCAL_SPILL_HINT = "Use read with offset/limit, or grep this path to search within it.";
|
|
@@ -408,6 +504,27 @@ var SyncSessionError = class extends Error {
|
|
|
408
504
|
this.name = "SyncSessionError";
|
|
409
505
|
}
|
|
410
506
|
};
|
|
507
|
+
async function readStableSessionMetadata(persistence, sessionId, firstSnapshot) {
|
|
508
|
+
const handle = await persistence.open(sessionId, "read");
|
|
509
|
+
try {
|
|
510
|
+
const snapshot = await persistence.stat(sessionId);
|
|
511
|
+
if (!snapshot || snapshot.formatStatus === "migration-required") return void 0;
|
|
512
|
+
try {
|
|
513
|
+
if (canonicalJson(firstSnapshot.revision) !== canonicalJson(snapshot.revision) || canonicalJson(firstSnapshot.header) !== canonicalJson(snapshot.header) || canonicalJson(handle.header) !== canonicalJson(snapshot.header)) return void 0;
|
|
514
|
+
} catch {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const inheritedEventCount = Number(handle.inheritedEventCount);
|
|
518
|
+
if (!Number.isSafeInteger(inheritedEventCount) || inheritedEventCount < 0) return void 0;
|
|
519
|
+
return {
|
|
520
|
+
snapshot,
|
|
521
|
+
header: handle.header,
|
|
522
|
+
inheritedEventCount
|
|
523
|
+
};
|
|
524
|
+
} finally {
|
|
525
|
+
await handle.close();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
411
528
|
function spillSessionDirectoryName(sessionId) {
|
|
412
529
|
return `session-${createHash("sha256").update(sessionId).digest("hex").slice(0, 12)}`;
|
|
413
530
|
}
|
|
@@ -770,6 +887,9 @@ function sessionEventDigest(header, inheritedEventCount, events, workspace, inst
|
|
|
770
887
|
} : void 0
|
|
771
888
|
}));
|
|
772
889
|
}
|
|
890
|
+
function sessionEventListDigest(events) {
|
|
891
|
+
return sha256(canonicalJson(events));
|
|
892
|
+
}
|
|
773
893
|
function sessionStateDigest(workspace, workspacePosition, archived, pinned) {
|
|
774
894
|
return sha256(canonicalJson({
|
|
775
895
|
workspace: {
|
|
@@ -786,6 +906,70 @@ function sessionStateDigest(workspace, workspacePosition, archived, pinned) {
|
|
|
786
906
|
pinned
|
|
787
907
|
}));
|
|
788
908
|
}
|
|
909
|
+
function sessionExportFingerprint(input) {
|
|
910
|
+
if (input.revision === void 0) return void 0;
|
|
911
|
+
try {
|
|
912
|
+
return sha256(canonicalJson({
|
|
913
|
+
version: SESSION_EXPORT_CACHE_VERSION,
|
|
914
|
+
...input
|
|
915
|
+
}));
|
|
916
|
+
} catch {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
function sessionExportCacheKey(sessionId, branchId) {
|
|
921
|
+
return JSON.stringify([sessionId, branchId ?? null]);
|
|
922
|
+
}
|
|
923
|
+
function estimateJsonBytes(value, limit) {
|
|
924
|
+
const active = /* @__PURE__ */ new WeakSet();
|
|
925
|
+
const measure = (item) => {
|
|
926
|
+
if (item === null) return 4;
|
|
927
|
+
if (typeof item === "string") {
|
|
928
|
+
const bytes = Buffer.byteLength(item, "utf8");
|
|
929
|
+
return bytes > limit / 6 ? limit + 1 : bytes * 6 + 2;
|
|
930
|
+
}
|
|
931
|
+
if (typeof item === "number") return Number.isFinite(item) ? String(item).length : 4;
|
|
932
|
+
if (typeof item === "boolean") return item ? 4 : 5;
|
|
933
|
+
if (item === void 0 || typeof item === "function" || typeof item === "symbol") return 4;
|
|
934
|
+
if (typeof item === "bigint") return limit + 1;
|
|
935
|
+
if (Array.isArray(item)) {
|
|
936
|
+
if (active.has(item)) return limit + 1;
|
|
937
|
+
active.add(item);
|
|
938
|
+
let bytes = 2;
|
|
939
|
+
for (let index = 0; index < item.length; index += 1) {
|
|
940
|
+
if (index > 0) bytes += 1;
|
|
941
|
+
bytes += measure(item[index]);
|
|
942
|
+
if (bytes > limit) break;
|
|
943
|
+
}
|
|
944
|
+
active.delete(item);
|
|
945
|
+
return bytes;
|
|
946
|
+
}
|
|
947
|
+
if (typeof item === "object") {
|
|
948
|
+
if (active.has(item)) return limit + 1;
|
|
949
|
+
const prototype = Object.getPrototypeOf(item);
|
|
950
|
+
if (prototype !== Object.prototype && prototype !== null) return limit + 1;
|
|
951
|
+
active.add(item);
|
|
952
|
+
let bytes = 2;
|
|
953
|
+
let first = true;
|
|
954
|
+
for (const [key, child] of Object.entries(item)) {
|
|
955
|
+
if (child === void 0 || typeof child === "function" || typeof child === "symbol") continue;
|
|
956
|
+
if (!first) bytes += 1;
|
|
957
|
+
first = false;
|
|
958
|
+
bytes += Buffer.byteLength(key, "utf8") * 6 + 3 + measure(child);
|
|
959
|
+
if (bytes > limit) break;
|
|
960
|
+
}
|
|
961
|
+
active.delete(item);
|
|
962
|
+
return bytes;
|
|
963
|
+
}
|
|
964
|
+
return limit + 1;
|
|
965
|
+
};
|
|
966
|
+
try {
|
|
967
|
+
const bytes = measure(value);
|
|
968
|
+
return bytes <= limit ? bytes : void 0;
|
|
969
|
+
} catch {
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
789
973
|
function planSessionTransfers(direction, localEntries, remoteEntries) {
|
|
790
974
|
const toLocal = [];
|
|
791
975
|
const toRemote = [];
|
|
@@ -852,15 +1036,65 @@ async function collectBytes(stream, maxBytes = MAX_SYNC_TRANSFER_BYTES) {
|
|
|
852
1036
|
async function* oneChunk(data) {
|
|
853
1037
|
yield data;
|
|
854
1038
|
}
|
|
855
|
-
function
|
|
856
|
-
|
|
1039
|
+
function createSessionStateIndex(state) {
|
|
1040
|
+
const index = {
|
|
1041
|
+
state,
|
|
1042
|
+
recordsByLocalId: /* @__PURE__ */ new Map(),
|
|
1043
|
+
workspacesByLocalId: /* @__PURE__ */ new Map(),
|
|
1044
|
+
archivesByLocalSessionId: /* @__PURE__ */ new Map()
|
|
1045
|
+
};
|
|
1046
|
+
for (const metadata of Object.values(state.records)) {
|
|
1047
|
+
if (metadata.kind !== "session") continue;
|
|
1048
|
+
const current = index.recordsByLocalId.get(metadata.localEntityId);
|
|
1049
|
+
if (!current || compareSyncRevision(metadata.revision, current.revision) > 0) index.recordsByLocalId.set(metadata.localEntityId, metadata);
|
|
1050
|
+
}
|
|
1051
|
+
for (const mapping of Object.values(state.workspaces)) {
|
|
1052
|
+
const mappings = index.workspacesByLocalId.get(mapping.localWorkspaceId) ?? [];
|
|
1053
|
+
mappings.push(mapping);
|
|
1054
|
+
index.workspacesByLocalId.set(mapping.localWorkspaceId, mappings);
|
|
1055
|
+
}
|
|
1056
|
+
for (const archive of Object.values(state.archives)) if (!index.archivesByLocalSessionId.has(archive.localSessionId)) index.archivesByLocalSessionId.set(archive.localSessionId, archive);
|
|
1057
|
+
return index;
|
|
857
1058
|
}
|
|
858
|
-
function
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1059
|
+
function indexSessionRecord(index, metadata) {
|
|
1060
|
+
const key = sessionOriginKey(metadata);
|
|
1061
|
+
const previous = index.state.records[key];
|
|
1062
|
+
if (previous && previous.localEntityId !== metadata.localEntityId && index.recordsByLocalId.get(previous.localEntityId) === previous) {
|
|
1063
|
+
delete index.state.records[key];
|
|
1064
|
+
index.recordsByLocalId.delete(previous.localEntityId);
|
|
1065
|
+
for (const candidate of Object.values(index.state.records)) {
|
|
1066
|
+
if (candidate.kind !== "session" || candidate.localEntityId !== previous.localEntityId) continue;
|
|
1067
|
+
const current = index.recordsByLocalId.get(previous.localEntityId);
|
|
1068
|
+
if (!current || compareSyncRevision(candidate.revision, current.revision) > 0) index.recordsByLocalId.set(previous.localEntityId, candidate);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
index.state.records[key] = metadata;
|
|
1072
|
+
const current = index.recordsByLocalId.get(metadata.localEntityId);
|
|
1073
|
+
if (current === previous || !current || compareSyncRevision(metadata.revision, current.revision) > 0) index.recordsByLocalId.set(metadata.localEntityId, metadata);
|
|
1074
|
+
}
|
|
1075
|
+
function indexWorkspaceMapping(index, key, mapping) {
|
|
1076
|
+
const previous = index.state.workspaces[key];
|
|
1077
|
+
if (previous) {
|
|
1078
|
+
const mappings = index.workspacesByLocalId.get(previous.localWorkspaceId);
|
|
1079
|
+
const previousIndex = mappings?.findIndex((item) => syncWorkspaceKey(item.originInstanceId, item.originWorkspaceId, item.remotePath) === key) ?? -1;
|
|
1080
|
+
if (mappings && previousIndex !== -1 && previous.localWorkspaceId === mapping.localWorkspaceId) {
|
|
1081
|
+
index.state.workspaces[key] = mapping;
|
|
1082
|
+
mappings[previousIndex] = mapping;
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
if (mappings && previousIndex !== -1) mappings.splice(previousIndex, 1);
|
|
1086
|
+
if (mappings?.length === 0) index.workspacesByLocalId.delete(previous.localWorkspaceId);
|
|
1087
|
+
}
|
|
1088
|
+
index.state.workspaces[key] = mapping;
|
|
1089
|
+
const mappings = index.workspacesByLocalId.get(mapping.localWorkspaceId) ?? [];
|
|
1090
|
+
mappings.push(mapping);
|
|
1091
|
+
index.workspacesByLocalId.set(mapping.localWorkspaceId, mappings);
|
|
1092
|
+
}
|
|
1093
|
+
function indexArchive(index, previousBranchId, archive) {
|
|
1094
|
+
delete index.state.archives[previousBranchId];
|
|
1095
|
+
index.state.archives[archive.branchId] = archive;
|
|
1096
|
+
index.archivesByLocalSessionId.clear();
|
|
1097
|
+
for (const entry of Object.values(index.state.archives)) if (!index.archivesByLocalSessionId.has(entry.localSessionId)) index.archivesByLocalSessionId.set(entry.localSessionId, entry);
|
|
864
1098
|
}
|
|
865
1099
|
function workspaceDigest(workspace) {
|
|
866
1100
|
return sha256(canonicalJson(workspace));
|
|
@@ -879,14 +1113,27 @@ function validateObjectPayload(object) {
|
|
|
879
1113
|
if (object.kind === "spill" && isRecord$1(object.reference) && isRecord$1(object.reference.owner) && typeof object.reference.owner.sessionId === "string" && isRecord$1(object.reference.ref) && typeof object.reference.ref.locator === "string" && object.id === spillKey(object.reference)) return object;
|
|
880
1114
|
throw new SyncSessionError("object-invalid", "session object kind or reference is invalid");
|
|
881
1115
|
}
|
|
1116
|
+
function validateEventDelta(value) {
|
|
1117
|
+
if (!isRecord$1(value) || !isNonNegativeSafeInteger(value.baseEventCount) || typeof value.baseEventDigest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(value.baseEventDigest) || !isNonNegativeSafeInteger(value.baseInheritedEventCount) || !isNonNegativeSafeInteger(value.fullEventCount) || value.fullEventCount < value.baseEventCount || typeof value.fullEventDigest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(value.fullEventDigest)) throw new SyncSessionError("manifest-invalid", "session event delta metadata is invalid");
|
|
1118
|
+
return {
|
|
1119
|
+
baseEventCount: value.baseEventCount,
|
|
1120
|
+
baseEventDigest: value.baseEventDigest,
|
|
1121
|
+
baseInheritedEventCount: value.baseInheritedEventCount,
|
|
1122
|
+
fullEventCount: value.fullEventCount,
|
|
1123
|
+
fullEventDigest: value.fullEventDigest
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
882
1126
|
function validateEntry(entry) {
|
|
883
1127
|
if (!isRecord$1(entry) || !isRecord$1(entry.metadata) || entry.metadata.kind !== "session") throw new SyncSessionError("manifest-invalid", "session entry metadata is invalid");
|
|
884
1128
|
const metadata = entry.metadata;
|
|
885
1129
|
if (typeof metadata.originInstanceId !== "string" || !metadata.originInstanceId || metadata.originInstanceId.length > 512 || /[\u0000-\u001f\u007f]/.test(metadata.originInstanceId) || typeof metadata.originEntityId !== "string" || !metadata.originEntityId || metadata.originEntityId.length > 512 || /[\u0000-\u001f\u007f]/.test(metadata.originEntityId) || typeof metadata.localEntityId !== "string" || !metadata.localEntityId || metadata.localEntityId.length > 512 || /[\u0000-\u001f\u007f]/.test(metadata.localEntityId) || typeof metadata.contentDigest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(metadata.contentDigest) || !metadata.revision || !isNonNegativeSafeInteger(metadata.revision.physicalTimeMs) || !isNonNegativeSafeInteger(metadata.revision.logicalCounter) || typeof metadata.revision.writerInstanceId !== "string" || !metadata.revision.writerInstanceId || /[\u0000-\u001f\u007f]/.test(metadata.revision.writerInstanceId)) throw new SyncSessionError("manifest-invalid", "session entry metadata fields are invalid");
|
|
886
1130
|
if (metadata.stateDigest !== void 0 && !/^sha256:[a-f0-9]{64}$/.test(metadata.stateDigest)) throw new SyncSessionError("manifest-invalid", "session state digest is invalid");
|
|
1131
|
+
if (metadata.deleted !== void 0 && typeof metadata.deleted !== "boolean") throw new SyncSessionError("manifest-invalid", "session tombstone flag is invalid");
|
|
887
1132
|
if (entry.branchId !== void 0 && (typeof entry.branchId !== "string" || !/^b-[a-f0-9]{32}$/.test(entry.branchId))) throw new SyncSessionError("manifest-invalid", "session branch ID is invalid");
|
|
888
1133
|
if (metadata.workspaceOrigin !== void 0) validateOrigin(metadata.workspaceOrigin);
|
|
1134
|
+
const eventDelta = entry.eventDelta === void 0 ? void 0 : validateEventDelta(entry.eventDelta);
|
|
889
1135
|
if (metadata.deleted) {
|
|
1136
|
+
if (eventDelta) throw new SyncSessionError("manifest-invalid", "session tombstones cannot carry event deltas");
|
|
890
1137
|
if (metadata.contentDigest !== sessionTombstoneDigest(metadata)) throw new SyncSessionError("manifest-invalid", "session tombstone digest is invalid");
|
|
891
1138
|
return {
|
|
892
1139
|
metadata,
|
|
@@ -896,11 +1143,19 @@ function validateEntry(entry) {
|
|
|
896
1143
|
const workspace = isRecord$1(entry.workspace) ? entry.workspace : void 0;
|
|
897
1144
|
const workspaceRevision = workspace && isRecord$1(workspace.revision) ? workspace.revision : void 0;
|
|
898
1145
|
if (typeof entry.sourceLocalSessionId !== "string" || !isRecord$1(entry.header) || typeof entry.inheritedEventCount !== "number" || !Number.isSafeInteger(entry.inheritedEventCount) || entry.inheritedEventCount < 0 || !Array.isArray(entry.events) || !workspace || !Array.isArray(entry.objects) || typeof workspace.originInstanceId !== "string" || typeof workspace.originWorkspaceId !== "string" || !isRecord$1(workspace.nodeOrigin) || typeof workspace.nodeOrigin.originInstanceId !== "string" || typeof workspace.nodeOrigin.originEntityId !== "string" || typeof workspace.remotePath !== "string" || !posix.isAbsolute(workspace.remotePath) || posix.normalize(workspace.remotePath) !== workspace.remotePath || typeof workspace.title !== "string" || typeof workspace.contentDigest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(workspace.contentDigest) || !workspaceRevision || !isNonNegativeSafeInteger(workspaceRevision.physicalTimeMs) || !isNonNegativeSafeInteger(workspaceRevision.logicalCounter) || typeof workspaceRevision.writerInstanceId !== "string" || typeof entry.workspacePosition !== "number" || !Number.isSafeInteger(entry.workspacePosition) || entry.workspacePosition < 0 || typeof entry.archived !== "boolean" || typeof entry.pinned !== "boolean") throw new SyncSessionError("manifest-invalid", "active session entry fields are invalid");
|
|
1146
|
+
if (eventDelta && (entry.branchId !== void 0 || entry.archived)) throw new SyncSessionError("manifest-invalid", "archives cannot use event tail deltas");
|
|
899
1147
|
if (entry.events.length > MAX_SYNC_SESSIONS * 100) throw new SyncSessionError("manifest-invalid", "session event count exceeds limit");
|
|
900
1148
|
const header = entry.header;
|
|
901
1149
|
if (header.version !== 4 || header.id !== entry.sourceLocalSessionId) throw new SyncSessionError("session-format", "session header format or id is invalid");
|
|
902
|
-
|
|
903
|
-
|
|
1150
|
+
let events;
|
|
1151
|
+
if (eventDelta) {
|
|
1152
|
+
if (eventDelta.baseInheritedEventCount !== entry.inheritedEventCount || eventDelta.fullEventCount > MAX_SYNC_SESSIONS * 100 || eventDelta.fullEventCount - eventDelta.baseEventCount !== entry.events.length) throw new SyncSessionError("manifest-invalid", "session event delta counts are inconsistent");
|
|
1153
|
+
events = structuredClone(entry.events);
|
|
1154
|
+
for (let index = 0; index < events.length; index += 1) if (!isNonNegativeSafeInteger(events[index]?.seq) || events[index]?.seq !== eventDelta.baseEventCount + index) throw new SyncSessionError("manifest-invalid", "session event delta sequence is invalid");
|
|
1155
|
+
} else {
|
|
1156
|
+
events = validateStoredEvents(header, structuredClone(entry.events));
|
|
1157
|
+
assertContiguous(header.id, events, 0);
|
|
1158
|
+
}
|
|
904
1159
|
const objects = entry.objects.map(validateObjectPayload);
|
|
905
1160
|
if (new Set(objects.map((object) => object.id)).size !== objects.length) throw new SyncSessionError("manifest-invalid", "session entry contains duplicate objects");
|
|
906
1161
|
const workspaceValue = workspace;
|
|
@@ -918,9 +1173,11 @@ function validateEntry(entry) {
|
|
|
918
1173
|
for (const ref of attachments.files) referencedObjects.add(attachmentKey("file", ref));
|
|
919
1174
|
for (const reference of [...spillReferences(events), ...legacySpillReferences(events, String(header.id))]) referencedObjects.add(spillKey(reference));
|
|
920
1175
|
const objectIds = new Set(objects.map((object) => object.id));
|
|
921
|
-
if ([...referencedObjects].some((id) => !objectIds.has(id)) || [...objectIds].some((id) => !referencedObjects.has(id))) throw new SyncSessionError("manifest-invalid", "session object references and payloads do not match");
|
|
1176
|
+
if ([...referencedObjects].some((id) => !objectIds.has(id)) || !eventDelta && [...objectIds].some((id) => !referencedObjects.has(id))) throw new SyncSessionError("manifest-invalid", "session object references and payloads do not match");
|
|
922
1177
|
const parentOrigin = entry.parentOrigin === void 0 ? void 0 : validateOrigin(entry.parentOrigin);
|
|
923
|
-
if (
|
|
1178
|
+
if (!eventDelta) {
|
|
1179
|
+
if (sessionEventDigest(header, entry.inheritedEventCount, events, workspaceValue, instructionSnapshot, objects, parentOrigin) !== metadata.contentDigest) throw new SyncSessionError("manifest-invalid", "session content digest is invalid");
|
|
1180
|
+
}
|
|
924
1181
|
if (metadata.stateDigest !== sessionStateDigest(workspaceValue, entry.workspacePosition, entry.archived, entry.pinned)) throw new SyncSessionError("manifest-invalid", "session state digest is invalid");
|
|
925
1182
|
return {
|
|
926
1183
|
metadata,
|
|
@@ -935,21 +1192,54 @@ function validateEntry(entry) {
|
|
|
935
1192
|
pinned: entry.pinned,
|
|
936
1193
|
...instructionSnapshot ? { instructionSnapshot } : {},
|
|
937
1194
|
events,
|
|
938
|
-
objects
|
|
1195
|
+
objects,
|
|
1196
|
+
...eventDelta ? { eventDelta } : {}
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
function validateManifestEntry(entry) {
|
|
1200
|
+
if (!isRecord$1(entry) || !isRecord$1(entry.metadata) || entry.metadata.kind !== "session") throw new SyncSessionError("manifest-invalid", "session manifest entry metadata is invalid");
|
|
1201
|
+
const metadata = entry.metadata;
|
|
1202
|
+
if (typeof metadata.originInstanceId !== "string" || !metadata.originInstanceId || metadata.originInstanceId.length > 512 || /[\u0000-\u001f\u007f]/.test(metadata.originInstanceId) || typeof metadata.originEntityId !== "string" || !metadata.originEntityId || metadata.originEntityId.length > 512 || /[\u0000-\u001f\u007f]/.test(metadata.originEntityId) || typeof metadata.localEntityId !== "string" || !metadata.localEntityId || metadata.localEntityId.length > 512 || /[\u0000-\u001f\u007f]/.test(metadata.localEntityId) || typeof metadata.contentDigest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(metadata.contentDigest) || !metadata.revision || !isNonNegativeSafeInteger(metadata.revision.physicalTimeMs) || !isNonNegativeSafeInteger(metadata.revision.logicalCounter) || typeof metadata.revision.writerInstanceId !== "string" || !metadata.revision.writerInstanceId || /[\u0000-\u001f\u007f]/.test(metadata.revision.writerInstanceId)) throw new SyncSessionError("manifest-invalid", "session manifest metadata fields are invalid");
|
|
1203
|
+
if (metadata.stateDigest !== void 0 && !/^sha256:[a-f0-9]{64}$/.test(metadata.stateDigest)) throw new SyncSessionError("manifest-invalid", "session manifest state digest is invalid");
|
|
1204
|
+
if (metadata.deleted !== void 0 && typeof metadata.deleted !== "boolean") throw new SyncSessionError("manifest-invalid", "session manifest tombstone flag is invalid");
|
|
1205
|
+
const hasEventCount = entry.eventCount !== void 0;
|
|
1206
|
+
const hasEventDigest = entry.eventDigest !== void 0;
|
|
1207
|
+
const hasInheritedEventCount = entry.inheritedEventCount !== void 0;
|
|
1208
|
+
if (hasEventCount || hasEventDigest || hasInheritedEventCount) {
|
|
1209
|
+
if (!hasEventCount || !hasEventDigest || !hasInheritedEventCount || metadata.deleted || !isNonNegativeSafeInteger(entry.eventCount) || entry.eventCount > MAX_SYNC_SESSIONS * 100 || typeof entry.eventDigest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(entry.eventDigest) || !isNonNegativeSafeInteger(entry.inheritedEventCount) || entry.inheritedEventCount > entry.eventCount) throw new SyncSessionError("manifest-invalid", "session event summary is invalid");
|
|
1210
|
+
}
|
|
1211
|
+
if (entry.branchId !== void 0 && (typeof entry.branchId !== "string" || !/^b-[a-f0-9]{32}$/.test(entry.branchId))) throw new SyncSessionError("manifest-invalid", "session manifest branch ID is invalid");
|
|
1212
|
+
if (metadata.workspaceOrigin !== void 0) validateOrigin(metadata.workspaceOrigin);
|
|
1213
|
+
if (metadata.deleted && metadata.contentDigest !== sessionTombstoneDigest(metadata)) throw new SyncSessionError("manifest-invalid", "session manifest tombstone digest is invalid");
|
|
1214
|
+
return {
|
|
1215
|
+
metadata,
|
|
1216
|
+
...entry.branchId ? { branchId: entry.branchId } : {},
|
|
1217
|
+
...hasEventCount ? { eventCount: entry.eventCount } : {},
|
|
1218
|
+
...hasEventDigest ? { eventDigest: entry.eventDigest } : {},
|
|
1219
|
+
...hasInheritedEventCount ? { inheritedEventCount: entry.inheritedEventCount } : {}
|
|
939
1220
|
};
|
|
940
1221
|
}
|
|
941
1222
|
function bundleDigest(bundle) {
|
|
1223
|
+
const sharedObjects = new Map((bundle.objects ?? []).map((object) => [object.id, object]));
|
|
942
1224
|
const manifest = bundle.entries.map((entry) => ({
|
|
943
1225
|
key: entry.branchId ?? sessionOriginKey(entry.metadata),
|
|
944
1226
|
revision: entry.metadata.revision,
|
|
945
1227
|
contentDigest: entry.metadata.contentDigest,
|
|
946
1228
|
stateDigest: entry.metadata.stateDigest,
|
|
947
1229
|
deleted: Boolean(entry.metadata.deleted),
|
|
948
|
-
objects: (entry.objects ?? []).map((
|
|
1230
|
+
objects: (entry.objects ?? (entry.sharedObjectIds ?? []).map((id) => {
|
|
1231
|
+
const object = sharedObjects.get(id);
|
|
1232
|
+
if (!object) throw new SyncSessionError("manifest-invalid", "session shared object id is unknown");
|
|
1233
|
+
return object;
|
|
1234
|
+
})).map((object) => ({
|
|
949
1235
|
id: object.id,
|
|
950
1236
|
digest: object.digest,
|
|
951
1237
|
bytes: object.bytes
|
|
952
|
-
})).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
|
|
1238
|
+
})).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0),
|
|
1239
|
+
eventDelta: entry.eventDelta === void 0 ? void 0 : {
|
|
1240
|
+
...entry.eventDelta,
|
|
1241
|
+
suffixDigest: sessionEventListDigest(entry.events ?? [])
|
|
1242
|
+
}
|
|
953
1243
|
})).sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
954
1244
|
const skipped = bundle.skipped.map((entry) => ({
|
|
955
1245
|
sessionId: entry.sessionId,
|
|
@@ -961,12 +1251,36 @@ function bundleDigest(bundle) {
|
|
|
961
1251
|
skipped
|
|
962
1252
|
}));
|
|
963
1253
|
}
|
|
1254
|
+
function materializeSharedObjects(bundle) {
|
|
1255
|
+
const shared = bundle.objects === void 0 ? /* @__PURE__ */ new Map() : new Map(bundle.objects.map((object) => {
|
|
1256
|
+
const validated = validateObjectPayload(object);
|
|
1257
|
+
return [validated.id, validated];
|
|
1258
|
+
}));
|
|
1259
|
+
if (shared.size !== (bundle.objects?.length ?? 0)) throw new SyncSessionError("manifest-invalid", "session bundle contains duplicate shared objects");
|
|
1260
|
+
for (const entry of bundle.entries) {
|
|
1261
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1262
|
+
if (entry.sharedObjectIds !== void 0 && (!Array.isArray(entry.sharedObjectIds) || new Set(entry.sharedObjectIds).size !== entry.sharedObjectIds.length || entry.sharedObjectIds.some((id) => !shared.has(id)))) throw new SyncSessionError("manifest-invalid", "session entry shared object references are invalid");
|
|
1263
|
+
}
|
|
1264
|
+
return shared;
|
|
1265
|
+
}
|
|
964
1266
|
function validateSyncSessionBundle(value) {
|
|
965
1267
|
if (!isRecord$1(value) || value.protocolVersion !== 1 || typeof value.instanceId !== "string" || !/^i-[a-f0-9]{32}$/.test(value.instanceId) || !Array.isArray(value.entries) || typeof value.digest !== "string") throw new SyncSessionError("manifest-invalid", "session bundle header is invalid");
|
|
1268
|
+
if (value.objects !== void 0 && (!Array.isArray(value.objects) || value.objects.some((object) => !isRecord$1(object)))) throw new SyncSessionError("manifest-invalid", "session bundle shared object table is invalid");
|
|
1269
|
+
const sharedObjects = materializeSharedObjects({
|
|
1270
|
+
...value.objects === void 0 ? {} : { objects: value.objects },
|
|
1271
|
+
entries: value.entries
|
|
1272
|
+
});
|
|
966
1273
|
const skipped = value.skipped === void 0 ? [] : value.skipped;
|
|
967
1274
|
if (!Array.isArray(skipped) || skipped.some((item) => !isRecord$1(item) || typeof item.sessionId !== "string" || typeof item.reason !== "string")) throw new SyncSessionError("manifest-invalid", "session bundle skip report is invalid");
|
|
968
1275
|
if (value.entries.length > MAX_SYNC_SESSIONS) throw new SyncSessionError("manifest-invalid", "session bundle contains too many records");
|
|
969
|
-
const entries = value.entries.map(
|
|
1276
|
+
const entries = value.entries.map((entry) => {
|
|
1277
|
+
const materialized = { ...entry };
|
|
1278
|
+
if (materialized.sharedObjectIds !== void 0) {
|
|
1279
|
+
if (materialized.objects !== void 0) throw new SyncSessionError("manifest-invalid", "session entry cannot carry inline and shared objects");
|
|
1280
|
+
materialized.objects = materialized.sharedObjectIds.map((id) => sharedObjects.get(id));
|
|
1281
|
+
}
|
|
1282
|
+
return validateEntry(materialized);
|
|
1283
|
+
});
|
|
970
1284
|
const keys = entries.map((entry) => entry.branchId ?? sessionOriginKey(entry.metadata));
|
|
971
1285
|
if (new Set(keys).size !== keys.length) throw new SyncSessionError("manifest-invalid", "session bundle contains duplicate records");
|
|
972
1286
|
const bundle = {
|
|
@@ -974,13 +1288,35 @@ function validateSyncSessionBundle(value) {
|
|
|
974
1288
|
instanceId: value.instanceId,
|
|
975
1289
|
entries,
|
|
976
1290
|
skipped,
|
|
1291
|
+
...value.objects === void 0 ? {} : { objects: value.objects },
|
|
977
1292
|
digest: value.digest
|
|
978
1293
|
};
|
|
979
1294
|
if (bundleDigest(bundle) !== bundle.digest) throw new SyncSessionError("manifest-digest", "session bundle digest does not match");
|
|
980
1295
|
if (Buffer.byteLength(JSON.stringify(bundle), "utf8") > 107374182400) throw new SyncSessionError("bundle-too-large", "session bundle exceeds the transfer limit");
|
|
981
1296
|
return bundle;
|
|
982
1297
|
}
|
|
983
|
-
function makeSyncSessionBundle(instanceId, entries, skipped = []) {
|
|
1298
|
+
function makeSyncSessionBundle(instanceId, entries, skipped = [], sharedObjects) {
|
|
1299
|
+
if (sharedObjects !== void 0) {
|
|
1300
|
+
const referencesShared = entries.some((entry) => (entry.objects?.length ?? 0) > 0 || entry.sharedObjectIds !== void 0);
|
|
1301
|
+
const bundle = {
|
|
1302
|
+
protocolVersion: 1,
|
|
1303
|
+
instanceId,
|
|
1304
|
+
entries: referencesShared ? entries.map((entry) => {
|
|
1305
|
+
if (entry.objects === void 0) return entry;
|
|
1306
|
+
const { objects, ...rest } = entry;
|
|
1307
|
+
return {
|
|
1308
|
+
...rest,
|
|
1309
|
+
sharedObjectIds: objects.map((object) => object.id)
|
|
1310
|
+
};
|
|
1311
|
+
}) : entries,
|
|
1312
|
+
skipped,
|
|
1313
|
+
...referencesShared ? { objects: sharedObjects } : {},
|
|
1314
|
+
digest: ""
|
|
1315
|
+
};
|
|
1316
|
+
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");
|
|
1318
|
+
return bundle;
|
|
1319
|
+
}
|
|
984
1320
|
const bundle = {
|
|
985
1321
|
protocolVersion: 1,
|
|
986
1322
|
instanceId,
|
|
@@ -992,6 +1328,80 @@ function makeSyncSessionBundle(instanceId, entries, skipped = []) {
|
|
|
992
1328
|
if (Buffer.byteLength(JSON.stringify(bundle), "utf8") > 107374182400) throw new SyncSessionError("bundle-too-large", "session bundle exceeds the transfer limit");
|
|
993
1329
|
return bundle;
|
|
994
1330
|
}
|
|
1331
|
+
function syncSessionEntryKey(entry) {
|
|
1332
|
+
return entry.branchId ?? sessionOriginKey(entry.metadata);
|
|
1333
|
+
}
|
|
1334
|
+
function withEventTail(entry, prefix) {
|
|
1335
|
+
if (!prefix || entry.metadata.deleted || entry.branchId || !entry.events || entry.events.length === 0 || prefix.eventCount === 0 || prefix.eventCount > entry.events.length || prefix.inheritedEventCount !== entry.inheritedEventCount) return entry;
|
|
1336
|
+
if (sessionEventListDigest(entry.events.slice(0, prefix.eventCount)) !== prefix.eventDigest) return entry;
|
|
1337
|
+
const fullEventDigest = entry.eventDigest !== void 0 && entry.eventCount === entry.events.length ? entry.eventDigest : sessionEventListDigest(entry.events);
|
|
1338
|
+
return {
|
|
1339
|
+
...entry,
|
|
1340
|
+
eventDelta: {
|
|
1341
|
+
baseEventCount: prefix.eventCount,
|
|
1342
|
+
baseEventDigest: prefix.eventDigest,
|
|
1343
|
+
baseInheritedEventCount: prefix.inheritedEventCount,
|
|
1344
|
+
fullEventCount: entry.events.length,
|
|
1345
|
+
fullEventDigest
|
|
1346
|
+
},
|
|
1347
|
+
events: entry.events.slice(prefix.eventCount)
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
function sessionManifestDigest(manifest) {
|
|
1351
|
+
const entries = manifest.entries.map((entry) => ({
|
|
1352
|
+
key: syncSessionEntryKey(entry),
|
|
1353
|
+
metadata: entry.metadata,
|
|
1354
|
+
eventCount: entry.eventCount,
|
|
1355
|
+
eventDigest: entry.eventDigest,
|
|
1356
|
+
inheritedEventCount: entry.inheritedEventCount
|
|
1357
|
+
})).sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
1358
|
+
const skipped = manifest.skipped.map((entry) => ({
|
|
1359
|
+
sessionId: entry.sessionId,
|
|
1360
|
+
reason: entry.reason
|
|
1361
|
+
})).sort((a, b) => a.sessionId < b.sessionId ? -1 : a.sessionId > b.sessionId ? 1 : 0);
|
|
1362
|
+
return sha256(canonicalJson({
|
|
1363
|
+
instanceId: manifest.instanceId,
|
|
1364
|
+
entries,
|
|
1365
|
+
skipped
|
|
1366
|
+
}));
|
|
1367
|
+
}
|
|
1368
|
+
function makeSyncSessionManifest(instanceId, entries, skipped = []) {
|
|
1369
|
+
const manifest = {
|
|
1370
|
+
protocolVersion: 1,
|
|
1371
|
+
instanceId,
|
|
1372
|
+
entries: entries.map((entry) => ({
|
|
1373
|
+
metadata: entry.metadata,
|
|
1374
|
+
...entry.branchId ? { branchId: entry.branchId } : {},
|
|
1375
|
+
...entry.eventCount === void 0 ? {} : { eventCount: entry.eventCount },
|
|
1376
|
+
...entry.eventDigest === void 0 ? {} : { eventDigest: entry.eventDigest },
|
|
1377
|
+
...entry.inheritedEventCount === void 0 ? {} : { inheritedEventCount: entry.inheritedEventCount }
|
|
1378
|
+
})),
|
|
1379
|
+
skipped,
|
|
1380
|
+
digest: ""
|
|
1381
|
+
};
|
|
1382
|
+
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");
|
|
1384
|
+
return manifest;
|
|
1385
|
+
}
|
|
1386
|
+
function validateSyncSessionManifest(value) {
|
|
1387
|
+
if (!isRecord$1(value) || value.protocolVersion !== 1 || typeof value.instanceId !== "string" || !/^i-[a-f0-9]{32}$/.test(value.instanceId) || !Array.isArray(value.entries) || typeof value.digest !== "string") throw new SyncSessionError("manifest-invalid", "session manifest header is invalid");
|
|
1388
|
+
const skipped = value.skipped === void 0 ? [] : value.skipped;
|
|
1389
|
+
if (!Array.isArray(skipped) || skipped.some((item) => !isRecord$1(item) || typeof item.sessionId !== "string" || typeof item.reason !== "string")) throw new SyncSessionError("manifest-invalid", "session manifest skip report is invalid");
|
|
1390
|
+
if (value.entries.length > MAX_SYNC_SESSIONS) throw new SyncSessionError("manifest-invalid", "session manifest contains too many records");
|
|
1391
|
+
const entries = value.entries.map(validateManifestEntry);
|
|
1392
|
+
const keys = entries.map(syncSessionEntryKey);
|
|
1393
|
+
if (new Set(keys).size !== keys.length) throw new SyncSessionError("manifest-invalid", "session manifest contains duplicate records");
|
|
1394
|
+
const manifest = {
|
|
1395
|
+
protocolVersion: 1,
|
|
1396
|
+
instanceId: value.instanceId,
|
|
1397
|
+
entries,
|
|
1398
|
+
skipped,
|
|
1399
|
+
digest: value.digest
|
|
1400
|
+
};
|
|
1401
|
+
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");
|
|
1403
|
+
return manifest;
|
|
1404
|
+
}
|
|
995
1405
|
function emptyApplyResult() {
|
|
996
1406
|
return {
|
|
997
1407
|
created: 0,
|
|
@@ -1066,12 +1476,16 @@ function arrayBuffer(data) {
|
|
|
1066
1476
|
if (value.toString("base64") !== data) throw new SyncSessionError("object-invalid", "session object is not canonical base64");
|
|
1067
1477
|
return value;
|
|
1068
1478
|
}
|
|
1069
|
-
function instructionsForSession(store, sessionId,
|
|
1479
|
+
function instructionsForSession(store, sessionId, stateIndex, preferredWorkspaceOrigin) {
|
|
1070
1480
|
const snapshot = store.get(sessionId);
|
|
1071
1481
|
if (!snapshot) return void 0;
|
|
1072
|
-
const workspaceMappings = snapshot.workspaceId ?
|
|
1482
|
+
const workspaceMappings = snapshot.workspaceId ? stateIndex.workspacesByLocalId.get(snapshot.workspaceId) ?? [] : [];
|
|
1073
1483
|
const workspaceMapping = preferredWorkspaceOrigin ? workspaceMappings.find((mapping) => mapping.originInstanceId === preferredWorkspaceOrigin.originInstanceId && mapping.originWorkspaceId === preferredWorkspaceOrigin.originEntityId) ?? (workspaceMappings.length === 1 ? workspaceMappings[0] : void 0) : workspaceMappings.length === 1 ? workspaceMappings[0] : void 0;
|
|
1074
|
-
const
|
|
1484
|
+
const inheritedFrom = snapshot.inheritedFromSessionId ? stateIndex.recordsByLocalId.get(snapshot.inheritedFromSessionId) : void 0;
|
|
1485
|
+
const inheritedFromOrigin = inheritedFrom ? {
|
|
1486
|
+
originInstanceId: inheritedFrom.originInstanceId,
|
|
1487
|
+
originEntityId: inheritedFrom.originEntityId
|
|
1488
|
+
} : void 0;
|
|
1075
1489
|
return {
|
|
1076
1490
|
text: snapshot.text,
|
|
1077
1491
|
capturedAt: snapshot.capturedAt,
|
|
@@ -1100,7 +1514,11 @@ var SyncSessionService = class {
|
|
|
1100
1514
|
journal;
|
|
1101
1515
|
now;
|
|
1102
1516
|
operationTail = Promise.resolve();
|
|
1103
|
-
|
|
1517
|
+
exportCache = /* @__PURE__ */ new Map();
|
|
1518
|
+
exportCacheStore;
|
|
1519
|
+
manifestPayloadCache = /* @__PURE__ */ new Map();
|
|
1520
|
+
manifestPayloadCacheBytes = 0;
|
|
1521
|
+
constructor(state, nodes, registry, bindings, getWorkspaces, getPersistence, getAttachments, getSpills, instructions, journal = new SyncSessionJournalStore(), now = Date.now, home) {
|
|
1104
1522
|
this.state = state;
|
|
1105
1523
|
this.nodes = nodes;
|
|
1106
1524
|
this.registry = registry;
|
|
@@ -1112,10 +1530,80 @@ var SyncSessionService = class {
|
|
|
1112
1530
|
this.instructions = instructions;
|
|
1113
1531
|
this.journal = journal;
|
|
1114
1532
|
this.now = now;
|
|
1533
|
+
this.exportCacheStore = new SyncSessionExportCacheStore(home);
|
|
1534
|
+
for (const [key, entry] of this.exportCacheStore.entries()) if (!this.exportCache.has(key)) this.exportCache.set(key, entry);
|
|
1115
1535
|
}
|
|
1116
1536
|
exportBundle() {
|
|
1117
1537
|
return this.serializeMutation(() => this.exportBundleNow());
|
|
1118
1538
|
}
|
|
1539
|
+
exportManifest() {
|
|
1540
|
+
return this.serializeMutation(async () => {
|
|
1541
|
+
const payloadCache = {
|
|
1542
|
+
instanceId: this.state.instanceId(),
|
|
1543
|
+
entries: /* @__PURE__ */ new Map(),
|
|
1544
|
+
bytes: 0
|
|
1545
|
+
};
|
|
1546
|
+
const bundle = await this.exportBundleNow({
|
|
1547
|
+
manifestOnly: true,
|
|
1548
|
+
payloadCache
|
|
1549
|
+
});
|
|
1550
|
+
const manifest = makeSyncSessionManifest(bundle.instanceId, bundle.entries, bundle.skipped);
|
|
1551
|
+
this.rememberManifestPayloads(manifest.digest, payloadCache);
|
|
1552
|
+
return manifest;
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
exportEntries(keys, manifestDigest, prefixes = [], options = {}) {
|
|
1556
|
+
return this.serializeMutation(async () => {
|
|
1557
|
+
if (keys.length > MAX_SYNC_SESSIONS || keys.some((key) => typeof key !== "string" || key.length > 2048) || new Set(keys).size !== keys.length) throw new SyncSessionError("manifest-invalid", "session entry selection is invalid");
|
|
1558
|
+
if (manifestDigest !== void 0 && !/^sha256:[a-f0-9]{64}$/.test(manifestDigest)) throw new SyncSessionError("manifest-invalid", "session manifest digest is invalid");
|
|
1559
|
+
const selectedKeys = new Set(keys);
|
|
1560
|
+
const prefixesByKey = /* @__PURE__ */ new Map();
|
|
1561
|
+
for (const prefix of prefixes) {
|
|
1562
|
+
if (!selectedKeys.has(prefix.key) || prefixesByKey.has(prefix.key) || !isNonNegativeSafeInteger(prefix.eventCount) || prefix.eventCount > MAX_SYNC_SESSIONS * 100 || !/^sha256:[a-f0-9]{64}$/.test(prefix.eventDigest) || !isNonNegativeSafeInteger(prefix.inheritedEventCount) || prefix.inheritedEventCount > prefix.eventCount) throw new SyncSessionError("manifest-invalid", "session event prefix is invalid");
|
|
1563
|
+
prefixesByKey.set(prefix.key, prefix);
|
|
1564
|
+
}
|
|
1565
|
+
let bundle;
|
|
1566
|
+
const payloadCache = manifestDigest ? this.manifestPayloadCache.get(manifestDigest) : void 0;
|
|
1567
|
+
if (payloadCache) {
|
|
1568
|
+
const cachedEntries = keys.flatMap((key) => {
|
|
1569
|
+
const entry = payloadCache.entries.get(key);
|
|
1570
|
+
return entry ? [entry] : [];
|
|
1571
|
+
});
|
|
1572
|
+
const cachedKeys = new Set(cachedEntries.map(syncSessionEntryKey));
|
|
1573
|
+
const missingKeys = keys.filter((key) => !cachedKeys.has(key));
|
|
1574
|
+
const additional = missingKeys.length > 0 ? await this.exportBundleNow({ selectedKeys: new Set(missingKeys) }) : makeSyncSessionBundle(payloadCache.instanceId, []);
|
|
1575
|
+
bundle = makeSyncSessionBundle(payloadCache.instanceId, [...cachedEntries, ...additional.entries], additional.skipped);
|
|
1576
|
+
} else bundle = await this.exportBundleNow({
|
|
1577
|
+
selectedKeys,
|
|
1578
|
+
sharedObjects: options.sharedObjects === true
|
|
1579
|
+
});
|
|
1580
|
+
const withTails = bundle.entries.map((entry) => withEventTail(entry, prefixesByKey.get(syncSessionEntryKey(entry))));
|
|
1581
|
+
if (bundle.objects !== void 0) return makeSyncSessionBundle(bundle.instanceId, withTails, bundle.skipped, bundle.objects);
|
|
1582
|
+
const inlineEntries = withTails.map((entry) => {
|
|
1583
|
+
if (entry.sharedObjectIds === void 0) return entry;
|
|
1584
|
+
const { sharedObjectIds, ...rest } = entry;
|
|
1585
|
+
return rest;
|
|
1586
|
+
});
|
|
1587
|
+
const sharedObjects = options.sharedObjects === true ? this.dedupeBundleObjects(inlineEntries) : void 0;
|
|
1588
|
+
return makeSyncSessionBundle(bundle.instanceId, inlineEntries, bundle.skipped, sharedObjects);
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
rememberManifestPayloads(digest, payloads) {
|
|
1592
|
+
if (payloads.entries.size === 0 || payloads.bytes > MAX_MANIFEST_PAYLOAD_CACHE_BYTES) return;
|
|
1593
|
+
const previous = this.manifestPayloadCache.get(digest);
|
|
1594
|
+
if (previous) {
|
|
1595
|
+
this.manifestPayloadCacheBytes -= previous.bytes;
|
|
1596
|
+
this.manifestPayloadCache.delete(digest);
|
|
1597
|
+
}
|
|
1598
|
+
while (this.manifestPayloadCache.size >= MAX_MANIFEST_PAYLOAD_CACHE_COUNT || this.manifestPayloadCacheBytes + payloads.bytes > MAX_MANIFEST_PAYLOAD_CACHE_BYTES) {
|
|
1599
|
+
const oldest = this.manifestPayloadCache.entries().next().value;
|
|
1600
|
+
if (!oldest) return;
|
|
1601
|
+
this.manifestPayloadCache.delete(oldest[0]);
|
|
1602
|
+
this.manifestPayloadCacheBytes -= oldest[1].bytes;
|
|
1603
|
+
}
|
|
1604
|
+
this.manifestPayloadCache.set(digest, payloads);
|
|
1605
|
+
this.manifestPayloadCacheBytes += payloads.bytes;
|
|
1606
|
+
}
|
|
1119
1607
|
applyBundle(value, sourceName, peerId = "p-000000000000000000000000", options = {}) {
|
|
1120
1608
|
return this.serializeMutation(() => this.applyBundleNow(value, sourceName, peerId, options));
|
|
1121
1609
|
}
|
|
@@ -1170,13 +1658,51 @@ var SyncSessionService = class {
|
|
|
1170
1658
|
restoreArchive(branchId) {
|
|
1171
1659
|
return this.serializeMutation(() => this.restoreArchiveNow(branchId));
|
|
1172
1660
|
}
|
|
1173
|
-
async
|
|
1661
|
+
async reuseCachedManifestEntry(persistence, workspace, descriptor, workspacePosition, sessionId, firstSnapshot, pinned, current, stateIndex) {
|
|
1662
|
+
if (!current || current.deleted || !current.workspaceOrigin) return void 0;
|
|
1663
|
+
const cache = this.exportCache.get(sessionExportCacheKey(String(sessionId)));
|
|
1664
|
+
if (!cache || cache.metadata.contentDigest !== current.contentDigest || cache.metadata.originInstanceId !== current.originInstanceId || cache.metadata.originEntityId !== current.originEntityId || current.workspaceOrigin.originInstanceId !== descriptor.originInstanceId || current.workspaceOrigin.originEntityId !== descriptor.originWorkspaceId || current.stateDigest !== sessionStateDigest(descriptor, workspacePosition, false, pinned)) return void 0;
|
|
1665
|
+
const stable = await readStableSessionMetadata(persistence, sessionId, firstSnapshot);
|
|
1666
|
+
if (!stable || !stable.header.cwd || posix.normalize(stable.header.cwd) !== posix.normalize(workspace.path)) return void 0;
|
|
1667
|
+
const parentMetadata = stable.header.parentSession ? stateIndex.recordsByLocalId.get(String(stable.header.parentSession)) : void 0;
|
|
1668
|
+
const parentOrigin = parentMetadata ? {
|
|
1669
|
+
originInstanceId: parentMetadata.originInstanceId,
|
|
1670
|
+
originEntityId: parentMetadata.originEntityId
|
|
1671
|
+
} : void 0;
|
|
1672
|
+
const instructionSnapshot = instructionsForSession(this.instructions, String(sessionId), stateIndex, {
|
|
1673
|
+
originInstanceId: descriptor.originInstanceId,
|
|
1674
|
+
originEntityId: descriptor.originWorkspaceId
|
|
1675
|
+
});
|
|
1676
|
+
const fingerprint = sessionExportFingerprint({
|
|
1677
|
+
revision: stable.snapshot.revision,
|
|
1678
|
+
header: stable.header,
|
|
1679
|
+
inheritedEventCount: stable.inheritedEventCount,
|
|
1680
|
+
workspace: descriptor,
|
|
1681
|
+
instructionSnapshot,
|
|
1682
|
+
parentOrigin
|
|
1683
|
+
});
|
|
1684
|
+
if (!fingerprint || fingerprint !== cache.fingerprint) return void 0;
|
|
1685
|
+
return {
|
|
1686
|
+
metadata: current,
|
|
1687
|
+
eventCount: cache.eventCount,
|
|
1688
|
+
eventDigest: cache.eventDigest,
|
|
1689
|
+
inheritedEventCount: cache.inheritedEventCount
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
async exportBundleNow(options = {}) {
|
|
1693
|
+
const { manifestOnly = false, selectedKeys, sharedObjects: useSharedObjects = false } = options;
|
|
1174
1694
|
const persistence = this.requirePersistence();
|
|
1175
1695
|
const workspaceRegistry = this.getWorkspaces();
|
|
1176
1696
|
if (!workspaceRegistry) throw new SyncSessionError("workspace-unavailable", "工作区注册表不可用");
|
|
1177
1697
|
await persistence.flush();
|
|
1698
|
+
const stateIndex = createSessionStateIndex(this.state.read());
|
|
1178
1699
|
const instanceId = this.state.instanceId();
|
|
1179
1700
|
const bindings = this.bindings.list();
|
|
1701
|
+
const bindingByMirrorPath = /* @__PURE__ */ new Map();
|
|
1702
|
+
for (const binding of bindings) {
|
|
1703
|
+
const key = normalize(binding.mirrorPath);
|
|
1704
|
+
if (!bindingByMirrorPath.has(key)) bindingByMirrorPath.set(key, binding);
|
|
1705
|
+
}
|
|
1180
1706
|
const workspaces = workspaceRegistry.list();
|
|
1181
1707
|
const archivedIds = new Set(workspaceRegistry.archivedSessionIds.map(String));
|
|
1182
1708
|
const pinnedIds = new Set((workspaceRegistry.pinnedSessionIds ?? []).map(String));
|
|
@@ -1184,10 +1710,33 @@ var SyncSessionService = class {
|
|
|
1184
1710
|
const skipped = [];
|
|
1185
1711
|
const eligibleLocalIds = /* @__PURE__ */ new Set();
|
|
1186
1712
|
const workspaceById = /* @__PURE__ */ new Map();
|
|
1187
|
-
const archivedBranchSessionIds = new Set(Object.values(
|
|
1713
|
+
const archivedBranchSessionIds = new Set(Object.values(stateIndex.state.archives).map((archive) => archive.localSessionId));
|
|
1714
|
+
const objectCache = /* @__PURE__ */ new Map();
|
|
1715
|
+
const descriptorCache = /* @__PURE__ */ new Map();
|
|
1716
|
+
const getWorkspaceDescriptor = (workspace, binding, nodeOrigin, preferredOrigin) => {
|
|
1717
|
+
const key = canonicalJson({
|
|
1718
|
+
workspace: {
|
|
1719
|
+
id: String(workspace.id),
|
|
1720
|
+
path: workspace.path,
|
|
1721
|
+
title: workspace.title
|
|
1722
|
+
},
|
|
1723
|
+
binding: {
|
|
1724
|
+
mirrorPath: normalize(binding.mirrorPath),
|
|
1725
|
+
nodeId: binding.nodeId,
|
|
1726
|
+
remotePath: posix.normalize(binding.remotePath)
|
|
1727
|
+
},
|
|
1728
|
+
nodeOrigin,
|
|
1729
|
+
preferredOrigin
|
|
1730
|
+
});
|
|
1731
|
+
const cached = descriptorCache.get(key);
|
|
1732
|
+
if (cached) return cached;
|
|
1733
|
+
const descriptor = this.workspaceDescriptor(workspace, binding, nodeOrigin, preferredOrigin, stateIndex);
|
|
1734
|
+
descriptorCache.set(key, descriptor);
|
|
1735
|
+
return descriptor;
|
|
1736
|
+
};
|
|
1188
1737
|
for (const workspace of workspaces) {
|
|
1189
1738
|
workspaceById.set(String(workspace.id), workspace);
|
|
1190
|
-
const binding =
|
|
1739
|
+
const binding = bindingByMirrorPath.get(workspaceCandidateKey(workspace));
|
|
1191
1740
|
if (!binding) continue;
|
|
1192
1741
|
const node = this.registry.get(binding.nodeId);
|
|
1193
1742
|
if (!node || !isNodeRemoteType(node.type)) continue;
|
|
@@ -1196,15 +1745,43 @@ var SyncSessionService = class {
|
|
|
1196
1745
|
for (let position = 0; position < workspace.sessionIds.length; position += 1) {
|
|
1197
1746
|
const sessionId = String(workspace.sessionIds[position]);
|
|
1198
1747
|
if (archivedBranchSessionIds.has(sessionId)) continue;
|
|
1199
|
-
const currentSession =
|
|
1748
|
+
const currentSession = stateIndex.recordsByLocalId.get(sessionId);
|
|
1200
1749
|
if (currentSession?.deleted) continue;
|
|
1201
|
-
const
|
|
1750
|
+
const localKey = currentSession ? syncSessionEntryKey({ metadata: currentSession }) : syncRecordKey("session", {
|
|
1751
|
+
originInstanceId: instanceId,
|
|
1752
|
+
originEntityId: sessionId
|
|
1753
|
+
});
|
|
1754
|
+
if (selectedKeys && !selectedKeys.has(localKey)) continue;
|
|
1755
|
+
const descriptor = getWorkspaceDescriptor(workspace, binding, nodeOrigin, currentSession?.workspaceOrigin);
|
|
1202
1756
|
const snapshot = await persistence.stat(SessionId(sessionId));
|
|
1203
1757
|
if (!snapshot) continue;
|
|
1204
1758
|
if (snapshot.formatStatus === "migration-required") throw new SyncSessionError("session-format", "会话格式需要由当前 DSH 版本迁移");
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1759
|
+
if (manifestOnly) {
|
|
1760
|
+
const cached = await this.reuseCachedManifestEntry(persistence, workspace, descriptor, position, SessionId(sessionId), snapshot, pinnedIds.has(sessionId), currentSession, stateIndex);
|
|
1761
|
+
if (cached) {
|
|
1762
|
+
entries.push(cached);
|
|
1763
|
+
eligibleLocalIds.add(sessionId);
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
const exported = await this.exportSessionEntry(persistence, workspace, descriptor, position, SessionId(sessionId), snapshot, archivedIds.has(sessionId), pinnedIds.has(sessionId), void 0, stateIndex, objectCache);
|
|
1768
|
+
if (exported) {
|
|
1769
|
+
const { entry } = exported;
|
|
1770
|
+
if (manifestOnly && options.payloadCache) {
|
|
1771
|
+
const key = syncSessionEntryKey(entry);
|
|
1772
|
+
const size = estimateJsonBytes(entry, MAX_MANIFEST_PAYLOAD_CACHE_BYTES - options.payloadCache.bytes);
|
|
1773
|
+
if (size !== void 0) {
|
|
1774
|
+
options.payloadCache.entries.set(key, entry);
|
|
1775
|
+
options.payloadCache.bytes += size;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
entries.push(manifestOnly ? {
|
|
1779
|
+
metadata: entry.metadata,
|
|
1780
|
+
...entry.branchId ? { branchId: entry.branchId } : {},
|
|
1781
|
+
eventCount: exported.eventCount,
|
|
1782
|
+
eventDigest: exported.eventDigest,
|
|
1783
|
+
inheritedEventCount: exported.inheritedEventCount
|
|
1784
|
+
} : entry);
|
|
1208
1785
|
eligibleLocalIds.add(sessionId);
|
|
1209
1786
|
} else skipped.push({
|
|
1210
1787
|
sessionId,
|
|
@@ -1212,11 +1789,15 @@ var SyncSessionService = class {
|
|
|
1212
1789
|
});
|
|
1213
1790
|
}
|
|
1214
1791
|
}
|
|
1215
|
-
const initial =
|
|
1792
|
+
const initial = stateIndex.state;
|
|
1216
1793
|
for (const metadata of Object.values(initial.records)) {
|
|
1217
1794
|
if (metadata.kind !== "session" || archivedBranchSessionIds.has(metadata.localEntityId)) continue;
|
|
1795
|
+
if (selectedKeys && !selectedKeys.has(syncSessionEntryKey({ metadata }))) continue;
|
|
1218
1796
|
if (metadata.deleted) {
|
|
1219
1797
|
entries.push({ metadata });
|
|
1798
|
+
const tombstoneKey = sessionExportCacheKey(metadata.localEntityId);
|
|
1799
|
+
this.exportCache.delete(tombstoneKey);
|
|
1800
|
+
this.exportCacheStore.delete(tombstoneKey);
|
|
1220
1801
|
continue;
|
|
1221
1802
|
}
|
|
1222
1803
|
if (eligibleLocalIds.has(metadata.localEntityId)) continue;
|
|
@@ -1229,46 +1810,83 @@ var SyncSessionService = class {
|
|
|
1229
1810
|
this.state.update((state) => {
|
|
1230
1811
|
state.records[sessionOriginKey(metadata)] = tombstone;
|
|
1231
1812
|
});
|
|
1813
|
+
indexSessionRecord(stateIndex, tombstone);
|
|
1814
|
+
const tombstoneKey = sessionExportCacheKey(metadata.localEntityId);
|
|
1815
|
+
this.exportCache.delete(tombstoneKey);
|
|
1816
|
+
this.exportCacheStore.delete(tombstoneKey);
|
|
1232
1817
|
entries.push({ metadata: tombstone });
|
|
1233
1818
|
}
|
|
1234
|
-
for (const archive of
|
|
1819
|
+
for (const archive of Object.values(stateIndex.state.archives)) {
|
|
1820
|
+
if (selectedKeys && !selectedKeys.has(archive.branchId)) continue;
|
|
1235
1821
|
const workspace = archive.workspaceId ? workspaceById.get(archive.workspaceId) : void 0;
|
|
1236
1822
|
if (!workspace) continue;
|
|
1237
|
-
const binding =
|
|
1823
|
+
const binding = bindingByMirrorPath.get(workspaceCandidateKey(workspace));
|
|
1238
1824
|
const node = binding ? this.registry.get(binding.nodeId) : void 0;
|
|
1239
1825
|
if (!binding || !node || !isNodeRemoteType(node.type)) continue;
|
|
1240
1826
|
const nodeOrigin = this.nodes.originForLocalNode(node.id);
|
|
1241
1827
|
if (!nodeOrigin) continue;
|
|
1242
|
-
const
|
|
1243
|
-
const descriptor = this.workspaceDescriptor(workspace, binding, nodeOrigin, archivedMetadata?.workspaceOrigin);
|
|
1828
|
+
const descriptor = getWorkspaceDescriptor(workspace, binding, nodeOrigin, stateIndex.archivesByLocalSessionId.get(archive.localSessionId)?.workspaceOrigin);
|
|
1244
1829
|
const snapshot = await persistence.stat(SessionId(archive.localSessionId));
|
|
1245
1830
|
if (!snapshot) continue;
|
|
1246
|
-
const
|
|
1247
|
-
if (
|
|
1248
|
-
|
|
1831
|
+
const exported = await this.exportSessionEntry(persistence, workspace, descriptor, archive.workspacePosition, SessionId(archive.localSessionId), snapshot, true, false, archive, stateIndex, objectCache);
|
|
1832
|
+
if (exported) {
|
|
1833
|
+
const { entry } = exported;
|
|
1834
|
+
if (manifestOnly && options.payloadCache) {
|
|
1835
|
+
const key = syncSessionEntryKey(entry);
|
|
1836
|
+
const size = estimateJsonBytes(entry, MAX_MANIFEST_PAYLOAD_CACHE_BYTES - options.payloadCache.bytes);
|
|
1837
|
+
if (size !== void 0) {
|
|
1838
|
+
options.payloadCache.entries.set(key, entry);
|
|
1839
|
+
options.payloadCache.bytes += size;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
entries.push(manifestOnly ? {
|
|
1843
|
+
metadata: entry.metadata,
|
|
1844
|
+
...entry.branchId ? { branchId: entry.branchId } : {},
|
|
1845
|
+
eventCount: exported.eventCount,
|
|
1846
|
+
eventDigest: exported.eventDigest,
|
|
1847
|
+
inheritedEventCount: exported.inheritedEventCount
|
|
1848
|
+
} : entry);
|
|
1849
|
+
} else skipped.push({
|
|
1249
1850
|
sessionId: archive.localSessionId,
|
|
1250
1851
|
reason: "archive-workspace-cwd-mismatch"
|
|
1251
1852
|
});
|
|
1252
1853
|
}
|
|
1253
|
-
|
|
1854
|
+
const bundleSharedObjects = useSharedObjects && entries.some((entry) => (entry.objects?.length ?? 0) > 0) ? this.dedupeBundleObjects(entries) : void 0;
|
|
1855
|
+
this.exportCacheStore.flush();
|
|
1856
|
+
return makeSyncSessionBundle(instanceId, entries, skipped, bundleSharedObjects);
|
|
1857
|
+
}
|
|
1858
|
+
dedupeBundleObjects(entries) {
|
|
1859
|
+
const shared = /* @__PURE__ */ new Map();
|
|
1860
|
+
for (const entry of entries) for (const object of entry.objects ?? []) {
|
|
1861
|
+
const current = shared.get(object.id);
|
|
1862
|
+
if (current === void 0) shared.set(object.id, object);
|
|
1863
|
+
else if (current.digest !== object.digest || current.bytes !== object.bytes) throw new SyncSessionError("object-invalid", "the same object id carries different payloads");
|
|
1864
|
+
}
|
|
1865
|
+
return [...shared.values()];
|
|
1254
1866
|
}
|
|
1255
1867
|
async applyBundleNow(value, sourceName, peerId, options, recoveryBatch) {
|
|
1256
|
-
const
|
|
1257
|
-
const
|
|
1868
|
+
const transferredBundle = validateSyncSessionBundle(value);
|
|
1869
|
+
const importedVia = recoveryBatch?.importedVia ?? options.importedVia ?? "peer";
|
|
1870
|
+
if (importedVia !== "peer" && transferredBundle.entries.some((entry) => entry.eventDelta)) throw new SyncSessionError("manifest-invalid", "event tail deltas are only accepted from sync peers");
|
|
1871
|
+
let pendingBatch = recoveryBatch ?? this.journal.findBatch(peerId, transferredBundle.digest);
|
|
1872
|
+
if (pendingBatch && !transferredBundle.entries.some((entry) => entry.eventDelta) && (pendingBatch.bundleDigest !== transferredBundle.digest || pendingBatch.peerId !== peerId)) throw new SyncSessionError("journal-bundle-mismatch", "会话 journal batch 与 bundle 不匹配");
|
|
1873
|
+
if (!pendingBatch) await this.exportBundleNow({ manifestOnly: true });
|
|
1874
|
+
const bundle = await this.expandEventDeltas(transferredBundle);
|
|
1875
|
+
pendingBatch ??= recoveryBatch ?? this.journal.findBatch(peerId, bundle.digest);
|
|
1258
1876
|
if (pendingBatch && (pendingBatch.bundleDigest !== bundle.digest || pendingBatch.peerId !== peerId)) throw new SyncSessionError("journal-bundle-mismatch", "会话 journal batch 与 bundle 不匹配");
|
|
1259
|
-
const
|
|
1877
|
+
const resolvedImportedVia = pendingBatch?.importedVia ?? importedVia;
|
|
1260
1878
|
const allowSameInstance = pendingBatch?.allowSameInstance ?? options.allowSameInstance ?? false;
|
|
1261
1879
|
const localInstanceId = this.state.instanceId();
|
|
1262
1880
|
if (bundle.instanceId === localInstanceId && !allowSameInstance) throw new SyncSessionError("same-instance", "cannot import sessions from this instance");
|
|
1263
|
-
if (!pendingBatch) await this.exportBundleNow();
|
|
1264
1881
|
this.preflightBundle(bundle, {
|
|
1265
|
-
importedVia,
|
|
1882
|
+
importedVia: resolvedImportedVia,
|
|
1266
1883
|
allowSameInstance
|
|
1267
1884
|
});
|
|
1268
1885
|
const safeSourceName = (pendingBatch?.sourceName ?? sourceName).replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 128) || "sync peer";
|
|
1269
1886
|
const recordKeys = bundle.entries.map((entry) => entry.branchId ?? sessionOriginKey(entry.metadata));
|
|
1270
|
-
const batchId = pendingBatch?.batchId ?? this.journal.beginWithBundle(peerId, safeSourceName, bundle.digest, recordKeys, bundle, new Date(this.now()).toISOString(),
|
|
1887
|
+
const batchId = pendingBatch?.batchId ?? this.journal.beginWithBundle(peerId, safeSourceName, bundle.digest, recordKeys, bundle, new Date(this.now()).toISOString(), resolvedImportedVia, allowSameInstance);
|
|
1271
1888
|
const result = emptyApplyResult();
|
|
1889
|
+
const attachmentImportCache = /* @__PURE__ */ new Map();
|
|
1272
1890
|
result.skipped += bundle.skipped.length;
|
|
1273
1891
|
result.unmapped += bundle.skipped.length;
|
|
1274
1892
|
const workspaceOrders = /* @__PURE__ */ new Map();
|
|
@@ -1364,7 +1982,7 @@ var SyncSessionService = class {
|
|
|
1364
1982
|
continue;
|
|
1365
1983
|
}
|
|
1366
1984
|
if (!current.deleted && entry.metadata.contentDigest === current.contentDigest && await this.requirePersistence().stat(SessionId(current.localEntityId))) {
|
|
1367
|
-
await this.applyMembershipOnly(entry, current, safeSourceName, entryWorkspace,
|
|
1985
|
+
await this.applyMembershipOnly(entry, current, safeSourceName, entryWorkspace, resolvedImportedVia);
|
|
1368
1986
|
result.updated += 1;
|
|
1369
1987
|
queueWorkspaceOrder(entryWorkspace.id, SessionId(current.localEntityId), entry.workspacePosition ?? 0);
|
|
1370
1988
|
continue;
|
|
@@ -1374,7 +1992,7 @@ var SyncSessionService = class {
|
|
|
1374
1992
|
let prepared;
|
|
1375
1993
|
if (preparedValue === void 0) {
|
|
1376
1994
|
try {
|
|
1377
|
-
prepared = await this.prepareEntry(entry, safeSourceName, plannedSessionIds, entryWorkspace,
|
|
1995
|
+
prepared = await this.prepareEntry(entry, safeSourceName, plannedSessionIds, entryWorkspace, resolvedImportedVia, attachmentImportCache);
|
|
1378
1996
|
} catch (error) {
|
|
1379
1997
|
if (error instanceof SyncSessionError && error.code === "workspace-unmapped") {
|
|
1380
1998
|
result.skipped += 1;
|
|
@@ -1399,13 +2017,42 @@ var SyncSessionService = class {
|
|
|
1399
2017
|
}
|
|
1400
2018
|
await this.orderWorkspaceSessions(workspaceOrders);
|
|
1401
2019
|
this.journal.complete(batchId);
|
|
2020
|
+
this.exportCacheStore.flush();
|
|
1402
2021
|
return result;
|
|
1403
2022
|
} catch (error) {
|
|
1404
2023
|
if (error instanceof SyncSessionError) throw error;
|
|
1405
2024
|
throw new SyncSessionError("session-apply-failed", "会话同步应用失败", error);
|
|
1406
2025
|
}
|
|
1407
2026
|
}
|
|
1408
|
-
async
|
|
2027
|
+
async expandEventDeltas(bundle) {
|
|
2028
|
+
if (!bundle.entries.some((entry) => entry.eventDelta)) return bundle;
|
|
2029
|
+
const persistence = this.requirePersistence();
|
|
2030
|
+
const state = this.state.read();
|
|
2031
|
+
const entries = [];
|
|
2032
|
+
for (const entry of bundle.entries) {
|
|
2033
|
+
const delta = entry.eventDelta;
|
|
2034
|
+
if (!delta) {
|
|
2035
|
+
entries.push(entry);
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
const current = state.records[sessionOriginKey(entry.metadata)];
|
|
2039
|
+
if (!current || current.deleted) throw new SyncSessionError("session-delta-prefix", "event delta base session is missing");
|
|
2040
|
+
const snapshot = await persistence.stat(SessionId(current.localEntityId));
|
|
2041
|
+
if (!snapshot) throw new SyncSessionError("session-delta-prefix", "event delta base session is missing");
|
|
2042
|
+
const stable = await this.readStableSession(persistence, SessionId(current.localEntityId), snapshot);
|
|
2043
|
+
if (stable.events.length !== delta.baseEventCount || stable.inheritedEventCount !== delta.baseInheritedEventCount || sessionEventListDigest(stable.events) !== delta.baseEventDigest) throw new SyncSessionError("session-delta-prefix", "event delta base prefix no longer matches");
|
|
2044
|
+
const events = [...stable.events, ...entry.events ?? []];
|
|
2045
|
+
if (events.length !== delta.fullEventCount || sessionEventListDigest(events) !== delta.fullEventDigest) throw new SyncSessionError("manifest-invalid", "expanded event delta digest does not match");
|
|
2046
|
+
const fullEntry = {
|
|
2047
|
+
...entry,
|
|
2048
|
+
events
|
|
2049
|
+
};
|
|
2050
|
+
delete fullEntry.eventDelta;
|
|
2051
|
+
entries.push(fullEntry);
|
|
2052
|
+
}
|
|
2053
|
+
return validateSyncSessionBundle(makeSyncSessionBundle(bundle.instanceId, entries, bundle.skipped));
|
|
2054
|
+
}
|
|
2055
|
+
async prepareEntry(entry, sourceName, plannedSessionIds, workspace, importedVia, attachmentImportCache) {
|
|
1409
2056
|
if (!entry.header || !entry.events || !entry.workspace || !entry.objects) throw new SyncSessionError("manifest-invalid", "active session entry is incomplete");
|
|
1410
2057
|
const sessionId = entry.branchId ? archivedSessionId(entry.branchId) : importedSessionId(entry.metadata);
|
|
1411
2058
|
const sourceHeader = entry.header;
|
|
@@ -1430,7 +2077,7 @@ var SyncSessionService = class {
|
|
|
1430
2077
|
await handle.close();
|
|
1431
2078
|
}
|
|
1432
2079
|
}
|
|
1433
|
-
const objectMap = await this.importObjects(entry.objects, sessionId);
|
|
2080
|
+
const objectMap = await this.importObjects(entry.objects, sessionId, attachmentImportCache);
|
|
1434
2081
|
const validatedEvents = validateStoredEvents(header, this.rewriteEvents(entry.events, entry.objects, objectMap, sessionId, String(entry.header.id)));
|
|
1435
2082
|
assertContiguous(sessionId, validatedEvents, 0);
|
|
1436
2083
|
const instructionWorkspaceId = entry.instructionSnapshot?.workspaceOrigin ? this.localWorkspaceIdForOrigin(entry.instructionSnapshot.workspaceOrigin, entry.instructionSnapshot.workspaceRemotePath) : void 0;
|
|
@@ -1516,7 +2163,7 @@ var SyncSessionService = class {
|
|
|
1516
2163
|
if (!committedWorkspace) throw new SyncSessionError("workspace-unmapped", "目标实例没有对应的可用远程节点");
|
|
1517
2164
|
return committedWorkspace;
|
|
1518
2165
|
}
|
|
1519
|
-
async importObjects(objects, sessionId) {
|
|
2166
|
+
async importObjects(objects, sessionId, attachmentImportCache) {
|
|
1520
2167
|
const attachments = objects.some((object) => object.kind === "image" || object.kind === "file") ? this.getAttachments() : void 0;
|
|
1521
2168
|
const spills = objects.some((object) => object.kind === "spill" && !object.unavailable) ? this.getSpills() : void 0;
|
|
1522
2169
|
if (objects.some((object) => object.kind === "image" || object.kind === "file") && !attachments) throw new SyncSessionError("attachments-unavailable", "目标实例的附件存储服务不可用");
|
|
@@ -1524,6 +2171,13 @@ var SyncSessionService = class {
|
|
|
1524
2171
|
const output = /* @__PURE__ */ new Map();
|
|
1525
2172
|
for (const object of objects) {
|
|
1526
2173
|
if (object.kind === "spill" && object.unavailable) continue;
|
|
2174
|
+
if (object.kind !== "spill") {
|
|
2175
|
+
const cached = attachmentImportCache?.get(object.id);
|
|
2176
|
+
if (cached) {
|
|
2177
|
+
output.set(object.id, cached);
|
|
2178
|
+
continue;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
1527
2181
|
const data = arrayBuffer(object.data);
|
|
1528
2182
|
if (data.byteLength !== object.bytes || sha256(data) !== object.digest) throw new SyncSessionError("object-integrity", "session object failed digest validation");
|
|
1529
2183
|
if (object.kind === "image") {
|
|
@@ -1533,12 +2187,14 @@ var SyncSessionService = class {
|
|
|
1533
2187
|
...object.ref.name === void 0 ? {} : { name: object.ref.name }
|
|
1534
2188
|
});
|
|
1535
2189
|
output.set(object.id, ref);
|
|
2190
|
+
attachmentImportCache?.set(object.id, ref);
|
|
1536
2191
|
} else if (object.kind === "file") {
|
|
1537
2192
|
const ref = await attachments.saveFileStream({
|
|
1538
2193
|
data: oneChunk(data),
|
|
1539
2194
|
name: object.ref.name
|
|
1540
2195
|
});
|
|
1541
2196
|
output.set(object.id, ref);
|
|
2197
|
+
attachmentImportCache?.set(object.id, ref);
|
|
1542
2198
|
} else {
|
|
1543
2199
|
const ref = await saveSpillTransfer(spills, {
|
|
1544
2200
|
owner: { sessionId },
|
|
@@ -1930,10 +2586,11 @@ var SyncSessionService = class {
|
|
|
1930
2586
|
}
|
|
1931
2587
|
return existed !== void 0;
|
|
1932
2588
|
}
|
|
1933
|
-
workspaceDescriptor(workspace, binding, nodeOrigin, preferredOrigin) {
|
|
1934
|
-
const state =
|
|
1935
|
-
const
|
|
1936
|
-
const
|
|
2589
|
+
workspaceDescriptor(workspace, binding, nodeOrigin, preferredOrigin, stateIndex = createSessionStateIndex(this.state.read())) {
|
|
2590
|
+
const state = stateIndex.state;
|
|
2591
|
+
const workspaceMappings = stateIndex.workspacesByLocalId.get(String(workspace.id)) ?? [];
|
|
2592
|
+
const matchingMappings = workspaceMappings.filter((mapping) => mapping.localNodeId === binding.nodeId && posix.normalize(mapping.remotePath) === posix.normalize(binding.remotePath));
|
|
2593
|
+
const prior = (preferredOrigin ? workspaceMappings.find((mapping) => mapping.originInstanceId === preferredOrigin.originInstanceId && mapping.originWorkspaceId === preferredOrigin.originEntityId && mapping.localNodeId === binding.nodeId) : void 0) ?? (matchingMappings.length === 1 ? matchingMappings[0] : void 0);
|
|
1937
2594
|
const originInstanceId = preferredOrigin?.originInstanceId ?? prior?.originInstanceId ?? this.state.instanceId();
|
|
1938
2595
|
const originWorkspaceId = preferredOrigin?.originEntityId ?? prior?.originWorkspaceId ?? workspace.id;
|
|
1939
2596
|
const base = {
|
|
@@ -1948,35 +2605,52 @@ var SyncSessionService = class {
|
|
|
1948
2605
|
const current = state.workspaces[key];
|
|
1949
2606
|
const revision = current && current.contentDigest === digest ? current.revision : this.state.nextRevision(this.now());
|
|
1950
2607
|
const mapping = {
|
|
1951
|
-
|
|
2608
|
+
originInstanceId,
|
|
2609
|
+
originWorkspaceId,
|
|
1952
2610
|
localWorkspaceId: workspace.id,
|
|
1953
2611
|
localPath: workspace.path,
|
|
1954
2612
|
localNodeId: binding.nodeId,
|
|
2613
|
+
remotePath: base.remotePath,
|
|
2614
|
+
title: workspace.title,
|
|
1955
2615
|
revision,
|
|
1956
2616
|
contentDigest: digest
|
|
1957
2617
|
};
|
|
1958
|
-
|
|
1959
|
-
draft
|
|
1960
|
-
|
|
2618
|
+
if (!current || canonicalJson(current) !== canonicalJson(mapping)) {
|
|
2619
|
+
this.state.update((draft) => {
|
|
2620
|
+
draft.workspaces[key] = mapping;
|
|
2621
|
+
});
|
|
2622
|
+
indexWorkspaceMapping(stateIndex, key, mapping);
|
|
2623
|
+
}
|
|
1961
2624
|
return {
|
|
1962
2625
|
...base,
|
|
1963
2626
|
revision,
|
|
1964
2627
|
contentDigest: digest
|
|
1965
2628
|
};
|
|
1966
2629
|
}
|
|
1967
|
-
async exportSessionEntry(persistence, workspace, descriptor, workspacePosition, sessionId, firstSnapshot, archived, pinned, archive) {
|
|
1968
|
-
const { header, inheritedEventCount, events } = await this.readStableSession(persistence, sessionId, firstSnapshot);
|
|
1969
|
-
if (!header.cwd || posix.normalize(header.cwd) !== posix.normalize(workspace.path)) return void 0;
|
|
2630
|
+
async exportSessionEntry(persistence, workspace, descriptor, workspacePosition, sessionId, firstSnapshot, archived, pinned, archive, stateIndex, objectCache) {
|
|
2631
|
+
const { header, inheritedEventCount, events, revision: sourceRevision } = await this.readStableSession(persistence, sessionId, firstSnapshot);
|
|
1970
2632
|
const localSessionId = String(sessionId);
|
|
1971
|
-
const
|
|
1972
|
-
|
|
1973
|
-
|
|
2633
|
+
const cacheKey = sessionExportCacheKey(localSessionId);
|
|
2634
|
+
if (!header.cwd || posix.normalize(header.cwd) !== posix.normalize(workspace.path)) {
|
|
2635
|
+
this.exportCache.delete(cacheKey);
|
|
2636
|
+
this.exportCacheStore.delete(cacheKey);
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
const index = stateIndex ?? createSessionStateIndex(this.state.read());
|
|
2640
|
+
const state = index.state;
|
|
2641
|
+
const parentMetadata = header.parentSession ? index.recordsByLocalId.get(String(header.parentSession)) : void 0;
|
|
2642
|
+
const parentOrigin = parentMetadata ? {
|
|
2643
|
+
originInstanceId: parentMetadata.originInstanceId,
|
|
2644
|
+
originEntityId: parentMetadata.originEntityId
|
|
2645
|
+
} : void 0;
|
|
2646
|
+
const syncInstructions = instructionsForSession(this.instructions, String(sessionId), index, {
|
|
1974
2647
|
originInstanceId: descriptor.originInstanceId,
|
|
1975
2648
|
originEntityId: descriptor.originWorkspaceId
|
|
1976
2649
|
});
|
|
1977
|
-
const
|
|
2650
|
+
const eventDigest = sessionEventListDigest(events);
|
|
2651
|
+
const objects = await this.exportObjects(events, localSessionId, objectCache);
|
|
1978
2652
|
const digest = sessionEventDigest(header, inheritedEventCount, events, descriptor, syncInstructions, objects, parentOrigin);
|
|
1979
|
-
const current = archive ? void 0 :
|
|
2653
|
+
const current = archive ? void 0 : index.recordsByLocalId.get(localSessionId) ?? state.records[syncRecordKey("session", {
|
|
1980
2654
|
originInstanceId: this.state.instanceId(),
|
|
1981
2655
|
originEntityId: localSessionId
|
|
1982
2656
|
})];
|
|
@@ -2015,6 +2689,7 @@ var SyncSessionService = class {
|
|
|
2015
2689
|
delete draft.archives[archive.branchId];
|
|
2016
2690
|
draft.archives[branchId] = updatedArchive;
|
|
2017
2691
|
});
|
|
2692
|
+
indexArchive(index, archive.branchId, updatedArchive);
|
|
2018
2693
|
outputBranchId = branchId;
|
|
2019
2694
|
} else contentDigest = archive.contentDigest;
|
|
2020
2695
|
} else if (!current || current.contentDigest !== digest || current.deleted || workspaceOriginChanged || !current.workspaceOrigin || sessionStateChanged) revision = this.state.nextRevision(this.now());
|
|
@@ -2030,23 +2705,60 @@ var SyncSessionService = class {
|
|
|
2030
2705
|
...archive ? {} : current?.importedVia ? { importedVia: current.importedVia } : {},
|
|
2031
2706
|
...archive ? {} : current?.sourceName ? { sourceName: current.sourceName } : {}
|
|
2032
2707
|
};
|
|
2033
|
-
if (!archive && (!current || current.contentDigest !== digest || current.deleted || workspaceOriginChanged || !current.workspaceOrigin || sessionStateChanged))
|
|
2034
|
-
|
|
2035
|
-
|
|
2708
|
+
if (!archive && (!current || current.contentDigest !== digest || current.deleted || workspaceOriginChanged || !current.workspaceOrigin || sessionStateChanged)) {
|
|
2709
|
+
this.state.update((draft) => {
|
|
2710
|
+
draft.records[sessionOriginKey(metadata)] = metadata;
|
|
2711
|
+
});
|
|
2712
|
+
indexSessionRecord(index, metadata);
|
|
2713
|
+
}
|
|
2714
|
+
if (!archive && !objects.some((object) => object.kind === "spill")) {
|
|
2715
|
+
const fingerprint = sessionExportFingerprint({
|
|
2716
|
+
revision: sourceRevision,
|
|
2717
|
+
header,
|
|
2718
|
+
inheritedEventCount,
|
|
2719
|
+
workspace: descriptor,
|
|
2720
|
+
instructionSnapshot: syncInstructions,
|
|
2721
|
+
parentOrigin
|
|
2722
|
+
});
|
|
2723
|
+
if (fingerprint) {
|
|
2724
|
+
const entry = {
|
|
2725
|
+
fingerprint,
|
|
2726
|
+
metadata,
|
|
2727
|
+
eventCount: events.length,
|
|
2728
|
+
eventDigest,
|
|
2729
|
+
inheritedEventCount
|
|
2730
|
+
};
|
|
2731
|
+
this.exportCache.set(cacheKey, entry);
|
|
2732
|
+
this.exportCacheStore.set(cacheKey, entry);
|
|
2733
|
+
} else {
|
|
2734
|
+
this.exportCache.delete(cacheKey);
|
|
2735
|
+
this.exportCacheStore.delete(cacheKey);
|
|
2736
|
+
}
|
|
2737
|
+
} else {
|
|
2738
|
+
this.exportCache.delete(cacheKey);
|
|
2739
|
+
this.exportCacheStore.delete(cacheKey);
|
|
2740
|
+
}
|
|
2036
2741
|
return {
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
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
|
+
},
|
|
2759
|
+
eventCount: events.length,
|
|
2760
|
+
eventDigest,
|
|
2761
|
+
inheritedEventCount
|
|
2050
2762
|
};
|
|
2051
2763
|
}
|
|
2052
2764
|
async readStableSession(persistence, sessionId, firstSnapshot) {
|
|
@@ -2069,10 +2781,11 @@ var SyncSessionService = class {
|
|
|
2069
2781
|
assertContiguous(sessionId, validated, 0);
|
|
2070
2782
|
const after = await persistence.stat(sessionId);
|
|
2071
2783
|
if (!after) throw new SyncSessionError("session-missing", "会话在导出期间消失");
|
|
2072
|
-
if (
|
|
2784
|
+
if (canonicalJson(snapshot.revision) === canonicalJson(after.revision) && canonicalJson(snapshot.header) === canonicalJson(after.header) && canonicalJson(handle.header) === canonicalJson(after.header)) return {
|
|
2073
2785
|
header: handle.header,
|
|
2074
2786
|
inheritedEventCount: Number(handle.inheritedEventCount),
|
|
2075
|
-
events: [...validated]
|
|
2787
|
+
events: [...validated],
|
|
2788
|
+
revision: after.revision
|
|
2076
2789
|
};
|
|
2077
2790
|
snapshot = after;
|
|
2078
2791
|
} finally {
|
|
@@ -2081,7 +2794,7 @@ var SyncSessionService = class {
|
|
|
2081
2794
|
}
|
|
2082
2795
|
throw new SyncSessionError("session-changing", "会话在导出期间持续变化,请稍后重试");
|
|
2083
2796
|
}
|
|
2084
|
-
async exportObjects(events, sessionId) {
|
|
2797
|
+
async exportObjects(events, sessionId, objectCache) {
|
|
2085
2798
|
const attachments = attachmentReferences(events);
|
|
2086
2799
|
const spills = spillReferences(events);
|
|
2087
2800
|
const structuredSpills = new Set(spills.map(spillKey));
|
|
@@ -2090,27 +2803,43 @@ var SyncSessionService = class {
|
|
|
2090
2803
|
const attachmentStore = attachments.images.length + attachments.files.length > 0 ? this.getAttachments() : void 0;
|
|
2091
2804
|
if (attachments.images.length + attachments.files.length > 0 && !attachmentStore) throw new SyncSessionError("attachments-unavailable", "附件存储服务不可用");
|
|
2092
2805
|
for (const ref of attachments.images) {
|
|
2806
|
+
const key = attachmentKey("image", ref);
|
|
2807
|
+
const cached = objectCache?.get(key)?.object;
|
|
2808
|
+
if (cached?.kind === "image" && canonicalJson(cached.ref) === canonicalJson(ref)) {
|
|
2809
|
+
payloads.set(key, cached);
|
|
2810
|
+
continue;
|
|
2811
|
+
}
|
|
2093
2812
|
const stored = await attachmentStore.readImage(ref);
|
|
2094
2813
|
const digest = sha256(stored.data);
|
|
2095
|
-
|
|
2096
|
-
id:
|
|
2814
|
+
const object = {
|
|
2815
|
+
id: key,
|
|
2097
2816
|
kind: "image",
|
|
2098
2817
|
ref: stored.ref,
|
|
2099
2818
|
bytes: stored.data.byteLength,
|
|
2100
2819
|
digest,
|
|
2101
2820
|
data: Buffer.from(stored.data).toString("base64")
|
|
2102
|
-
}
|
|
2821
|
+
};
|
|
2822
|
+
payloads.set(key, object);
|
|
2823
|
+
objectCache?.set(key, { object });
|
|
2103
2824
|
}
|
|
2104
2825
|
for (const ref of attachments.files) {
|
|
2826
|
+
const key = attachmentKey("file", ref);
|
|
2827
|
+
const cached = objectCache?.get(key)?.object;
|
|
2828
|
+
if (cached?.kind === "file" && canonicalJson(cached.ref) === canonicalJson(ref)) {
|
|
2829
|
+
payloads.set(key, cached);
|
|
2830
|
+
continue;
|
|
2831
|
+
}
|
|
2105
2832
|
const data = await collectBytes(attachmentStore.readFileStream(ref));
|
|
2106
|
-
|
|
2107
|
-
id:
|
|
2833
|
+
const object = {
|
|
2834
|
+
id: key,
|
|
2108
2835
|
kind: "file",
|
|
2109
2836
|
ref,
|
|
2110
2837
|
bytes: data.byteLength,
|
|
2111
2838
|
digest: sha256(data),
|
|
2112
2839
|
data: data.toString("base64")
|
|
2113
|
-
}
|
|
2840
|
+
};
|
|
2841
|
+
payloads.set(key, object);
|
|
2842
|
+
objectCache?.set(key, { object });
|
|
2114
2843
|
}
|
|
2115
2844
|
if (spills.length > 0) {
|
|
2116
2845
|
const store = this.getSpills();
|
|
@@ -2120,6 +2849,12 @@ var SyncSessionService = class {
|
|
|
2120
2849
|
untransferred.push(reference);
|
|
2121
2850
|
continue;
|
|
2122
2851
|
}
|
|
2852
|
+
const key = spillKey(reference);
|
|
2853
|
+
const cached = objectCache?.get(key)?.object;
|
|
2854
|
+
if (cached?.kind === "spill" && canonicalJson(cached.reference) === canonicalJson(reference) && (reference.ref.bytes === void 0 || cached.bytes === reference.ref.bytes)) {
|
|
2855
|
+
payloads.set(key, cached);
|
|
2856
|
+
continue;
|
|
2857
|
+
}
|
|
2123
2858
|
try {
|
|
2124
2859
|
const data = await collectBytes(readSpillStream(store, reference));
|
|
2125
2860
|
if (reference.ref.bytes !== void 0 && data.byteLength !== reference.ref.bytes) {
|
|
@@ -2132,15 +2867,16 @@ var SyncSessionService = class {
|
|
|
2132
2867
|
untransferred.push(reference);
|
|
2133
2868
|
continue;
|
|
2134
2869
|
}
|
|
2135
|
-
const
|
|
2136
|
-
payloads.set(key, {
|
|
2870
|
+
const object = {
|
|
2137
2871
|
id: key,
|
|
2138
2872
|
kind: "spill",
|
|
2139
2873
|
reference,
|
|
2140
2874
|
bytes: data.byteLength,
|
|
2141
2875
|
digest: sha256(data),
|
|
2142
2876
|
data: data.toString("base64")
|
|
2143
|
-
}
|
|
2877
|
+
};
|
|
2878
|
+
payloads.set(key, object);
|
|
2879
|
+
objectCache?.set(key, { object });
|
|
2144
2880
|
} catch {
|
|
2145
2881
|
untransferred.push(reference);
|
|
2146
2882
|
}
|
|
@@ -2172,6 +2908,6 @@ var SyncSessionService = class {
|
|
|
2172
2908
|
}
|
|
2173
2909
|
};
|
|
2174
2910
|
//#endregion
|
|
2175
|
-
export {
|
|
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 };
|
|
2176
2912
|
|
|
2177
|
-
//# sourceMappingURL=session-sync-
|
|
2913
|
+
//# sourceMappingURL=session-sync-C0uoizyB.js.map
|