@remnic/core 1.1.22 → 1.1.24
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/dist/access-cli.js +15 -15
- package/dist/access-http.d.ts +9 -1
- package/dist/access-http.js +9 -9
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +8 -8
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-DT9L2DW4.d.ts → access-service-CEyV8XJ5.d.ts} +19 -2
- package/dist/access-service.d.ts +1 -1
- package/dist/access-service.js +6 -6
- package/dist/briefing.js +3 -3
- package/dist/causal-consolidation.js +4 -4
- package/dist/{chunk-YO3AZEE5.js → chunk-25YQM6XW.js} +3 -3
- package/dist/{chunk-LDJANWTK.js → chunk-2DM72JF3.js} +12 -12
- package/dist/{chunk-TLM762GT.js → chunk-2WIPXV3Y.js} +2 -2
- package/dist/{chunk-QOHBYVZG.js → chunk-3F24QTRI.js} +2 -2
- package/dist/{chunk-5IQC4OG6.js → chunk-4H6DURG6.js} +2 -2
- package/dist/{chunk-26OQECWH.js → chunk-6CB4E7ZV.js} +4 -4
- package/dist/{chunk-NOQ74SJN.js → chunk-7D6O46PF.js} +2 -2
- package/dist/{chunk-FF46Q3SN.js → chunk-AMVN77EU.js} +360 -32
- package/dist/chunk-AMVN77EU.js.map +1 -0
- package/dist/{chunk-7Q2P774N.js → chunk-F33CJ5CH.js} +13 -3
- package/dist/chunk-F33CJ5CH.js.map +1 -0
- package/dist/{chunk-FSODDMR2.js → chunk-IANK6Y5W.js} +2 -2
- package/dist/{chunk-UA6OCL6S.js → chunk-JUYT2J3K.js} +106 -11
- package/dist/chunk-JUYT2J3K.js.map +1 -0
- package/dist/{chunk-NGPO6S3M.js → chunk-LCTP7YRU.js} +42 -5
- package/dist/chunk-LCTP7YRU.js.map +1 -0
- package/dist/{chunk-GGCJ253V.js → chunk-MVAOT247.js} +8 -8
- package/dist/{chunk-SH5S7XYD.js → chunk-MXFBBHJU.js} +72 -2
- package/dist/chunk-MXFBBHJU.js.map +1 -0
- package/dist/{chunk-VMQRBXJ5.js → chunk-NW7JW5GA.js} +2 -2
- package/dist/{chunk-SZKCBLS5.js → chunk-PUXCIHRL.js} +2 -2
- package/dist/{chunk-2IRT26RZ.js → chunk-QYHQ2JHL.js} +2 -2
- package/dist/{chunk-CN4P6SVA.js → chunk-RCZRL5BE.js} +2 -2
- package/dist/{chunk-SGIXDVSF.js → chunk-S27EXIHY.js} +2 -2
- package/dist/{chunk-5ML4TH3E.js → chunk-TFORLO3O.js} +4 -4
- package/dist/{chunk-TOFUTKQN.js → chunk-TR4DK5OH.js} +2 -2
- package/dist/{chunk-6ORWKANA.js → chunk-VYU7PXUS.js} +2 -2
- package/dist/{chunk-FFU4GMST.js → chunk-WNARATI3.js} +2 -2
- package/dist/{chunk-KSFBM6TV.js → chunk-YITUHONZ.js} +2 -2
- package/dist/{cli-BN0CkYzI.d.ts → cli-BguVmIwO.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +18 -18
- package/dist/compounding/engine.js +3 -3
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/index.js +3 -3
- package/dist/entity-retrieval.js +3 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.js +30 -24
- package/dist/index.js.map +1 -1
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/storage.js +3 -3
- package/dist/offline-sync.d.ts +38 -1
- package/dist/offline-sync.js +8 -2
- package/dist/operator-toolkit.js +6 -6
- package/dist/orchestrator.js +11 -11
- package/dist/schemas.d.ts +22 -22
- package/dist/secure-store/index.js +2 -2
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +2 -2
- package/dist/transfer/types.d.ts +12 -12
- package/dist/verified-recall.js +3 -3
- package/package.json +1 -1
- package/src/access-http.test.ts +239 -0
- package/src/access-http.ts +128 -7
- package/src/access-service-offline-file-content.test.ts +37 -0
- package/src/access-service.ts +70 -0
- package/src/index.ts +4 -0
- package/src/offline-sync.test.ts +395 -79
- package/src/offline-sync.ts +473 -32
- package/src/secure-store/secure-fs.ts +84 -3
- package/src/storage.ts +12 -0
- package/dist/chunk-7Q2P774N.js.map +0 -1
- package/dist/chunk-FF46Q3SN.js.map +0 -1
- package/dist/chunk-NGPO6S3M.js.map +0 -1
- package/dist/chunk-SH5S7XYD.js.map +0 -1
- package/dist/chunk-UA6OCL6S.js.map +0 -1
- /package/dist/{chunk-YO3AZEE5.js.map → chunk-25YQM6XW.js.map} +0 -0
- /package/dist/{chunk-LDJANWTK.js.map → chunk-2DM72JF3.js.map} +0 -0
- /package/dist/{chunk-TLM762GT.js.map → chunk-2WIPXV3Y.js.map} +0 -0
- /package/dist/{chunk-QOHBYVZG.js.map → chunk-3F24QTRI.js.map} +0 -0
- /package/dist/{chunk-5IQC4OG6.js.map → chunk-4H6DURG6.js.map} +0 -0
- /package/dist/{chunk-26OQECWH.js.map → chunk-6CB4E7ZV.js.map} +0 -0
- /package/dist/{chunk-NOQ74SJN.js.map → chunk-7D6O46PF.js.map} +0 -0
- /package/dist/{chunk-FSODDMR2.js.map → chunk-IANK6Y5W.js.map} +0 -0
- /package/dist/{chunk-GGCJ253V.js.map → chunk-MVAOT247.js.map} +0 -0
- /package/dist/{chunk-VMQRBXJ5.js.map → chunk-NW7JW5GA.js.map} +0 -0
- /package/dist/{chunk-SZKCBLS5.js.map → chunk-PUXCIHRL.js.map} +0 -0
- /package/dist/{chunk-2IRT26RZ.js.map → chunk-QYHQ2JHL.js.map} +0 -0
- /package/dist/{chunk-CN4P6SVA.js.map → chunk-RCZRL5BE.js.map} +0 -0
- /package/dist/{chunk-SGIXDVSF.js.map → chunk-S27EXIHY.js.map} +0 -0
- /package/dist/{chunk-5ML4TH3E.js.map → chunk-TFORLO3O.js.map} +0 -0
- /package/dist/{chunk-TOFUTKQN.js.map → chunk-TR4DK5OH.js.map} +0 -0
- /package/dist/{chunk-6ORWKANA.js.map → chunk-VYU7PXUS.js.map} +0 -0
- /package/dist/{chunk-FFU4GMST.js.map → chunk-WNARATI3.js.map} +0 -0
- /package/dist/{chunk-KSFBM6TV.js.map → chunk-YITUHONZ.js.map} +0 -0
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import {
|
|
11
11
|
MAGIC_HEADER_SIZE,
|
|
12
12
|
isEncryptedFile
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-MXFBBHJU.js";
|
|
14
14
|
import {
|
|
15
15
|
parseFlexibleIsoTimestamp
|
|
16
16
|
} from "./chunk-P7FMDTKL.js";
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
readdir,
|
|
25
25
|
readFile,
|
|
26
26
|
rename,
|
|
27
|
+
rm,
|
|
27
28
|
stat,
|
|
28
29
|
unlink,
|
|
29
30
|
writeFile
|
|
@@ -33,34 +34,13 @@ var OFFLINE_SYNC_SNAPSHOT_FORMAT = "remnic.offline-sync.snapshot.v1";
|
|
|
33
34
|
var OFFLINE_SYNC_CHANGESET_FORMAT = "remnic.offline-sync.changeset.v1";
|
|
34
35
|
var OFFLINE_SYNC_STATE_VERSION = 1;
|
|
35
36
|
var OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES = 64 * 1024 * 1024;
|
|
37
|
+
var OFFLINE_SYNC_FILE_CONTENT_TRANSFER_CHUNK_BYTES = 8 * 1024 * 1024;
|
|
38
|
+
var OFFLINE_SYNC_APPLY_MAX_BODY_BYTES = 16 * 1024 * 1024;
|
|
36
39
|
var SYNC_INTERNAL_DIR = ".offline-sync";
|
|
40
|
+
var OFFLINE_SYNC_UPLOAD_STAGING_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
37
41
|
var EXCLUDED_FILE_NAMES = /* @__PURE__ */ new Set([
|
|
38
42
|
".sync-state.json"
|
|
39
43
|
]);
|
|
40
|
-
var DERIVED_RUNTIME_STATE_BASENAMES = /* @__PURE__ */ new Set([
|
|
41
|
-
".artifact-write-version.log",
|
|
42
|
-
".memory-status-version.log",
|
|
43
|
-
"fact-hashes.ready",
|
|
44
|
-
"fact-hashes.txt",
|
|
45
|
-
"buffer-surprise-ledger.jsonl",
|
|
46
|
-
"buffer.json",
|
|
47
|
-
"embeddings.json",
|
|
48
|
-
"entity-mention-index.json",
|
|
49
|
-
"index_tags.json",
|
|
50
|
-
"index_time.json",
|
|
51
|
-
"last_graph_recall.json",
|
|
52
|
-
"last_intent.json",
|
|
53
|
-
"last_qmd_recall.json",
|
|
54
|
-
"last_recall.json",
|
|
55
|
-
"lcm.sqlite",
|
|
56
|
-
"lcm.sqlite-shm",
|
|
57
|
-
"lcm.sqlite-wal",
|
|
58
|
-
"memory-lifecycle-ledger.jsonl",
|
|
59
|
-
"memory-projection.sqlite",
|
|
60
|
-
"memory-projection.sqlite-shm",
|
|
61
|
-
"memory-projection.sqlite-wal",
|
|
62
|
-
"recall_impressions.jsonl"
|
|
63
|
-
]);
|
|
64
44
|
var EXCLUDED_FILE_PREFIXES = [
|
|
65
45
|
".remnic-sync.",
|
|
66
46
|
".remnic-sync-state."
|
|
@@ -281,7 +261,6 @@ function shouldExcludeRelPath(relPosix, includeTranscripts) {
|
|
|
281
261
|
const parts = relPosix.split("/");
|
|
282
262
|
if (parts.some((part) => DEFAULT_TRANSFER_EXCLUDE_DIRS.has(part))) return true;
|
|
283
263
|
if (parts.some((part) => part === SYNC_INTERNAL_DIR)) return true;
|
|
284
|
-
if (isDerivedRuntimeStatePath(parts)) return true;
|
|
285
264
|
if (!includeTranscripts && parts[0] === "transcripts") return true;
|
|
286
265
|
const basename = parts[parts.length - 1] ?? "";
|
|
287
266
|
if (isCanonicalRuntimeStatePath(parts) && basename.includes(".tmp-")) return true;
|
|
@@ -291,11 +270,7 @@ function shouldExcludeRelPath(relPosix, includeTranscripts) {
|
|
|
291
270
|
function shouldIgnoreIncomingRuntimePath(relPosix) {
|
|
292
271
|
const parts = relPosix.split("/");
|
|
293
272
|
const basename = parts[parts.length - 1] ?? "";
|
|
294
|
-
return
|
|
295
|
-
}
|
|
296
|
-
function isDerivedRuntimeStatePath(parts) {
|
|
297
|
-
const basename = parts[parts.length - 1] ?? "";
|
|
298
|
-
return isCanonicalRuntimeStatePath(parts) && DERIVED_RUNTIME_STATE_BASENAMES.has(basename);
|
|
273
|
+
return isCanonicalRuntimeStatePath(parts) && basename.includes(".tmp-");
|
|
299
274
|
}
|
|
300
275
|
function isCanonicalRuntimeStatePath(parts) {
|
|
301
276
|
if (parts[0] === "state") return true;
|
|
@@ -477,6 +452,9 @@ async function readOfflineSyncFileContentChunk(options) {
|
|
|
477
452
|
}
|
|
478
453
|
async function buildOfflineSyncChangeset(options) {
|
|
479
454
|
const includeTranscripts = options.includeTranscripts !== false;
|
|
455
|
+
const excludedPaths = new Set(
|
|
456
|
+
(options.excludePaths ?? []).map((relPath) => normalizeRelativePath(relPath, "excludePaths[]"))
|
|
457
|
+
);
|
|
480
458
|
const base = byPath(filterBaseFilesForMode(
|
|
481
459
|
normalizeFileStates(options.baseFiles),
|
|
482
460
|
includeTranscripts
|
|
@@ -492,6 +470,7 @@ async function buildOfflineSyncChangeset(options) {
|
|
|
492
470
|
const currentMap = byPath(current.files);
|
|
493
471
|
const changes = [];
|
|
494
472
|
for (const relPath of unionPaths(base, currentMap)) {
|
|
473
|
+
if (excludedPaths.has(relPath)) continue;
|
|
495
474
|
const baseEntry = base.get(relPath);
|
|
496
475
|
const currentEntry = currentMap.get(relPath);
|
|
497
476
|
if (currentEntry && currentEntry.sha256 !== baseEntry?.sha256) {
|
|
@@ -874,6 +853,352 @@ async function writeSafeFile(root, relPath, content, writeFileHook) {
|
|
|
874
853
|
throw error;
|
|
875
854
|
}
|
|
876
855
|
}
|
|
856
|
+
async function applyOfflineSyncFileContentChunk(options) {
|
|
857
|
+
const root = await ensureSyncRoot(options.root, "applyOfflineSyncFileContentChunk");
|
|
858
|
+
const sourceId = normalizeSourceId(options.sourceId, "sourceId");
|
|
859
|
+
const relPath = normalizeRelativePath(options.path, "path");
|
|
860
|
+
const includeTranscripts = options.includeTranscripts !== false;
|
|
861
|
+
if (shouldExcludeRelPath(relPath, includeTranscripts)) {
|
|
862
|
+
throw new Error(`offline sync file content path is excluded: ${relPath}`);
|
|
863
|
+
}
|
|
864
|
+
const sha256 = assertSha256(options.sha256, "sha256");
|
|
865
|
+
const bytes = assertNonNegativeInteger(options.bytes, "bytes");
|
|
866
|
+
const mtimeMs = assertNonNegativeFinite(options.mtimeMs, "mtimeMs");
|
|
867
|
+
const offset = options.offset === void 0 ? 0 : assertNonNegativeInteger(options.offset, "offset");
|
|
868
|
+
const baseSha256 = options.baseSha256 === void 0 ? void 0 : assertSha256(options.baseSha256, "baseSha256");
|
|
869
|
+
if (!Buffer.isBuffer(options.content)) {
|
|
870
|
+
throw new Error("content must be a Buffer");
|
|
871
|
+
}
|
|
872
|
+
if (options.content.length > OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES) {
|
|
873
|
+
throw new Error(
|
|
874
|
+
`content chunk must be ${OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES} bytes or fewer`
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
if (bytes > 0 && options.content.length === 0) {
|
|
878
|
+
throw new Error("content chunk must be non-empty before EOF");
|
|
879
|
+
}
|
|
880
|
+
if (offset > bytes || offset + options.content.length > bytes) {
|
|
881
|
+
throw new Error(`content chunk range exceeds declared file size for ${relPath}`);
|
|
882
|
+
}
|
|
883
|
+
if (options.writeFile && !options.writeFileChunks) {
|
|
884
|
+
throw new Error("offline sync upload storage hooks require writeFileChunks");
|
|
885
|
+
}
|
|
886
|
+
if (options.writeFile && !options.writeStagingFile) {
|
|
887
|
+
throw new Error("offline sync upload storage hooks require writeStagingFile");
|
|
888
|
+
}
|
|
889
|
+
if (offset === 0) {
|
|
890
|
+
await pruneOfflineUploadStaging(root);
|
|
891
|
+
}
|
|
892
|
+
const upload = await writeOfflineUploadChunk({
|
|
893
|
+
root,
|
|
894
|
+
sourceId,
|
|
895
|
+
relPath,
|
|
896
|
+
sha256,
|
|
897
|
+
bytes,
|
|
898
|
+
offset,
|
|
899
|
+
content: options.content,
|
|
900
|
+
readFile: options.readFile,
|
|
901
|
+
writeFile: options.writeFile,
|
|
902
|
+
writeStagingFile: options.writeStagingFile
|
|
903
|
+
});
|
|
904
|
+
const done = offset + options.content.length === bytes;
|
|
905
|
+
const baseResult = {
|
|
906
|
+
path: relPath,
|
|
907
|
+
sha256,
|
|
908
|
+
bytes,
|
|
909
|
+
mtimeMs,
|
|
910
|
+
offset,
|
|
911
|
+
chunkBytes: options.content.length,
|
|
912
|
+
done
|
|
913
|
+
};
|
|
914
|
+
if (!done) {
|
|
915
|
+
return {
|
|
916
|
+
...baseResult,
|
|
917
|
+
applied: false,
|
|
918
|
+
skipped: false
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
const digest = await digestOfflineUploadStagingContent({
|
|
922
|
+
root,
|
|
923
|
+
upload,
|
|
924
|
+
readFile: options.readFile
|
|
925
|
+
});
|
|
926
|
+
if (digest.sha256 !== sha256 || digest.bytes !== bytes) {
|
|
927
|
+
await cleanupOfflineUpload(upload).catch(() => {
|
|
928
|
+
});
|
|
929
|
+
throw new Error(`offline sync upload checksum mismatch for ${relPath}`);
|
|
930
|
+
}
|
|
931
|
+
const currentSnapshot = await buildOfflineSyncSnapshotForPaths({
|
|
932
|
+
root: root.abs,
|
|
933
|
+
sourceId: "local",
|
|
934
|
+
paths: [relPath],
|
|
935
|
+
includeContent: false,
|
|
936
|
+
includeTranscripts,
|
|
937
|
+
readFile: options.readFile
|
|
938
|
+
});
|
|
939
|
+
const currentFile = currentSnapshot.files[0];
|
|
940
|
+
const uploadedState = {
|
|
941
|
+
path: relPath,
|
|
942
|
+
sha256,
|
|
943
|
+
bytes,
|
|
944
|
+
mtimeMs
|
|
945
|
+
};
|
|
946
|
+
try {
|
|
947
|
+
if (currentFile?.sha256 === sha256) {
|
|
948
|
+
return {
|
|
949
|
+
...baseResult,
|
|
950
|
+
applied: false,
|
|
951
|
+
skipped: true,
|
|
952
|
+
currentFile: toFileState(currentFile)
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
if (!baseSha256 && currentFile) {
|
|
956
|
+
const conflict = await recordConflict({
|
|
957
|
+
root,
|
|
958
|
+
relPath,
|
|
959
|
+
reason: "remote_exists_for_local_create",
|
|
960
|
+
localSha256: currentFile.sha256,
|
|
961
|
+
incomingSha256: sha256,
|
|
962
|
+
writeConflictCopies: false,
|
|
963
|
+
sourceId,
|
|
964
|
+
writeFile: options.writeFile
|
|
965
|
+
});
|
|
966
|
+
return {
|
|
967
|
+
...baseResult,
|
|
968
|
+
applied: false,
|
|
969
|
+
skipped: false,
|
|
970
|
+
conflict,
|
|
971
|
+
currentFile: toFileState(currentFile)
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
if (baseSha256 && currentFile?.sha256 !== baseSha256) {
|
|
975
|
+
const conflict = await recordConflict({
|
|
976
|
+
root,
|
|
977
|
+
relPath,
|
|
978
|
+
reason: currentFile ? "remote_changed_for_local_update" : "remote_deleted_for_local_update",
|
|
979
|
+
baseSha256,
|
|
980
|
+
localSha256: currentFile?.sha256,
|
|
981
|
+
incomingSha256: sha256,
|
|
982
|
+
writeConflictCopies: false,
|
|
983
|
+
sourceId,
|
|
984
|
+
writeFile: options.writeFile
|
|
985
|
+
});
|
|
986
|
+
return {
|
|
987
|
+
...baseResult,
|
|
988
|
+
applied: false,
|
|
989
|
+
skipped: false,
|
|
990
|
+
conflict,
|
|
991
|
+
...currentFile ? { currentFile: toFileState(currentFile) } : {}
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
await writeSafeFileFromUpload(root, relPath, upload, options.readFile, options.writeFileChunks);
|
|
995
|
+
return {
|
|
996
|
+
...baseResult,
|
|
997
|
+
applied: true,
|
|
998
|
+
skipped: false,
|
|
999
|
+
currentFile: uploadedState
|
|
1000
|
+
};
|
|
1001
|
+
} finally {
|
|
1002
|
+
await cleanupOfflineUpload(upload).catch(() => {
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function offlineUploadRelPath(options) {
|
|
1007
|
+
const key = hashText([
|
|
1008
|
+
options.sourceId,
|
|
1009
|
+
options.relPath,
|
|
1010
|
+
options.sha256,
|
|
1011
|
+
String(options.bytes)
|
|
1012
|
+
].join("\0"));
|
|
1013
|
+
return `${SYNC_INTERNAL_DIR}/uploads/${key}.part`;
|
|
1014
|
+
}
|
|
1015
|
+
async function offlineUploadPath(root, options) {
|
|
1016
|
+
const relPath = offlineUploadRelPath(options);
|
|
1017
|
+
return {
|
|
1018
|
+
kind: "single",
|
|
1019
|
+
relPath,
|
|
1020
|
+
filePath: await resolveSafeArchiveTarget(root, relPath)
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
async function offlineUploadChunkPath(root, options) {
|
|
1024
|
+
const uploadRelPath = offlineUploadRelPath(options);
|
|
1025
|
+
const relPath = `${uploadRelPath}/${String(options.offset).padStart(20, "0")}.part`;
|
|
1026
|
+
return {
|
|
1027
|
+
kind: "chunks",
|
|
1028
|
+
relPath,
|
|
1029
|
+
filePath: await resolveSafeArchiveTarget(root, relPath)
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
async function writeOfflineUploadChunk(options) {
|
|
1033
|
+
if ((options.writeFile || options.writeStagingFile) && !options.readFile) {
|
|
1034
|
+
throw new Error("offline sync upload chunk storage hooks require readFile");
|
|
1035
|
+
}
|
|
1036
|
+
const uploadRoot = {
|
|
1037
|
+
...await offlineUploadPath(options.root, options),
|
|
1038
|
+
kind: "chunks"
|
|
1039
|
+
};
|
|
1040
|
+
if (options.offset === 0) {
|
|
1041
|
+
await rm(uploadRoot.filePath, { recursive: true, force: true }).catch(() => {
|
|
1042
|
+
});
|
|
1043
|
+
} else {
|
|
1044
|
+
const existing = await stat(uploadRoot.filePath).catch((error) => {
|
|
1045
|
+
if (error.code === "ENOENT") return null;
|
|
1046
|
+
throw error;
|
|
1047
|
+
});
|
|
1048
|
+
if (!existing || !existing.isDirectory()) {
|
|
1049
|
+
throw new Error(`offline sync upload is missing initial chunk for ${options.relPath}`);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const chunk = await offlineUploadChunkPath(options.root, { ...options, offset: options.offset });
|
|
1053
|
+
const writeStagingFile = options.writeStagingFile ?? options.writeFile;
|
|
1054
|
+
if (writeStagingFile) {
|
|
1055
|
+
await writeOfflineUploadContent({
|
|
1056
|
+
root: options.root,
|
|
1057
|
+
relPath: chunk.relPath,
|
|
1058
|
+
filePath: chunk.filePath,
|
|
1059
|
+
content: options.content,
|
|
1060
|
+
writeFile: writeStagingFile
|
|
1061
|
+
});
|
|
1062
|
+
return uploadRoot;
|
|
1063
|
+
}
|
|
1064
|
+
await mkdir(path.dirname(chunk.filePath), { recursive: true });
|
|
1065
|
+
const existingChunk = await lstat(chunk.filePath).catch((error) => {
|
|
1066
|
+
if (error.code === "ENOENT") return null;
|
|
1067
|
+
throw error;
|
|
1068
|
+
});
|
|
1069
|
+
if (existingChunk?.isSymbolicLink()) {
|
|
1070
|
+
throw new Error(`offline sync upload chunk is a symlink: ${chunk.relPath}`);
|
|
1071
|
+
}
|
|
1072
|
+
await writeFile(chunk.filePath, options.content, { mode: 384 });
|
|
1073
|
+
return uploadRoot;
|
|
1074
|
+
}
|
|
1075
|
+
async function pruneOfflineUploadStaging(root) {
|
|
1076
|
+
const uploadsRelPath = `${SYNC_INTERNAL_DIR}/uploads`;
|
|
1077
|
+
const uploadsPath = await resolveSafeArchiveTarget(root, uploadsRelPath);
|
|
1078
|
+
const entries = await readdir(uploadsPath, { withFileTypes: true }).catch((error) => {
|
|
1079
|
+
if (error.code === "ENOENT") return [];
|
|
1080
|
+
throw error;
|
|
1081
|
+
});
|
|
1082
|
+
const now = Date.now();
|
|
1083
|
+
await Promise.all(entries.map(async (entry) => {
|
|
1084
|
+
if (!/^[a-f0-9]{64}\.part$/i.test(entry.name)) return;
|
|
1085
|
+
const relPath = `${uploadsRelPath}/${entry.name}`;
|
|
1086
|
+
const filePath = await resolveSafeArchiveTarget(root, relPath);
|
|
1087
|
+
const info = await lstat(filePath).catch((error) => {
|
|
1088
|
+
if (error.code === "ENOENT") return null;
|
|
1089
|
+
throw error;
|
|
1090
|
+
});
|
|
1091
|
+
if (!info) return;
|
|
1092
|
+
if (now - info.mtimeMs <= OFFLINE_SYNC_UPLOAD_STAGING_MAX_AGE_MS) return;
|
|
1093
|
+
await rm(filePath, { recursive: true, force: true });
|
|
1094
|
+
}));
|
|
1095
|
+
}
|
|
1096
|
+
async function* readOfflineUploadStagingChunks(options) {
|
|
1097
|
+
if (options.upload.kind === "single") {
|
|
1098
|
+
yield await readOfflineUploadContent({
|
|
1099
|
+
root: options.root,
|
|
1100
|
+
relPath: options.upload.relPath,
|
|
1101
|
+
filePath: options.upload.filePath,
|
|
1102
|
+
readFile: options.readFile
|
|
1103
|
+
});
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
const entries = await readdir(options.upload.filePath);
|
|
1107
|
+
const chunkNames = entries.filter((entry) => /^\d{20}\.part$/.test(entry)).sort();
|
|
1108
|
+
if (chunkNames.length === 0) {
|
|
1109
|
+
throw new Error(`offline sync upload is missing chunks for ${options.upload.relPath}`);
|
|
1110
|
+
}
|
|
1111
|
+
let expectedOffset = 0;
|
|
1112
|
+
for (const chunkName of chunkNames) {
|
|
1113
|
+
const offset = Number(chunkName.slice(0, 20));
|
|
1114
|
+
if (!Number.isSafeInteger(offset) || offset !== expectedOffset) {
|
|
1115
|
+
throw new Error(
|
|
1116
|
+
`offline sync upload offset mismatch for ${options.upload.relPath}: expected ${expectedOffset}, got ${offset}`
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
const relPath = `${options.upload.relPath}/${chunkName}`;
|
|
1120
|
+
const filePath = await resolveSafeArchiveTarget(options.root, relPath);
|
|
1121
|
+
const content = await readOfflineUploadContent({
|
|
1122
|
+
root: options.root,
|
|
1123
|
+
relPath,
|
|
1124
|
+
filePath,
|
|
1125
|
+
readFile: options.readFile
|
|
1126
|
+
});
|
|
1127
|
+
expectedOffset += content.length;
|
|
1128
|
+
yield content;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
async function digestOfflineUploadStagingContent(options) {
|
|
1132
|
+
const hash = createHash("sha256");
|
|
1133
|
+
let bytes = 0;
|
|
1134
|
+
for await (const chunk of readOfflineUploadStagingChunks(options)) {
|
|
1135
|
+
hash.update(chunk);
|
|
1136
|
+
bytes += chunk.length;
|
|
1137
|
+
}
|
|
1138
|
+
return { sha256: hash.digest("hex"), bytes };
|
|
1139
|
+
}
|
|
1140
|
+
async function writeSafeFileFromUpload(root, relPath, upload, readFile2, writeFileChunks) {
|
|
1141
|
+
const target = await resolveSafeArchiveTarget(root, relPath);
|
|
1142
|
+
const chunks = readOfflineUploadStagingChunks({ root, upload, readFile: readFile2 });
|
|
1143
|
+
if (writeFileChunks) {
|
|
1144
|
+
await writeFileChunks({ root: root.abs, path: relPath, filePath: target, chunks });
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
1148
|
+
const tmp = path.join(
|
|
1149
|
+
path.dirname(target),
|
|
1150
|
+
`.remnic-sync.${process.pid}.${randomUUID()}.tmp`
|
|
1151
|
+
);
|
|
1152
|
+
const handle = await open(tmp, "w", 384);
|
|
1153
|
+
try {
|
|
1154
|
+
for await (const chunk of chunks) {
|
|
1155
|
+
if (chunk.length > 0) await handle.write(chunk);
|
|
1156
|
+
}
|
|
1157
|
+
await handle.close();
|
|
1158
|
+
const targetStat = await lstat(target).catch((error) => {
|
|
1159
|
+
if (error.code === "ENOENT") return null;
|
|
1160
|
+
throw error;
|
|
1161
|
+
});
|
|
1162
|
+
if (targetStat?.isSymbolicLink()) {
|
|
1163
|
+
throw new Error(`offline sync target is a symlink: ${relPath}`);
|
|
1164
|
+
}
|
|
1165
|
+
await rename(tmp, target);
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
await handle.close().catch(() => {
|
|
1168
|
+
});
|
|
1169
|
+
await unlink(tmp).catch(() => {
|
|
1170
|
+
});
|
|
1171
|
+
throw error;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
async function cleanupOfflineUpload(upload) {
|
|
1175
|
+
if (upload.kind === "chunks") {
|
|
1176
|
+
await rm(upload.filePath, { recursive: true, force: true });
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
await unlink(upload.filePath).catch((error) => {
|
|
1180
|
+
if (error.code === "ENOENT") return;
|
|
1181
|
+
throw error;
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
async function readOfflineUploadContent(options) {
|
|
1185
|
+
if (options.readFile) {
|
|
1186
|
+
return options.readFile({
|
|
1187
|
+
root: options.root.abs,
|
|
1188
|
+
path: options.relPath,
|
|
1189
|
+
filePath: options.filePath
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
return readFile(options.filePath);
|
|
1193
|
+
}
|
|
1194
|
+
async function writeOfflineUploadContent(options) {
|
|
1195
|
+
await options.writeFile({
|
|
1196
|
+
root: options.root.abs,
|
|
1197
|
+
path: options.relPath,
|
|
1198
|
+
filePath: options.filePath,
|
|
1199
|
+
content: options.content
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
877
1202
|
async function deleteSafeFile(root, relPath, deleteFile) {
|
|
878
1203
|
const target = await resolveSafeArchiveTarget(root, relPath);
|
|
879
1204
|
if (deleteFile) {
|
|
@@ -974,6 +1299,8 @@ export {
|
|
|
974
1299
|
OFFLINE_SYNC_CHANGESET_FORMAT,
|
|
975
1300
|
OFFLINE_SYNC_STATE_VERSION,
|
|
976
1301
|
OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
|
|
1302
|
+
OFFLINE_SYNC_FILE_CONTENT_TRANSFER_CHUNK_BYTES,
|
|
1303
|
+
OFFLINE_SYNC_APPLY_MAX_BODY_BYTES,
|
|
977
1304
|
normalizeOfflineSyncSnapshot,
|
|
978
1305
|
normalizeOfflineSyncChangeset,
|
|
979
1306
|
buildOfflineSyncSnapshot,
|
|
@@ -984,6 +1311,7 @@ export {
|
|
|
984
1311
|
summarizeOfflineSyncPendingChanges,
|
|
985
1312
|
applyOfflineSyncSnapshot,
|
|
986
1313
|
applyOfflineSyncChangeset,
|
|
1314
|
+
applyOfflineSyncFileContentChunk,
|
|
987
1315
|
defaultOfflineSyncStatePath,
|
|
988
1316
|
readOfflineSyncState,
|
|
989
1317
|
writeOfflineSyncState,
|
|
@@ -991,4 +1319,4 @@ export {
|
|
|
991
1319
|
normalizeOfflineSyncState,
|
|
992
1320
|
fileStatesFromSnapshot
|
|
993
1321
|
};
|
|
994
|
-
//# sourceMappingURL=chunk-
|
|
1322
|
+
//# sourceMappingURL=chunk-AMVN77EU.js.map
|