@remnic/core 1.1.15 → 1.1.16
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 +2 -2
- package/dist/access-http.d.ts +1 -1
- package/dist/access-http.js +5 -5
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +4 -4
- package/dist/access-schema.d.ts +17 -3
- package/dist/access-schema.js +3 -1
- package/dist/{access-service-BCMine1s.d.ts → access-service-DZXc7qwR.d.ts} +11 -1
- package/dist/access-service.d.ts +1 -1
- package/dist/access-service.js +2 -2
- package/dist/{chunk-GSP6ZKOY.js → chunk-2OZ6GP27.js} +81 -18
- package/dist/chunk-2OZ6GP27.js.map +1 -0
- package/dist/{chunk-VWFIQOTJ.js → chunk-66H2DZYB.js} +8 -1
- package/dist/chunk-66H2DZYB.js.map +1 -0
- package/dist/{chunk-ZYRMKWVW.js → chunk-HJILHQOR.js} +4 -4
- package/dist/{chunk-HJ2WMBFB.js → chunk-MTYLGYOQ.js} +15 -4
- package/dist/chunk-MTYLGYOQ.js.map +1 -0
- package/dist/{chunk-BNATB54A.js → chunk-SK42SSAN.js} +3 -3
- package/dist/{chunk-5D2G67ZQ.js → chunk-Y2YBRCEF.js} +29 -3
- package/dist/chunk-Y2YBRCEF.js.map +1 -0
- package/dist/{cli-B71zQ6XK.d.ts → cli-kVwab1_L.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +6 -6
- package/dist/index.d.ts +4 -4
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/offline-sync.d.ts +10 -1
- package/dist/offline-sync.js +3 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/transfer/types.d.ts +12 -12
- package/package.json +1 -1
- package/src/access-http.test.ts +73 -0
- package/src/access-http.ts +15 -0
- package/src/access-schema.ts +12 -0
- package/src/access-service-namespace.test.ts +64 -1
- package/src/access-service.ts +44 -0
- package/src/index.ts +1 -0
- package/src/offline-sync.test.ts +125 -0
- package/src/offline-sync.ts +107 -18
- package/dist/chunk-5D2G67ZQ.js.map +0 -1
- package/dist/chunk-GSP6ZKOY.js.map +0 -1
- package/dist/chunk-HJ2WMBFB.js.map +0 -1
- package/dist/chunk-VWFIQOTJ.js.map +0 -1
- /package/dist/{chunk-ZYRMKWVW.js.map → chunk-HJILHQOR.js.map} +0 -0
- /package/dist/{chunk-BNATB54A.js.map → chunk-SK42SSAN.js.map} +0 -0
package/src/access-service.ts
CHANGED
|
@@ -138,6 +138,7 @@ import {
|
|
|
138
138
|
import {
|
|
139
139
|
applyOfflineSyncChangeset,
|
|
140
140
|
buildOfflineSyncSnapshot,
|
|
141
|
+
buildOfflineSyncSnapshotForPaths,
|
|
141
142
|
type OfflineSyncApplyChangesetResult,
|
|
142
143
|
type OfflineSyncSnapshot,
|
|
143
144
|
} from "./offline-sync.js";
|
|
@@ -616,6 +617,13 @@ export interface EngramAccessOfflineSyncSnapshotRequest {
|
|
|
616
617
|
includeContent?: boolean;
|
|
617
618
|
}
|
|
618
619
|
|
|
620
|
+
export interface EngramAccessOfflineSyncFilesRequest {
|
|
621
|
+
namespace?: string;
|
|
622
|
+
principal?: string;
|
|
623
|
+
includeTranscripts?: boolean;
|
|
624
|
+
paths: string[];
|
|
625
|
+
}
|
|
626
|
+
|
|
619
627
|
export interface EngramAccessOfflineSyncApplyRequest {
|
|
620
628
|
namespace?: string;
|
|
621
629
|
principal?: string;
|
|
@@ -626,6 +634,10 @@ export interface EngramAccessOfflineSyncSnapshotResponse extends OfflineSyncSnap
|
|
|
626
634
|
namespace: string;
|
|
627
635
|
}
|
|
628
636
|
|
|
637
|
+
export interface EngramAccessOfflineSyncFilesResponse extends OfflineSyncSnapshot {
|
|
638
|
+
namespace: string;
|
|
639
|
+
}
|
|
640
|
+
|
|
629
641
|
export interface EngramAccessOfflineSyncApplyResponse extends OfflineSyncApplyChangesetResult {
|
|
630
642
|
namespace: string;
|
|
631
643
|
}
|
|
@@ -5568,6 +5580,38 @@ export class EngramAccessService {
|
|
|
5568
5580
|
};
|
|
5569
5581
|
}
|
|
5570
5582
|
|
|
5583
|
+
async offlineSyncFiles(
|
|
5584
|
+
options: EngramAccessOfflineSyncFilesRequest,
|
|
5585
|
+
): Promise<EngramAccessOfflineSyncFilesResponse> {
|
|
5586
|
+
const resolvedNamespace = this.resolveReadableNamespace(options.namespace, options.principal);
|
|
5587
|
+
const storage = await this.orchestrator.getStorage(resolvedNamespace);
|
|
5588
|
+
const storageHash = createHash("sha256").update(storage.dir).digest("hex").slice(0, 16);
|
|
5589
|
+
try {
|
|
5590
|
+
const snapshot = await buildOfflineSyncSnapshotForPaths({
|
|
5591
|
+
root: storage.dir,
|
|
5592
|
+
sourceId: `remnic:${resolvedNamespace}:${storageHash}`,
|
|
5593
|
+
paths: options.paths,
|
|
5594
|
+
includeContent: true,
|
|
5595
|
+
includeTranscripts: options.includeTranscripts !== false,
|
|
5596
|
+
readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
|
|
5597
|
+
});
|
|
5598
|
+
return {
|
|
5599
|
+
namespace: resolvedNamespace,
|
|
5600
|
+
...snapshot,
|
|
5601
|
+
};
|
|
5602
|
+
} catch (error) {
|
|
5603
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5604
|
+
if (
|
|
5605
|
+
message.startsWith("paths[]:") ||
|
|
5606
|
+
message.startsWith("buildOfflineSyncSnapshotForPaths: record path ") ||
|
|
5607
|
+
message.startsWith("offline sync snapshot path is excluded:")
|
|
5608
|
+
) {
|
|
5609
|
+
throw new EngramAccessInputError(message);
|
|
5610
|
+
}
|
|
5611
|
+
throw error;
|
|
5612
|
+
}
|
|
5613
|
+
}
|
|
5614
|
+
|
|
5571
5615
|
async offlineSyncApply(
|
|
5572
5616
|
options: EngramAccessOfflineSyncApplyRequest,
|
|
5573
5617
|
): Promise<EngramAccessOfflineSyncApplyResponse> {
|
package/src/index.ts
CHANGED
package/src/offline-sync.test.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
applyOfflineSyncSnapshot,
|
|
10
10
|
buildOfflineSyncChangeset,
|
|
11
11
|
buildOfflineSyncSnapshot,
|
|
12
|
+
buildOfflineSyncSnapshotForPaths,
|
|
12
13
|
} from "./offline-sync.js";
|
|
13
14
|
import { isEncryptedFile } from "./secure-store/secure-fs.js";
|
|
14
15
|
import { StorageManager } from "./storage.js";
|
|
@@ -106,6 +107,130 @@ test("offline changeset pushes local edits when the remote is still at the share
|
|
|
106
107
|
}
|
|
107
108
|
});
|
|
108
109
|
|
|
110
|
+
test("offline changeset only carries content for changed local files", async () => {
|
|
111
|
+
const local = await tempDir("remnic-offline-changeset-content");
|
|
112
|
+
try {
|
|
113
|
+
await write(local, "facts/unchanged.md", "same");
|
|
114
|
+
await write(local, "facts/changed.md", "before");
|
|
115
|
+
const base = await buildOfflineSyncSnapshot({
|
|
116
|
+
root: local,
|
|
117
|
+
sourceId: "remote",
|
|
118
|
+
includeContent: false,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await write(local, "facts/changed.md", "after");
|
|
122
|
+
await write(local, "facts/empty.md", "");
|
|
123
|
+
const changeset = await buildOfflineSyncChangeset({
|
|
124
|
+
root: local,
|
|
125
|
+
sourceId: "laptop",
|
|
126
|
+
baseFiles: base.files,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
assert.deepEqual(
|
|
130
|
+
changeset.changes.map((change) => change.path),
|
|
131
|
+
["facts/changed.md", "facts/empty.md"],
|
|
132
|
+
);
|
|
133
|
+
const empty = changeset.changes.find((change) => change.path === "facts/empty.md");
|
|
134
|
+
assert.equal(empty?.type, "upsert");
|
|
135
|
+
if (empty?.type === "upsert") {
|
|
136
|
+
assert.equal(empty.file.contentBase64, "");
|
|
137
|
+
}
|
|
138
|
+
assert.equal(JSON.stringify(changeset).includes("same"), false);
|
|
139
|
+
} finally {
|
|
140
|
+
await rm(local, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("offline pull accepts metadata-only snapshots when files are unchanged", async () => {
|
|
145
|
+
const remote = await tempDir("remnic-offline-metadata-remote");
|
|
146
|
+
const local = await tempDir("remnic-offline-metadata-local");
|
|
147
|
+
try {
|
|
148
|
+
await write(remote, "facts/shared.md", "base");
|
|
149
|
+
const initial = await buildOfflineSyncSnapshot({
|
|
150
|
+
root: remote,
|
|
151
|
+
sourceId: "remote",
|
|
152
|
+
includeContent: true,
|
|
153
|
+
});
|
|
154
|
+
const firstPull = await applyOfflineSyncSnapshot({
|
|
155
|
+
root: local,
|
|
156
|
+
snapshot: initial,
|
|
157
|
+
});
|
|
158
|
+
const metadataOnly = await buildOfflineSyncSnapshot({
|
|
159
|
+
root: remote,
|
|
160
|
+
sourceId: "remote",
|
|
161
|
+
includeContent: false,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const secondPull = await applyOfflineSyncSnapshot({
|
|
165
|
+
root: local,
|
|
166
|
+
snapshot: metadataOnly,
|
|
167
|
+
baseFiles: firstPull.nextBaseFiles,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
assert.equal(secondPull.conflicts.length, 0);
|
|
171
|
+
assert.equal(secondPull.upserted, 0);
|
|
172
|
+
assert.equal(secondPull.skipped, 1);
|
|
173
|
+
} finally {
|
|
174
|
+
await rm(remote, { recursive: true, force: true });
|
|
175
|
+
await rm(local, { recursive: true, force: true });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("offline pull applies snapshots with content only for remote-changed files", async () => {
|
|
180
|
+
const remote = await tempDir("remnic-offline-partial-remote");
|
|
181
|
+
const local = await tempDir("remnic-offline-partial-local");
|
|
182
|
+
try {
|
|
183
|
+
await write(remote, "facts/shared.md", "base");
|
|
184
|
+
await write(remote, "facts/stable.md", "unchanged");
|
|
185
|
+
const initial = await buildOfflineSyncSnapshot({
|
|
186
|
+
root: remote,
|
|
187
|
+
sourceId: "remote",
|
|
188
|
+
includeContent: true,
|
|
189
|
+
});
|
|
190
|
+
const firstPull = await applyOfflineSyncSnapshot({
|
|
191
|
+
root: local,
|
|
192
|
+
snapshot: initial,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await write(remote, "facts/shared.md", "remote edit");
|
|
196
|
+
const metadataOnly = await buildOfflineSyncSnapshot({
|
|
197
|
+
root: remote,
|
|
198
|
+
sourceId: "remote",
|
|
199
|
+
includeContent: false,
|
|
200
|
+
});
|
|
201
|
+
const changedContent = await buildOfflineSyncSnapshotForPaths({
|
|
202
|
+
root: remote,
|
|
203
|
+
sourceId: "remote",
|
|
204
|
+
paths: ["facts/shared.md"],
|
|
205
|
+
includeContent: true,
|
|
206
|
+
});
|
|
207
|
+
const contentByPath = new Map(
|
|
208
|
+
changedContent.files.map((file) => [file.path, file.contentBase64]),
|
|
209
|
+
);
|
|
210
|
+
const hydrated = {
|
|
211
|
+
...metadataOnly,
|
|
212
|
+
files: metadataOnly.files.map((file) => {
|
|
213
|
+
const contentBase64 = contentByPath.get(file.path);
|
|
214
|
+
return contentBase64 === undefined ? file : { ...file, contentBase64 };
|
|
215
|
+
}),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const secondPull = await applyOfflineSyncSnapshot({
|
|
219
|
+
root: local,
|
|
220
|
+
snapshot: hydrated,
|
|
221
|
+
baseFiles: firstPull.nextBaseFiles,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
assert.equal(secondPull.upserted, 1);
|
|
225
|
+
assert.equal(secondPull.conflicts.length, 0);
|
|
226
|
+
assert.equal(await readUtf8(local, "facts/shared.md"), "remote edit");
|
|
227
|
+
assert.equal(await readUtf8(local, "facts/stable.md"), "unchanged");
|
|
228
|
+
} finally {
|
|
229
|
+
await rm(remote, { recursive: true, force: true });
|
|
230
|
+
await rm(local, { recursive: true, force: true });
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
109
234
|
test("offline pull preserves local edits when both sides changed since the base", async () => {
|
|
110
235
|
const remote = await tempDir("remnic-offline-conflict-remote");
|
|
111
236
|
const local = await tempDir("remnic-offline-conflict-local");
|
package/src/offline-sync.ts
CHANGED
|
@@ -127,6 +127,14 @@ export interface OfflineSyncFileWriteTarget extends OfflineSyncFileTarget {
|
|
|
127
127
|
content: Buffer;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
interface OfflineSyncFileRecordOptions {
|
|
131
|
+
root: SafeArchiveRoot;
|
|
132
|
+
relPath: string;
|
|
133
|
+
filePath: string;
|
|
134
|
+
includeContent: boolean;
|
|
135
|
+
readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
|
|
136
|
+
}
|
|
137
|
+
|
|
130
138
|
const SYNC_INTERNAL_DIR = ".offline-sync";
|
|
131
139
|
const EXCLUDED_FILE_NAMES = new Set([
|
|
132
140
|
".sync-state.json",
|
|
@@ -406,6 +414,24 @@ function filterBaseFilesForMode(
|
|
|
406
414
|
return files.filter((file) => !shouldExcludeRelPath(file.path, includeTranscripts));
|
|
407
415
|
}
|
|
408
416
|
|
|
417
|
+
async function readOfflineSyncFileRecord(
|
|
418
|
+
options: OfflineSyncFileRecordOptions,
|
|
419
|
+
): Promise<OfflineSyncFileRecord> {
|
|
420
|
+
const relPath = validateArchiveRelativePath(options.relPath, "offlineSyncFile.path");
|
|
421
|
+
const bytes = options.readFile
|
|
422
|
+
? await options.readFile({ root: options.root.abs, path: relPath, filePath: options.filePath })
|
|
423
|
+
: await readFile(options.filePath);
|
|
424
|
+
const digest = sha256Buffer(bytes);
|
|
425
|
+
const st = await stat(options.filePath);
|
|
426
|
+
return {
|
|
427
|
+
path: relPath,
|
|
428
|
+
sha256: digest.sha256,
|
|
429
|
+
bytes: digest.bytes,
|
|
430
|
+
mtimeMs: st.mtimeMs,
|
|
431
|
+
...(options.includeContent ? { contentBase64: bytes.toString("base64") } : {}),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
409
435
|
export async function buildOfflineSyncSnapshot(options: {
|
|
410
436
|
root: string;
|
|
411
437
|
sourceId: string;
|
|
@@ -432,18 +458,13 @@ export async function buildOfflineSyncSnapshot(options: {
|
|
|
432
458
|
continue;
|
|
433
459
|
}
|
|
434
460
|
if (!entry.isFile()) continue;
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
:
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
sha256: digest.sha256,
|
|
443
|
-
bytes: digest.bytes,
|
|
444
|
-
mtimeMs: st.mtimeMs,
|
|
445
|
-
...(options.includeContent === true ? { contentBase64: bytes.toString("base64") } : {}),
|
|
446
|
-
});
|
|
461
|
+
files.push(await readOfflineSyncFileRecord({
|
|
462
|
+
root,
|
|
463
|
+
relPath: relPosix,
|
|
464
|
+
filePath: abs,
|
|
465
|
+
includeContent: options.includeContent === true,
|
|
466
|
+
readFile: options.readFile,
|
|
467
|
+
}));
|
|
447
468
|
}
|
|
448
469
|
}
|
|
449
470
|
|
|
@@ -459,6 +480,53 @@ export async function buildOfflineSyncSnapshot(options: {
|
|
|
459
480
|
};
|
|
460
481
|
}
|
|
461
482
|
|
|
483
|
+
export async function buildOfflineSyncSnapshotForPaths(options: {
|
|
484
|
+
root: string;
|
|
485
|
+
sourceId: string;
|
|
486
|
+
paths: readonly string[];
|
|
487
|
+
includeContent?: boolean;
|
|
488
|
+
includeTranscripts?: boolean;
|
|
489
|
+
now?: Date;
|
|
490
|
+
readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
|
|
491
|
+
}): Promise<OfflineSyncSnapshot> {
|
|
492
|
+
const rootAbs = path.resolve(options.root);
|
|
493
|
+
const root = await prepareSafeArchiveRoot(rootAbs, "buildOfflineSyncSnapshotForPaths", "root");
|
|
494
|
+
const includeTranscripts = options.includeTranscripts !== false;
|
|
495
|
+
const files: OfflineSyncFileRecord[] = [];
|
|
496
|
+
const seen = new Set<string>();
|
|
497
|
+
|
|
498
|
+
for (const rawPath of options.paths) {
|
|
499
|
+
const relPath = normalizeRelativePath(rawPath, "paths[]");
|
|
500
|
+
if (seen.has(relPath)) continue;
|
|
501
|
+
seen.add(relPath);
|
|
502
|
+
if (shouldExcludeRelPath(relPath, includeTranscripts)) {
|
|
503
|
+
throw new Error(`offline sync snapshot path is excluded: ${relPath}`);
|
|
504
|
+
}
|
|
505
|
+
const filePath = await resolveSafeArchiveTarget(root, relPath);
|
|
506
|
+
const st = await lstat(filePath).catch((error: unknown) => {
|
|
507
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
508
|
+
throw error;
|
|
509
|
+
});
|
|
510
|
+
if (!st || st.isSymbolicLink() || !st.isFile()) continue;
|
|
511
|
+
files.push(await readOfflineSyncFileRecord({
|
|
512
|
+
root,
|
|
513
|
+
relPath,
|
|
514
|
+
filePath,
|
|
515
|
+
includeContent: options.includeContent === true,
|
|
516
|
+
readFile: options.readFile,
|
|
517
|
+
}));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
|
|
522
|
+
schemaVersion: 1,
|
|
523
|
+
createdAt: (options.now ?? new Date()).toISOString(),
|
|
524
|
+
sourceId: normalizeSourceId(options.sourceId, "sourceId"),
|
|
525
|
+
includeTranscripts,
|
|
526
|
+
files: files.sort(compareByPath),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
462
530
|
export async function buildOfflineSyncChangeset(options: {
|
|
463
531
|
root: string;
|
|
464
532
|
sourceId: string;
|
|
@@ -475,7 +543,7 @@ export async function buildOfflineSyncChangeset(options: {
|
|
|
475
543
|
const current = await buildOfflineSyncSnapshot({
|
|
476
544
|
root: options.root,
|
|
477
545
|
sourceId: options.sourceId,
|
|
478
|
-
includeContent:
|
|
546
|
+
includeContent: false,
|
|
479
547
|
includeTranscripts,
|
|
480
548
|
now: options.now,
|
|
481
549
|
readFile: options.readFile,
|
|
@@ -487,11 +555,24 @@ export async function buildOfflineSyncChangeset(options: {
|
|
|
487
555
|
const baseEntry = base.get(relPath);
|
|
488
556
|
const currentEntry = currentMap.get(relPath);
|
|
489
557
|
if (currentEntry && currentEntry.sha256 !== baseEntry?.sha256) {
|
|
558
|
+
const file = await buildOfflineSyncSnapshotForPaths({
|
|
559
|
+
root: options.root,
|
|
560
|
+
sourceId: options.sourceId,
|
|
561
|
+
paths: [relPath],
|
|
562
|
+
includeContent: true,
|
|
563
|
+
includeTranscripts,
|
|
564
|
+
now: options.now,
|
|
565
|
+
readFile: options.readFile,
|
|
566
|
+
});
|
|
567
|
+
const record = file.files[0];
|
|
568
|
+
if (!record || typeof record.contentBase64 !== "string" || record.sha256 !== currentEntry.sha256) {
|
|
569
|
+
throw new Error(`offline sync file changed while building changeset: ${relPath}`);
|
|
570
|
+
}
|
|
490
571
|
changes.push({
|
|
491
572
|
type: "upsert",
|
|
492
573
|
path: relPath,
|
|
493
574
|
...(baseEntry ? { baseSha256: baseEntry.sha256 } : {}),
|
|
494
|
-
file:
|
|
575
|
+
file: record as OfflineSyncFileRecord & { contentBase64: string },
|
|
495
576
|
});
|
|
496
577
|
continue;
|
|
497
578
|
}
|
|
@@ -535,13 +616,15 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
535
616
|
writeFile?: (target: OfflineSyncFileWriteTarget) => Promise<void>;
|
|
536
617
|
deleteFile?: (target: OfflineSyncFileTarget) => Promise<void>;
|
|
537
618
|
}): Promise<OfflineSyncApplySnapshotResult> {
|
|
538
|
-
const snapshot = normalizeOfflineSyncSnapshot(options.snapshot
|
|
619
|
+
const snapshot = normalizeOfflineSyncSnapshot(options.snapshot);
|
|
539
620
|
const baseMap = byPath(filterBaseFilesForMode(
|
|
540
621
|
normalizeFileStates(options.baseFiles),
|
|
541
622
|
snapshot.includeTranscripts,
|
|
542
623
|
));
|
|
543
624
|
const incomingMap = byPath(snapshot.files);
|
|
544
|
-
const incomingBuffers = verifyRecordContents(snapshot.files, "offline sync snapshot"
|
|
625
|
+
const incomingBuffers = verifyRecordContents(snapshot.files, "offline sync snapshot", {
|
|
626
|
+
requireContent: false,
|
|
627
|
+
});
|
|
545
628
|
const root = await ensureSyncRoot(options.root, "applyOfflineSyncSnapshot");
|
|
546
629
|
const current = await buildOfflineSyncSnapshot({
|
|
547
630
|
root: root.abs,
|
|
@@ -582,7 +665,9 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
582
665
|
reason: "local_deleted_remote_modified",
|
|
583
666
|
baseSha256: base.sha256,
|
|
584
667
|
incomingSha256: incoming.sha256,
|
|
585
|
-
incomingBuffer:
|
|
668
|
+
incomingBuffer: options.writeConflictCopies === false
|
|
669
|
+
? incomingBuffers.get(relPath)
|
|
670
|
+
: requiredBuffer(incomingBuffers, relPath),
|
|
586
671
|
writeConflictCopies: options.writeConflictCopies !== false,
|
|
587
672
|
sourceId: snapshot.sourceId,
|
|
588
673
|
writeFile: options.writeFile,
|
|
@@ -615,7 +700,9 @@ export async function applyOfflineSyncSnapshot(options: {
|
|
|
615
700
|
baseSha256: base?.sha256,
|
|
616
701
|
localSha256: currentEntry?.sha256,
|
|
617
702
|
incomingSha256: incoming.sha256,
|
|
618
|
-
incomingBuffer:
|
|
703
|
+
incomingBuffer: options.writeConflictCopies === false
|
|
704
|
+
? incomingBuffers.get(relPath)
|
|
705
|
+
: requiredBuffer(incomingBuffers, relPath),
|
|
619
706
|
writeConflictCopies: options.writeConflictCopies !== false,
|
|
620
707
|
sourceId: snapshot.sourceId,
|
|
621
708
|
writeFile: options.writeFile,
|
|
@@ -774,10 +861,12 @@ export async function applyOfflineSyncChangeset(options: {
|
|
|
774
861
|
function verifyRecordContents(
|
|
775
862
|
records: readonly OfflineSyncFileRecord[],
|
|
776
863
|
context: string,
|
|
864
|
+
options: { requireContent?: boolean } = {},
|
|
777
865
|
): Map<string, Buffer> {
|
|
778
866
|
const buffers = new Map<string, Buffer>();
|
|
779
867
|
for (const record of records) {
|
|
780
868
|
if (typeof record.contentBase64 !== "string") {
|
|
869
|
+
if (options.requireContent === false) continue;
|
|
781
870
|
throw new Error(`${context}: contentBase64 is required for ${record.path}`);
|
|
782
871
|
}
|
|
783
872
|
const buffer = Buffer.from(record.contentBase64, "base64");
|