.
This commit is contained in:
parent
25f64ec690
commit
ff1f474324
10 changed files with 3271 additions and 2003 deletions
|
|
@ -85,14 +85,7 @@ test("highly fragmented local edits use a snapshot within the server operation b
|
|||
i % 2 ? `changed${i}` : `word${i}`
|
||||
).join(" ");
|
||||
f.contentCache.put(1, bytes(original));
|
||||
const syncer = f.syncer as unknown as {
|
||||
pushContent(
|
||||
path: string,
|
||||
version: number,
|
||||
snapshot: unknown
|
||||
): Promise<{ type: string }>;
|
||||
};
|
||||
const request = await syncer.pushContent(
|
||||
const request = await f.internals.content.createUploadPayload(
|
||||
"a.md",
|
||||
1,
|
||||
await hashSnapshot({ content: bytes(edited) })
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { SyncPhase } from "../sync-client/src/sync-operations/sync-scheduler";
|
||||
import assert from "node:assert/strict";
|
||||
import { PermanentSyncError } from "../sync-client/src/errors/errors";
|
||||
import { test } from "node:test";
|
||||
import { Database, emptyState } from "../sync-client/src/persistence/database";
|
||||
import { Database, createEmptyDatabase } from "../sync-client/src/persistence/database";
|
||||
import { Syncer } from "../sync-client/src/sync-operations/syncer";
|
||||
import { SyncClient, type StoredClient } from "../sync-client/src/sync-client";
|
||||
import { hashSnapshot } from "../sync-client/src/snapshot";
|
||||
|
|
@ -13,7 +14,7 @@ import { bytes, head, fixture } from "./sync-fixture";
|
|||
test("offline content notifications need no saves and restart reads the current bytes", async () => {
|
||||
const disk = new MemoryDisk();
|
||||
await disk.userWrite("a.md", bytes("original"));
|
||||
const initial = emptyState(JSON.stringify(["http://offline.test", "test"]));
|
||||
const initial = createEmptyDatabase(JSON.stringify(["http://offline.test", "test"]));
|
||||
initial.lastSeenUpdateId = 0;
|
||||
initial.local = { a: "a.md" };
|
||||
initial.documents.a = {
|
||||
|
|
@ -44,14 +45,14 @@ test("offline content notifications need no saves and restart reads the current
|
|||
const internals = client as unknown as {
|
||||
database: Database;
|
||||
syncer: {
|
||||
stopped: boolean;
|
||||
scan(): Promise<void>;
|
||||
preparePush(): Promise<boolean>;
|
||||
scheduler: { phase: SyncPhase };
|
||||
refreshLocalFiles(): Promise<void>;
|
||||
prepareNextUpload(): Promise<boolean>;
|
||||
};
|
||||
};
|
||||
await internals.syncer.scan();
|
||||
internals.syncer.stopped = false;
|
||||
assert.equal(await internals.syncer.preparePush(), true);
|
||||
await internals.syncer.refreshLocalFiles();
|
||||
internals.syncer.scheduler.phase = SyncPhase.Idle;
|
||||
assert.equal(await internals.syncer.prepareNextUpload(), true);
|
||||
const pending = internals.database.state.pending;
|
||||
assert(pending?.type === "content");
|
||||
assert.equal(pending.documentId, "a");
|
||||
|
|
@ -71,7 +72,7 @@ test("namespace notifications preserve the acknowledged prefix on restart", asyn
|
|||
const disk = new MemoryDisk();
|
||||
await disk.userWrite("d.md", bytes("A"));
|
||||
await disk.userWrite("b.md", bytes("B"));
|
||||
const initial = emptyState(JSON.stringify(["http://offline.test", "test"]));
|
||||
const initial = createEmptyDatabase(JSON.stringify(["http://offline.test", "test"]));
|
||||
initial.lastSeenUpdateId = 0;
|
||||
initial.local = { a: "c.md", b: "b.md" };
|
||||
initial.fileManifest.entries = { ...initial.local };
|
||||
|
|
@ -111,9 +112,9 @@ test("namespace notifications preserve the acknowledged prefix on restart", asyn
|
|||
try {
|
||||
const internals = client as unknown as {
|
||||
database: Database;
|
||||
syncer: { scan(): Promise<void> };
|
||||
syncer: { refreshLocalFiles(): Promise<void> };
|
||||
};
|
||||
await internals.syncer.scan();
|
||||
await internals.syncer.refreshLocalFiles();
|
||||
assert.deepEqual(internals.database.state.local, {
|
||||
a: "d.md",
|
||||
b: "b.md"
|
||||
|
|
@ -129,12 +130,12 @@ test("namespace notifications preserve the acknowledged prefix on restart", asyn
|
|||
|
||||
test("remote binary replacement and deletion do not create backup artifacts", async () => {
|
||||
const f = await fixture({ "a.bin": "private edit" });
|
||||
await f.files.apply(structuredClone(f.database.state), {
|
||||
await f.files.applyChanges(structuredClone(f.database.state), {
|
||||
a: { replacement: await hashSnapshot({ content: bytes("remote") }) }
|
||||
});
|
||||
assert.deepEqual(await f.disk.listFilesRecursively(), ["a.bin"]);
|
||||
assert.deepEqual(f.disk.userFiles(), new Map([["a.bin", bytes("remote")]]));
|
||||
await f.internals.reconcile({
|
||||
await f.internals.applyRemoteChanges({
|
||||
fileManifestId: 2,
|
||||
entries: {}
|
||||
});
|
||||
|
|
@ -150,7 +151,7 @@ test("rejected content recovery remains idempotent across a rename and network f
|
|||
return bytes("base");
|
||||
}
|
||||
});
|
||||
const snapshot = (await f.files.snapshot("a.md"))!;
|
||||
const snapshot = (await f.files.readSnapshot("a.md"))!;
|
||||
f.database.state.documents.a.base = {
|
||||
...head("a", 1, "base"),
|
||||
hash: (await hashSnapshot({ content: bytes("base") })).hash
|
||||
|
|
@ -171,9 +172,9 @@ test("rejected content recovery remains idempotent across a rename and network f
|
|||
oldPath: "a.md",
|
||||
relativePath: "renamed.md"
|
||||
});
|
||||
await assert.rejects(f.internals.finishPending(), /offline/);
|
||||
await assert.rejects(f.internals.uploads.completePendingUpload(), /offline/);
|
||||
offline = false;
|
||||
await f.internals.finishPending();
|
||||
await f.internals.uploads.completePendingUpload();
|
||||
assert.equal(f.database.state.pending, undefined);
|
||||
assert.equal(f.database.state.local.a, "renamed.md");
|
||||
});
|
||||
|
|
@ -203,7 +204,7 @@ test("an unmaterialized file with a directory at its path is deconflicted", asyn
|
|||
f.database.state.fileManifest.entries.a = "folder";
|
||||
await f.database.save();
|
||||
f.database.state.documents.a.remote = head("a", 2, "remote");
|
||||
await f.internals.reconcile(f.database.state.fileManifest);
|
||||
await f.internals.applyRemoteChanges(f.database.state.fileManifest);
|
||||
assert(
|
||||
[...f.disk.userFiles().values()].some(
|
||||
(value) => Buffer.from(value).toString() === "remote"
|
||||
|
|
@ -219,7 +220,7 @@ test("an unmaterialized file with a directory at its path is deconflicted", asyn
|
|||
test("ignored materialized content survives remote deletion", async () => {
|
||||
const f = await fixture();
|
||||
await f.settings.setSettings({ ignorePatterns: ["a.md"] });
|
||||
await f.internals.reconcile({
|
||||
await f.internals.applyRemoteChanges({
|
||||
fileManifestId: 2,
|
||||
entries: {}
|
||||
});
|
||||
|
|
@ -227,8 +228,8 @@ test("ignored materialized content survives remote deletion", async () => {
|
|||
assert.deepEqual(f.database.state.fileManifest.entries, {});
|
||||
});
|
||||
|
||||
test("unfinished bootstrap cannot be rebound to another vault", async () => {
|
||||
const database = emptyState(JSON.stringify(["http://example.test", "old"]));
|
||||
test("unfinished initializeFromServerSnapshot cannot be rebound to another vault", async () => {
|
||||
const database = createEmptyDatabase(JSON.stringify(["http://example.test", "old"]));
|
||||
// A scan may commit local identities before the first remote application.
|
||||
database.local = { a: "a.md" };
|
||||
database.documents.a = { observedHash: "" };
|
||||
|
|
@ -279,7 +280,7 @@ test("a rejected upload pauses all sync across restarts until an explicit retry"
|
|||
const reads = eventReads;
|
||||
await f.disk.userWrite("a.md", bytes("small"));
|
||||
await f.syncer.syncLocallyUpdatedFile({ relativePath: "a.md" });
|
||||
f.syncer.wake();
|
||||
f.syncer.requestSync();
|
||||
await assert.rejects(f.syncer.waitUntilFinished(), /413/);
|
||||
assert.equal(eventReads, reads);
|
||||
assert.equal(sent.length, 1);
|
||||
|
|
@ -292,7 +293,7 @@ test("a rejected upload pauses all sync across restarts until an explicit retry"
|
|||
assert.equal(sent.length, 1);
|
||||
reject = false;
|
||||
await f.syncer.retryRejectedRequests();
|
||||
f.syncer.wake();
|
||||
f.syncer.requestSync();
|
||||
await f.syncer.waitUntilFinished();
|
||||
assert.deepEqual(sent.map(({ id }) => id), ["a", "a", "a", "b"]);
|
||||
assert.deepEqual(sent[1], sent[0], "resolve the old request before uploading corrected bytes");
|
||||
|
|
@ -311,11 +312,11 @@ test("a transient upload failure retries the exact request and recovers its ackn
|
|||
return { type: "Accepted", ...head("a", 2, "local") };
|
||||
}
|
||||
});
|
||||
f.internals.stopped = false;
|
||||
await f.internals.preparePush();
|
||||
await assert.rejects(f.internals.finishPending(), /lost upload reply/);
|
||||
f.internals.scheduler.phase = SyncPhase.Idle;
|
||||
await f.internals.prepareNextUpload();
|
||||
await assert.rejects(f.internals.uploads.completePendingUpload(), /lost upload reply/);
|
||||
assert(f.database.state.pending);
|
||||
await f.internals.finishPending();
|
||||
await f.internals.uploads.completePendingUpload();
|
||||
assert.equal(requests.length, 2);
|
||||
assert.deepEqual(requests[1], requests[0]);
|
||||
assert.equal(f.database.state.documents.a.base!.vaultUpdateId, 2);
|
||||
|
|
@ -330,13 +331,13 @@ test("a persisted upload rejection pauses without filesystem writes", async () =
|
|||
throw new PermanentSyncError("HTTP 413");
|
||||
}
|
||||
});
|
||||
f.internals.stopped = false;
|
||||
await f.internals.preparePush();
|
||||
f.internals.scheduler.phase = SyncPhase.Idle;
|
||||
await f.internals.prepareNextUpload();
|
||||
f.disk.boundary = (label) => {
|
||||
if (!label.startsWith("read:"))
|
||||
throw new Error("Unexpected filesystem mutation");
|
||||
};
|
||||
await assert.rejects(f.internals.finishPending(), /413/);
|
||||
await assert.rejects(f.internals.uploads.completePendingUpload(), /413/);
|
||||
assert.equal(uploads, 1);
|
||||
assert.equal(f.database.state.pending?.rejection, "HTTP 413");
|
||||
assert.deepEqual(f.disk.userFiles(), new Map([["a.md", bytes("local")]]));
|
||||
|
|
@ -351,16 +352,16 @@ test("an explicit retry recovers an earlier accepted upload after a permanent re
|
|||
return { type: "Accepted", ...head("a", 2, "local") };
|
||||
}
|
||||
});
|
||||
f.internals.stopped = false;
|
||||
await f.internals.preparePush();
|
||||
await assert.rejects(f.internals.finishPending(), /413/);
|
||||
f.internals.scheduler.phase = SyncPhase.Idle;
|
||||
await f.internals.prepareNextUpload();
|
||||
await assert.rejects(f.internals.uploads.completePendingUpload(), /413/);
|
||||
await assert.rejects(f.syncer.waitUntilFinished(), /413/);
|
||||
await f.syncer.retryRejectedRequests();
|
||||
await f.internals.finishPending();
|
||||
await f.internals.uploads.completePendingUpload();
|
||||
assert.deepEqual(requests[1], requests[0]);
|
||||
assert.equal(f.database.state.documents.a.base?.vaultUpdateId, 2);
|
||||
assert.equal(f.database.state.pending, undefined);
|
||||
assert.equal(await f.internals.preparePush(), false);
|
||||
assert.equal(await f.internals.prepareNextUpload(), false);
|
||||
await f.syncer.waitUntilFinished();
|
||||
assert.deepEqual(f.disk.userFiles(), new Map([["a.md", bytes("local")]]));
|
||||
});
|
||||
|
|
@ -390,22 +391,22 @@ test("rejected manifests are resolved before publishing later namespace changes"
|
|||
base: { ...head("a", 1, "x"), hash: "hash" }
|
||||
};
|
||||
await f.database.save();
|
||||
await assert.rejects(f.internals.finishPending(), /413/);
|
||||
f.internals.stopped = false;
|
||||
assert.equal(await f.internals.preparePush(), false);
|
||||
await assert.rejects(f.internals.uploads.completePendingUpload(), /413/);
|
||||
f.internals.scheduler.phase = SyncPhase.Idle;
|
||||
assert.equal(await f.internals.prepareNextUpload(), false);
|
||||
f.database.state.local = {};
|
||||
reject = false;
|
||||
await f.syncer.retryRejectedRequests();
|
||||
await f.internals.finishPending();
|
||||
await f.internals.uploads.completePendingUpload();
|
||||
assert.deepEqual(f.database.state.fileManifest.entries, { a: "new.md" });
|
||||
assert(await f.internals.preparePush());
|
||||
await f.internals.finishPending();
|
||||
assert(await f.internals.prepareNextUpload());
|
||||
await f.internals.uploads.completePendingUpload();
|
||||
assert.deepEqual(f.database.state.fileManifest.entries, {});
|
||||
});
|
||||
|
||||
test("offline delete/recreate identities survive restarting before any network connection", async () => {
|
||||
const key = JSON.stringify(["http://offline.test", "test"]);
|
||||
const initial = emptyState(key);
|
||||
const initial = createEmptyDatabase(key);
|
||||
initial.lastSeenUpdateId = 0;
|
||||
initial.local.a = "a.md";
|
||||
initial.documents.a = { observedHash: "" };
|
||||
|
|
@ -425,10 +426,10 @@ test("offline delete/recreate identities survive restarting before any network c
|
|||
const second = await SyncClient.create({ fs: disk, persistence });
|
||||
try {
|
||||
const internals = second as unknown as {
|
||||
syncer: { scan(): Promise<void> };
|
||||
syncer: { refreshLocalFiles(): Promise<void> };
|
||||
database: Database;
|
||||
};
|
||||
await internals.syncer.scan();
|
||||
await internals.syncer.refreshLocalFiles();
|
||||
assert.equal(internals.database.state.local.a, undefined);
|
||||
assert.equal(
|
||||
Object.values(internals.database.state.local).filter(
|
||||
|
|
@ -466,12 +467,12 @@ test("offline notifications bind an otherwise empty client to its vault", async
|
|||
test("unignoring a deleted file resumes the deferred remote deletion without resurrection", async () => {
|
||||
const f = await fixture();
|
||||
await f.settings.setSettings({ ignorePatterns: ["a.md"] });
|
||||
await f.internals.reconcile({
|
||||
await f.internals.applyRemoteChanges({
|
||||
fileManifestId: 2,
|
||||
entries: {}
|
||||
});
|
||||
await f.settings.setSettings({ ignorePatterns: [] });
|
||||
await f.internals.reconcile(f.database.state.fileManifest);
|
||||
await f.internals.applyRemoteChanges(f.database.state.fileManifest);
|
||||
assert.equal(f.disk.userFiles().size, 0);
|
||||
assert.deepEqual(f.database.state.local, {});
|
||||
});
|
||||
|
|
@ -479,7 +480,7 @@ test("unignoring a deleted file resumes the deferred remote deletion without res
|
|||
test("an ignored file does not move when a remote rename targets its path", async () => {
|
||||
const f = await fixture({ "a.md": "A", "b.md": "B" });
|
||||
await f.settings.setSettings({ ignorePatterns: ["a.md"] });
|
||||
await f.internals.reconcile({
|
||||
await f.internals.applyRemoteChanges({
|
||||
fileManifestId: 2,
|
||||
entries: { a: "c.md", b: "a.md" }
|
||||
});
|
||||
|
|
@ -490,8 +491,8 @@ test("an ignored file does not move when a remote rename targets its path", asyn
|
|||
["b.md", bytes("B")]
|
||||
])
|
||||
);
|
||||
f.internals.stopped = false;
|
||||
assert.equal(await f.internals.preparePush(), false);
|
||||
f.internals.scheduler.phase = SyncPhase.Idle;
|
||||
assert.equal(await f.internals.prepareNextUpload(), false);
|
||||
assert.deepEqual(
|
||||
f.disk.userFiles(),
|
||||
new Map([
|
||||
|
|
@ -522,7 +523,7 @@ for (const kind of ["path", "content"] as const) {
|
|||
relativePath: "c.md"
|
||||
});
|
||||
}
|
||||
await f.internals.incorporateEventBatch({
|
||||
await f.internals.applyEventBatch({
|
||||
headEventId: 3,
|
||||
endEventId: 3,
|
||||
events: [2, 3].map((id) =>
|
||||
|
|
@ -622,15 +623,15 @@ test("ignoring a locally renamed file defers its move without forgetting the loc
|
|||
relativePath: "ignored.md"
|
||||
});
|
||||
await f.settings.setSettings({ ignorePatterns: ["ignored.md"] });
|
||||
await f.internals.scan();
|
||||
f.internals.stopped = false;
|
||||
assert.equal(await f.internals.preparePush(), false);
|
||||
await f.internals.reconcile({
|
||||
await f.internals.refreshLocalFiles();
|
||||
f.internals.scheduler.phase = SyncPhase.Idle;
|
||||
assert.equal(await f.internals.prepareNextUpload(), false);
|
||||
await f.internals.applyRemoteChanges({
|
||||
fileManifestId: 2,
|
||||
entries: { a: "a.md" }
|
||||
});
|
||||
await f.settings.setSettings({ ignorePatterns: [] });
|
||||
await f.internals.reconcile(f.database.state.fileManifest);
|
||||
await f.internals.applyRemoteChanges(f.database.state.fileManifest);
|
||||
assert.equal(f.database.state.local.a, "ignored.md");
|
||||
assert.equal(
|
||||
Buffer.from(
|
||||
|
|
@ -645,25 +646,25 @@ test("a notified deletion of an ignored file does not delete its server membersh
|
|||
await f.settings.setSettings({ ignorePatterns: ["a.md"] });
|
||||
await f.disk.userDelete("a.md");
|
||||
await f.syncer.syncLocallyDeletedFile("a.md");
|
||||
await f.internals.scan();
|
||||
f.internals.stopped = false;
|
||||
assert.equal(await f.internals.preparePush(), false);
|
||||
await f.internals.refreshLocalFiles();
|
||||
f.internals.scheduler.phase = SyncPhase.Idle;
|
||||
assert.equal(await f.internals.prepareNextUpload(), false);
|
||||
assert.equal(f.database.state.local.a, undefined);
|
||||
await f.settings.setSettings({ ignorePatterns: [] });
|
||||
await f.internals.reconcile(f.database.state.fileManifest);
|
||||
assert(await f.internals.preparePush());
|
||||
await f.internals.applyRemoteChanges(f.database.state.fileManifest);
|
||||
assert(await f.internals.prepareNextUpload());
|
||||
assert.equal(f.database.state.pending?.type, "fileManifest");
|
||||
});
|
||||
|
||||
test("an incoming ignored document cannot force an existing ignored file to move", async () => {
|
||||
const f = await fixture();
|
||||
await f.settings.setSettings({ ignorePatterns: ["a.md", "*conflict*"] });
|
||||
await f.internals.reconcile({
|
||||
await f.internals.applyRemoteChanges({
|
||||
fileManifestId: 2,
|
||||
entries: { a: "b.md", other: "a.md" }
|
||||
});
|
||||
f.internals.stopped = false;
|
||||
await f.internals.preparePush();
|
||||
f.internals.scheduler.phase = SyncPhase.Idle;
|
||||
await f.internals.prepareNextUpload();
|
||||
assert.deepEqual(f.disk.userFiles(), new Map([["a.md", bytes("local")]]));
|
||||
assert.equal(
|
||||
new Set(Object.values(f.database.state.local)).size,
|
||||
|
|
@ -677,9 +678,9 @@ test("rejected content is reported to callers and an explicit retry clears the r
|
|||
throw new PermanentSyncError("HTTP 413");
|
||||
}
|
||||
});
|
||||
f.internals.stopped = false;
|
||||
await f.internals.preparePush();
|
||||
await assert.rejects(f.internals.finishPending(), /413/);
|
||||
f.internals.scheduler.phase = SyncPhase.Idle;
|
||||
await f.internals.prepareNextUpload();
|
||||
await assert.rejects(f.internals.uploads.completePendingUpload(), /413/);
|
||||
await assert.rejects(f.syncer.waitUntilFinished(), /413/);
|
||||
assert(f.syncer.isBusy);
|
||||
await f.syncer.retryRejectedRequests();
|
||||
|
|
@ -689,7 +690,7 @@ test("rejected content is reported to callers and an explicit retry clears the r
|
|||
|
||||
test("notification saves cannot roll back an uncertain engine commit, even if the first reload fails", async () => {
|
||||
const key = JSON.stringify(["http://offline.test", "test"]);
|
||||
const initial = emptyState(key);
|
||||
const initial = createEmptyDatabase(key);
|
||||
initial.lastSeenUpdateId = 0;
|
||||
const disk = new MemoryDisk();
|
||||
const store = new MemoryPersistence({
|
||||
|
|
@ -784,7 +785,7 @@ for (const edited of [false, true]) {
|
|||
});
|
||||
if (edited) await f.disk.userWrite("a.md", bytes("my new edit"));
|
||||
f = await fixture({}, service, f);
|
||||
await (f.syncer as unknown as { bootstrap(): Promise<void> }).bootstrap();
|
||||
await (f.syncer as unknown as { initializeFromServerSnapshot(): Promise<void> }).initializeFromServerSnapshot();
|
||||
assert.equal(f.database.state.lastSeenUpdateId, 3);
|
||||
assert.deepEqual((await f.disk.read("a.md"))?.content, bytes("restored"));
|
||||
const other = Object.keys(f.database.state.local).filter((id) => id !== "a");
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
|||
import { test } from "node:test";
|
||||
import {
|
||||
Database,
|
||||
emptyState,
|
||||
createEmptyDatabase,
|
||||
type StoredDatabase
|
||||
} from "../sync-client/src/persistence/database";
|
||||
import { Settings } from "../sync-client/src/persistence/settings";
|
||||
|
|
@ -29,7 +29,7 @@ for (const [count, delivery] of [
|
|||
{ timeout: 5_000 },
|
||||
async () => {
|
||||
const logger = new Logger();
|
||||
const initial = emptyState("event-delivery");
|
||||
const initial = createEmptyDatabase("event-delivery");
|
||||
initial.lastSeenUpdateId = 0;
|
||||
const persistence = new MemoryPersistence({ database: initial });
|
||||
const database = new Database(
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ for (const equal of [false, true])
|
|||
};
|
||||
const next = structuredClone(f.database.state);
|
||||
next.local = { a: "b.md", b: "a.md" };
|
||||
await f.files.apply(next);
|
||||
await f.files.applyChanges(next);
|
||||
assert.deepEqual(f.database.state.local, next.local);
|
||||
assert.deepEqual(
|
||||
f.disk.userFiles(),
|
||||
|
|
@ -40,7 +40,7 @@ for (const direction of ["file-to-directory", "directory-to-file"])
|
|||
? { [ids[0]]: "folder/a.md", [ids[1]]: "b.md" }
|
||||
: { [ids[0]]: "folder", [ids[1]]: "other.md" };
|
||||
const expected = structuredClone(next.local);
|
||||
await f.files.apply(next);
|
||||
await f.files.applyChanges(next);
|
||||
assert.deepEqual(f.database.state.local, expected);
|
||||
assert.deepEqual(
|
||||
new Set(f.disk.userFiles().keys()),
|
||||
|
|
@ -59,7 +59,7 @@ test("an unexpected occupant is preserved under a visible conflict name", async
|
|||
await f.disk.userWrite("remote.md", bytes("unexpected"));
|
||||
const next = structuredClone(f.database.state);
|
||||
next.local.a = "remote.md";
|
||||
await f.files.apply(next);
|
||||
await f.files.applyChanges(next);
|
||||
assert.equal(f.database.state.local.a, "remote.md");
|
||||
assert.equal(f.disk.userFiles().size, 2);
|
||||
assert.deepEqual(
|
||||
|
|
@ -73,9 +73,9 @@ test("an unexpected occupant is preserved under a visible conflict name", async
|
|||
|
||||
test("content edited after planning is merged with the incoming snapshot", async () => {
|
||||
const f = await fixture({ "a.md": "base\n" });
|
||||
const expected = (await f.files.snapshot("a.md"))!;
|
||||
const expected = (await f.files.readSnapshot("a.md"))!;
|
||||
await f.disk.userWrite("a.md", bytes("base\nlocal\n"));
|
||||
await f.files.apply(structuredClone(f.database.state), {
|
||||
await f.files.applyChanges(structuredClone(f.database.state), {
|
||||
a: {
|
||||
expected,
|
||||
replacement: await hashSnapshot({
|
||||
|
|
@ -94,7 +94,7 @@ test("deleting the source after planning does not resurrect it", async () => {
|
|||
const next = structuredClone(f.database.state);
|
||||
next.local.a = "remote.md";
|
||||
await f.disk.userDelete("a.md");
|
||||
await f.files.apply(next, {
|
||||
await f.files.applyChanges(next, {
|
||||
a: { replacement: await hashSnapshot({ content: bytes("remote") }) }
|
||||
});
|
||||
assert.deepEqual(f.disk.userFiles(), new Map());
|
||||
|
|
@ -109,7 +109,7 @@ for (const protect of ["ignore", "size"] as const)
|
|||
if (protect === "ignore")
|
||||
await f.settings.setSetting("ignorePatterns", ["a.md"]);
|
||||
else await f.settings.setSetting("maxFileSizeMB", 0);
|
||||
await f.files.apply(next, {
|
||||
await f.files.applyChanges(next, {
|
||||
a: {
|
||||
replacement: await hashSnapshot({
|
||||
content: bytes("remote")
|
||||
|
|
@ -149,12 +149,12 @@ for (const when of ["planning", "installed"])
|
|||
next.local = { a: "b.md", b: "a.md" };
|
||||
if (when === "planning")
|
||||
await assert.rejects(
|
||||
f.files.apply(next),
|
||||
f.files.applyChanges(next),
|
||||
LocalChangesDuringReconciliation
|
||||
);
|
||||
else await f.files.apply(next);
|
||||
else await f.files.applyChanges(next);
|
||||
assert(changed);
|
||||
await f.internals.scan();
|
||||
await f.internals.refreshLocalFiles();
|
||||
assert.equal(f.database.state.local.a, "editor.md");
|
||||
assert.deepEqual(
|
||||
f.disk.userFiles().get(f.database.state.local.a),
|
||||
|
|
@ -189,9 +189,9 @@ for (const action of ["move", "delete"] as const)
|
|||
};
|
||||
const next = structuredClone(f.database.state);
|
||||
next.local = { a: "remote-a.md", b: "remote-b.md" };
|
||||
await f.files.apply(next);
|
||||
await f.files.applyChanges(next);
|
||||
assert(changed);
|
||||
await f.internals.scan();
|
||||
await f.internals.refreshLocalFiles();
|
||||
assert.deepEqual(f.disk.userFiles().get("b.md"), bytes("replacement"));
|
||||
assert.equal(f.disk.userFiles().has("remote-b.md"), false);
|
||||
assert.equal(
|
||||
|
|
@ -207,7 +207,7 @@ test("a file and nested directory exchange their exact paths", async () => {
|
|||
const [a, b] = Object.keys(f.database.state.local);
|
||||
const next = structuredClone(f.database.state);
|
||||
next.local = { [a]: "b", [b]: "a/deep/child" };
|
||||
await f.files.apply(next);
|
||||
await f.files.applyChanges(next);
|
||||
assert.deepEqual(f.database.state.local, { [a]: "b", [b]: "a/deep/child" });
|
||||
assert.deepEqual(
|
||||
f.disk.userFiles(),
|
||||
|
|
@ -223,7 +223,7 @@ for (const phase of ["before", "durable"])
|
|||
const f = await fixture({ "a.md": "base\n" });
|
||||
const original = structuredClone(f.database.state);
|
||||
const write = {
|
||||
expected: (await f.files.snapshot("a.md"))!,
|
||||
expected: (await f.files.readSnapshot("a.md"))!,
|
||||
replacement: await hashSnapshot({
|
||||
content: bytes("remote\nbase\n")
|
||||
})
|
||||
|
|
@ -239,25 +239,25 @@ for (const phase of ["before", "durable"])
|
|||
}
|
||||
};
|
||||
await assert.rejects(
|
||||
f.files.apply(structuredClone(original), { a: write }),
|
||||
f.files.applyChanges(structuredClone(original), { a: write }),
|
||||
/save failed/
|
||||
);
|
||||
f.persistence.boundary = () => {};
|
||||
await f.database.recoverPersistence();
|
||||
await f.files.apply(structuredClone(original), { a: write });
|
||||
await f.files.applyChanges(structuredClone(original), { a: write });
|
||||
assert.deepEqual(
|
||||
f.disk.userFiles().get("a.md"),
|
||||
bytes("remote\nbase\nlocal\n")
|
||||
);
|
||||
assert.equal(
|
||||
f.database.state.documents.a.observedHash,
|
||||
(await f.files.snapshot("a.md"))!.hash
|
||||
(await f.files.readSnapshot("a.md"))!.hash
|
||||
);
|
||||
});
|
||||
|
||||
test("atomic updater includes an edit made after the last snapshot without unlinking the file", async () => {
|
||||
const f = await fixture({ "a.md": "base\n" });
|
||||
const expected = (await f.files.snapshot("a.md"))!;
|
||||
const expected = (await f.files.readSnapshot("a.md"))!;
|
||||
let edited = false;
|
||||
f.disk.boundary = async (label) => {
|
||||
assert(
|
||||
|
|
@ -269,7 +269,7 @@ test("atomic updater includes an edit made after the last snapshot without unlin
|
|||
await f.disk.userWrite("a.md", bytes("base\nlatest edit\n"));
|
||||
}
|
||||
};
|
||||
await f.files.apply(structuredClone(f.database.state), {
|
||||
await f.files.applyChanges(structuredClone(f.database.state), {
|
||||
a: {
|
||||
expected,
|
||||
replacement: await hashSnapshot({
|
||||
|
|
@ -284,7 +284,7 @@ test("atomic updater includes an edit made after the last snapshot without unlin
|
|||
);
|
||||
assert.equal(
|
||||
f.database.state.documents.a.observedHash,
|
||||
(await f.files.snapshot("a.md"))!.hash
|
||||
(await f.files.readSnapshot("a.md"))!.hash
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -295,9 +295,9 @@ test("a failed atomic update leaves database metadata unchanged", async () => {
|
|||
if (label === "before:write:a.md") throw new Error("update failed");
|
||||
};
|
||||
await assert.rejects(
|
||||
f.files.apply(structuredClone(original), {
|
||||
f.files.applyChanges(structuredClone(original), {
|
||||
a: {
|
||||
expected: (await f.files.snapshot("a.md"))!,
|
||||
expected: (await f.files.readSnapshot("a.md"))!,
|
||||
replacement: await hashSnapshot({
|
||||
content: bytes("remote")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { fixture } from "./sync-fixture";
|
|||
f.syncer.start();
|
||||
await f.syncer.waitUntilFinished();
|
||||
const before = delivered;
|
||||
f.syncer.wake();
|
||||
f.syncer.requestSync();
|
||||
await f.syncer.waitUntilFinished();
|
||||
assert(delivered > before, "Background sync must remain usable");
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
|||
import { test } from "node:test";
|
||||
import {
|
||||
Database,
|
||||
emptyState,
|
||||
createEmptyDatabase,
|
||||
type StoredDatabase
|
||||
} from "../sync-client/src/persistence/database";
|
||||
import { Settings } from "../sync-client/src/persistence/settings";
|
||||
|
|
@ -87,7 +87,7 @@ async function fixture(syncIntervalMs: number | undefined) {
|
|||
};
|
||||
}
|
||||
};
|
||||
const initial = emptyState("polling-test");
|
||||
const initial = createEmptyDatabase("polling-test");
|
||||
initial.lastSeenUpdateId = 2;
|
||||
initial.local = { [documentId]: "note.bin" };
|
||||
initial.fileManifest = { fileManifestId: 2, entries: { ...initial.local } };
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import * as fs from "node:fs/promises";
|
|||
import * as path from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { NodeFileSystemOperations } from "../local-client-cli/src/node-filesystem";
|
||||
import { Database, emptyState } from "../sync-client/src/persistence/database";
|
||||
import { Database, createEmptyDatabase } from "../sync-client/src/persistence/database";
|
||||
import { FileOperations } from "../sync-client/src/file-operations/file-operations";
|
||||
import type { ServerConfig } from "../sync-client/src/services/server-config";
|
||||
import { hashSnapshot } from "../sync-client/src/snapshot";
|
||||
|
|
@ -97,7 +97,7 @@ test(
|
|||
"original bytes"
|
||||
);
|
||||
}
|
||||
const initial = emptyState("real");
|
||||
const initial = createEmptyDatabase("real");
|
||||
initial.local = { a: "a.md", b: "b.md" };
|
||||
for (const [id, relative] of Object.entries(initial.local))
|
||||
initial.documents[id] = {
|
||||
|
|
@ -119,7 +119,7 @@ test(
|
|||
} as ServerConfig);
|
||||
const next = structuredClone(initial);
|
||||
next.local = { a: "b.md", b: "a.md" };
|
||||
await operations.apply(next);
|
||||
await operations.applyChanges(next);
|
||||
assert.equal(
|
||||
Buffer.from(
|
||||
(await disk.read("a.md"))!.content
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { test } from "node:test";
|
|||
import { MemoryDisk, MemoryPersistence } from "./storage";
|
||||
import {
|
||||
Database,
|
||||
emptyState,
|
||||
createEmptyDatabase,
|
||||
type StoredDatabase
|
||||
} from "../sync-client/src/persistence/database";
|
||||
import { FileOperations } from "../sync-client/src/file-operations/file-operations";
|
||||
|
|
@ -28,7 +28,7 @@ async function fixture(
|
|||
}
|
||||
) {
|
||||
const disk = new MemoryDisk();
|
||||
const initial = emptyState("recovery-test");
|
||||
const initial = createEmptyDatabase("recovery-test");
|
||||
for (const [id, { path, text }] of Object.entries(contents)) {
|
||||
await disk.userWrite(path, bytes(text));
|
||||
initial.local[id] = path;
|
||||
|
|
@ -211,7 +211,7 @@ for (const [name, create] of [
|
|||
trace(label);
|
||||
};
|
||||
baseline.persistence.boundary = trace;
|
||||
await baseline.restart().files.apply(baseline.next, baseline.writes);
|
||||
await baseline.restart().files.applyChanges(baseline.next, baseline.writes);
|
||||
assert.deepEqual(baseline.disk.userFiles(), baseline.expected);
|
||||
assert.deepEqual(
|
||||
baseline.persistence.snapshot().database!.local,
|
||||
|
|
@ -242,7 +242,7 @@ for (const [name, create] of [
|
|||
await assert.rejects(
|
||||
current
|
||||
.restart()
|
||||
.files.apply(current.next, current.writes),
|
||||
.files.applyChanges(current.next, current.writes),
|
||||
/interrupted/
|
||||
);
|
||||
assert(fired);
|
||||
|
|
@ -279,7 +279,7 @@ for (const [name, create] of [
|
|||
assert.equal(
|
||||
runtime.database.state.documents[id]
|
||||
.observedHash,
|
||||
(await runtime.files.snapshot(path))!.hash
|
||||
(await runtime.files.readSnapshot(path))!.hash
|
||||
);
|
||||
}
|
||||
assert(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { SyncPhase } from "../sync-client/src/sync-operations/sync-scheduler";
|
||||
import assert from "node:assert/strict";
|
||||
import { SUPPORTED_API_VERSION } from "../sync-client/src/consts";
|
||||
import { PermanentSyncError } from "../sync-client/src/errors/errors";
|
||||
import { test } from "node:test";
|
||||
import type { Database } from "../sync-client/src/persistence/database";
|
||||
import { emptyState } from "../sync-client/src/persistence/database";
|
||||
import { createEmptyDatabase } from "../sync-client/src/persistence/database";
|
||||
import { SyncClient } from "../sync-client/src/sync-client";
|
||||
import { hashSnapshot } from "../sync-client/src/snapshot";
|
||||
import { EventListeners } from "../sync-client/src/utils/data-structures/event-listeners";
|
||||
|
|
@ -32,9 +33,9 @@ test("ignored rename must not produce a duplicate submitted path", async () => {
|
|||
relativePath: "a.md"
|
||||
});
|
||||
await f.settings.setSettings({ ignorePatterns: ["ignored.md"] });
|
||||
await f.internals.scan();
|
||||
f.internals.stopped = false;
|
||||
await f.internals.preparePush();
|
||||
await f.internals.refreshLocalFiles();
|
||||
f.internals.scheduler.phase = SyncPhase.Idle;
|
||||
await f.internals.prepareNextUpload();
|
||||
const p = f.database.state.pending;
|
||||
assert(p?.type === "fileManifest");
|
||||
assert.equal(
|
||||
|
|
@ -51,12 +52,12 @@ test("ignored untracked ancestor must remain in place", async () => {
|
|||
await f.disk.userWrite("private", bytes("LOCAL PRIVATE"));
|
||||
await f.settings.setSettings({ ignorePatterns: ["private"] });
|
||||
(f.database.state.documents.a ??= {}).remote = head("a", 2, "remote");
|
||||
await f.internals.reconcile({
|
||||
await f.internals.applyRemoteChanges({
|
||||
fileManifestId: 3,
|
||||
entries: { a: "private/note.md" }
|
||||
});
|
||||
f.internals.stopped = false;
|
||||
await f.internals.preparePush();
|
||||
f.internals.scheduler.phase = SyncPhase.Idle;
|
||||
await f.internals.prepareNextUpload();
|
||||
assert.deepEqual(await f.disk.read("private"), {
|
||||
content: bytes("LOCAL PRIVATE")
|
||||
});
|
||||
|
|
@ -70,7 +71,7 @@ test("oversized untracked file must remain in place", async () => {
|
|||
await f.disk.userWrite("a.md", bytes("LOCAL OVERSIZED"));
|
||||
await f.settings.setSettings({ maxFileSizeMB: 5 / (1024 * 1024) });
|
||||
(f.database.state.documents.a ??= {}).remote = head("a", 2, "R");
|
||||
await f.internals.reconcile({
|
||||
await f.internals.applyRemoteChanges({
|
||||
fileManifestId: 3,
|
||||
entries: { a: "a.md" }
|
||||
});
|
||||
|
|
@ -105,15 +106,15 @@ test("rejected unknown success must use submitted content as merge base", async
|
|||
};
|
||||
f.database.state.lastSeenUpdateId = 1;
|
||||
await f.disk.userWrite("a.md", bytes("base A"));
|
||||
f.internals.stopped = false;
|
||||
await f.internals.preparePush();
|
||||
f.internals.scheduler.phase = SyncPhase.Idle;
|
||||
await f.internals.prepareNextUpload();
|
||||
const req = f.database.state.pending!.request.requestId;
|
||||
await assert.rejects(f.internals.finishPending(), /413/);
|
||||
await assert.rejects(f.internals.uploads.completePendingUpload(), /413/);
|
||||
await f.disk.userWrite("a.md", bytes("base"));
|
||||
reject = false;
|
||||
await f.syncer.retryRejectedRequests();
|
||||
await f.internals.finishPending();
|
||||
await f.internals.incorporateEventBatch({
|
||||
await f.internals.uploads.completePendingUpload();
|
||||
await f.internals.applyEventBatch({
|
||||
headEventId: 3,
|
||||
endEventId: 3,
|
||||
events: [
|
||||
|
|
@ -150,7 +151,7 @@ test("bootstrap adopts same-path identity even after multiple notifications", as
|
|||
fileManifest: { fileManifestId: 2, entries: { remoteA: "a.md" } },
|
||||
documents: [head("remoteA", 1, "remote a")]
|
||||
};
|
||||
await f.internals.scan(initial);
|
||||
await f.internals.refreshLocalFiles(initial);
|
||||
assert.equal(f.database.state.local.remoteA, "a.md");
|
||||
});
|
||||
|
||||
|
|
@ -176,9 +177,9 @@ test("oversized remote head must not cause identical stale uploads forever", asy
|
|||
};
|
||||
(f.database.state.documents.a ??= {}).remote = head("a", 2, remoteText);
|
||||
await f.settings.setSettings({ maxFileSizeMB: 10 / (1024 * 1024) });
|
||||
f.internals.stopped = false;
|
||||
f.internals.scheduler.phase = SyncPhase.Idle;
|
||||
// Even a small local edit must wait until its required remote base is readable.
|
||||
assert.equal(await f.internals.preparePush(), false);
|
||||
assert.equal(await f.internals.prepareNextUpload(), false);
|
||||
assert.equal(attempts, 0);
|
||||
assert.equal(f.database.state.pending, undefined);
|
||||
});
|
||||
|
|
@ -205,12 +206,12 @@ test("local undo of accepted-but-rejected manifest must survive replay", async (
|
|||
oldPath: "a.md",
|
||||
relativePath: "b.md"
|
||||
});
|
||||
await f.internals.scan();
|
||||
f.internals.stopped = false;
|
||||
await f.internals.preparePush();
|
||||
await f.internals.refreshLocalFiles();
|
||||
f.internals.scheduler.phase = SyncPhase.Idle;
|
||||
await f.internals.prepareNextUpload();
|
||||
const req = f.database.state.pending!.request.requestId;
|
||||
f.internals.stopped = true;
|
||||
await assert.rejects(f.internals.finishPending(), /413/);
|
||||
f.internals.scheduler.phase = SyncPhase.Stopped;
|
||||
await assert.rejects(f.internals.uploads.completePendingUpload(), /413/);
|
||||
await f.disk.userRename("b.md", "a.md");
|
||||
await f.syncer.syncLocallyUpdatedFile({
|
||||
oldPath: "b.md",
|
||||
|
|
@ -218,8 +219,8 @@ test("local undo of accepted-but-rejected manifest must survive replay", async (
|
|||
});
|
||||
reject = false;
|
||||
await f.syncer.retryRejectedRequests();
|
||||
await f.internals.finishPending();
|
||||
await f.internals.incorporateEventBatch({
|
||||
await f.internals.uploads.completePendingUpload();
|
||||
await f.internals.applyEventBatch({
|
||||
headEventId: 2,
|
||||
endEventId: 2,
|
||||
events: [
|
||||
|
|
@ -397,7 +398,7 @@ test("oversized files should be filtered before reading complete snapshots", asy
|
|||
f.disk.boundary = (label) => {
|
||||
if (label === "read:snapshot:huge.bin") reads++;
|
||||
};
|
||||
await f.internals.scan();
|
||||
await f.internals.refreshLocalFiles();
|
||||
assert.equal(reads, 0);
|
||||
});
|
||||
|
||||
|
|
@ -412,7 +413,7 @@ test("permanent destination failure replans the affected file and finishes the b
|
|||
const next = structuredClone(f.database.state);
|
||||
next.local = { a: "unsupported/a.md", b: "good.md" };
|
||||
next.fileManifest = { fileManifestId: 2, entries: { ...next.local } };
|
||||
await f.files.apply(next);
|
||||
await f.files.applyChanges(next);
|
||||
await f.settings.setSettings({ ignorePatterns: ["unsupported/**"] });
|
||||
assert.deepEqual(
|
||||
[...f.disk.userFiles().values()]
|
||||
|
|
@ -508,7 +509,7 @@ for (const interrupted of [false, true]) {
|
|||
await f.database.save();
|
||||
const saved = f.persistence.snapshot();
|
||||
const catchUp = async () =>
|
||||
f.internals.incorporateEventBatch(
|
||||
f.internals.applyEventBatch(
|
||||
await service.getEvents!(f.database.state.lastSeenUpdateId!)
|
||||
);
|
||||
if (interrupted) {
|
||||
|
|
@ -539,7 +540,7 @@ test("an ignored occupant arriving after the scan keeps its path and privacy", a
|
|||
);
|
||||
await f.settings.setSettings({ ignorePatterns: ["private"] });
|
||||
(f.database.state.documents.a ??= {}).remote = head("a", 2, "remote");
|
||||
await f.internals.reconcile({
|
||||
await f.internals.applyRemoteChanges({
|
||||
fileManifestId: 3,
|
||||
entries: { a: "private/note.md" }
|
||||
});
|
||||
|
|
@ -547,8 +548,8 @@ test("an ignored occupant arriving after the scan keeps its path and privacy", a
|
|||
(await f.disk.read("private"))?.content,
|
||||
bytes("SECRET")
|
||||
);
|
||||
f.internals.stopped = false;
|
||||
await f.internals.preparePush();
|
||||
f.internals.scheduler.phase = SyncPhase.Idle;
|
||||
await f.internals.prepareNextUpload();
|
||||
const { pending } = f.database.state;
|
||||
assert(
|
||||
pending?.type !== "content" ||
|
||||
|
|
@ -571,7 +572,7 @@ test("content notifications during every snapshot do not starve a complete names
|
|||
});
|
||||
}
|
||||
};
|
||||
await f.internals.scan();
|
||||
await f.internals.refreshLocalFiles();
|
||||
assert.equal(notifications, 2);
|
||||
assert.deepEqual(f.database.state.local, { a: "a.md", b: "b.md" });
|
||||
});
|
||||
|
|
@ -604,10 +605,10 @@ for (const boundary of ["before:save", "durable:save"]) {
|
|||
};
|
||||
f.database.state.lastSeenUpdateId = 1;
|
||||
await f.disk.userWrite("a.md", bytes("base A"));
|
||||
f.internals.stopped = false;
|
||||
await f.internals.preparePush();
|
||||
f.internals.scheduler.phase = SyncPhase.Idle;
|
||||
await f.internals.prepareNextUpload();
|
||||
const { requestId } = f.database.state.pending!.request;
|
||||
await assert.rejects(f.internals.finishPending(), /413/);
|
||||
await assert.rejects(f.internals.uploads.completePendingUpload(), /413/);
|
||||
await f.disk.userWrite("a.md", bytes("base"));
|
||||
const first = {
|
||||
headEventId: 3,
|
||||
|
|
@ -639,15 +640,15 @@ for (const boundary of ["before:save", "durable:save"]) {
|
|||
if (label === boundary) throw new Error("injected crash");
|
||||
};
|
||||
await assert.rejects(
|
||||
f.internals.finishPending(),
|
||||
f.internals.uploads.completePendingUpload(),
|
||||
/injected crash/
|
||||
);
|
||||
f.persistence.boundary = () => { };
|
||||
f.disk.crash(true);
|
||||
f = await fixture({}, service, f);
|
||||
assert.equal(f.database.state.lastSeenUpdateId, 1);
|
||||
await f.internals.finishPending();
|
||||
await f.internals.incorporateEventBatch(first);
|
||||
await f.internals.uploads.completePendingUpload();
|
||||
await f.internals.applyEventBatch(first);
|
||||
assert.equal(f.database.state.lastSeenUpdateId, 3);
|
||||
assert.deepEqual(
|
||||
(await f.disk.read("a.md"))?.content,
|
||||
|
|
@ -753,21 +754,21 @@ for (const retry of [false, true]) {
|
|||
};
|
||||
f.database.state.lastSeenUpdateId = 1;
|
||||
await f.disk.userWrite("a.md", bytes("edited"));
|
||||
f.internals.stopped = false;
|
||||
await f.internals.preparePush();
|
||||
if (retry) await assert.rejects(f.internals.finishPending(), /413/);
|
||||
f.internals.scheduler.phase = SyncPhase.Idle;
|
||||
await f.internals.prepareNextUpload();
|
||||
if (retry) await assert.rejects(f.internals.uploads.completePendingUpload(), /413/);
|
||||
// Recovery folds queued deletions into the durable namespace before
|
||||
// an outstanding request is retried.
|
||||
f.internals.stopped = true;
|
||||
f.internals.scheduler.phase = SyncPhase.Stopped;
|
||||
await f.disk.userDelete("a.md");
|
||||
await f.syncer.syncLocallyDeletedFile("a.md");
|
||||
await f.internals.scan();
|
||||
await f.internals.refreshLocalFiles();
|
||||
assert.deepEqual(f.database.state.local, {});
|
||||
if (retry) {
|
||||
reject = false;
|
||||
await f.syncer.retryRejectedRequests();
|
||||
}
|
||||
await f.internals.finishPending();
|
||||
await f.internals.uploads.completePendingUpload();
|
||||
assert.equal(f.database.state.pending, undefined);
|
||||
assert.deepEqual(f.database.state.local, {});
|
||||
assert.equal(await f.disk.read("a.md"), undefined);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import type { UploadQueue } from "../sync-client/src/sync-operations/upload-queue";
|
||||
import type { ContentSync } from "../sync-client/src/sync-operations/content-sync";
|
||||
import { fetchRemoteChanges, type RemoteApplicationOptions } from "../sync-client/src/sync-operations/remote-changes";
|
||||
import type { EventBatch } from "../sync-client/src/services/types/EventBatch";
|
||||
import type { FileManifest } from "../sync-client/src/services/types/FileManifest";
|
||||
import type { VaultSnapshot } from "../sync-client/src/services/types/VaultSnapshot";
|
||||
import { SyncPhase } from "../sync-client/src/sync-operations/sync-scheduler";
|
||||
import {
|
||||
Database,
|
||||
emptyState,
|
||||
createEmptyDatabase,
|
||||
type StoredDatabase
|
||||
} from "../sync-client/src/persistence/database";
|
||||
import { Settings } from "../sync-client/src/persistence/settings";
|
||||
|
|
@ -35,7 +42,7 @@ export async function fixture(
|
|||
const disk = recovered?.disk ?? new MemoryDisk();
|
||||
const initial = recovered
|
||||
? (recovered.persistence.snapshot().database as StoredDatabase)
|
||||
: emptyState("test");
|
||||
: createEmptyDatabase("test");
|
||||
if (!recovered) {
|
||||
initial.lastSeenUpdateId = 0;
|
||||
for (const [path, content] of Object.entries(contents)) {
|
||||
|
|
@ -99,19 +106,14 @@ export async function fixture(
|
|||
{ entries: changes, save: async () => {} }
|
||||
);
|
||||
const internals = syncer as unknown as {
|
||||
finishPending: () => Promise<void>;
|
||||
preparePush: () => Promise<boolean>;
|
||||
scan: (
|
||||
content: ContentSync;
|
||||
uploads: UploadQueue;
|
||||
prepareNextUpload: () => Promise<boolean>;
|
||||
refreshLocalFiles: (
|
||||
initial?: import("../sync-client/src/services/types/VaultSnapshot").VaultSnapshot
|
||||
) => Promise<void>;
|
||||
incorporateEventBatch: (
|
||||
batch: import("../sync-client/src/services/types/EventBatch").EventBatch
|
||||
) => Promise<void>;
|
||||
stopped: boolean;
|
||||
reconcile: (remote: {
|
||||
fileManifestId: number;
|
||||
entries: Record<string, string>;
|
||||
}) => Promise<void>;
|
||||
scheduler: { phase: SyncPhase };
|
||||
applyRemoteChanges: (remote: FileManifest, options?: RemoteApplicationOptions) => Promise<void>;
|
||||
};
|
||||
return {
|
||||
disk,
|
||||
|
|
@ -119,7 +121,18 @@ export async function fixture(
|
|||
files,
|
||||
settings,
|
||||
syncer,
|
||||
internals,
|
||||
internals: {
|
||||
content: internals.content,
|
||||
uploads: internals.uploads,
|
||||
scheduler: internals.scheduler,
|
||||
prepareNextUpload: () => internals.prepareNextUpload(),
|
||||
refreshLocalFiles: (initial?: VaultSnapshot) => internals.refreshLocalFiles(initial),
|
||||
applyRemoteChanges: (manifest: FileManifest) => internals.applyRemoteChanges(manifest),
|
||||
applyEventBatch: async (batch: EventBatch) => {
|
||||
const remote = await fetchRemoteChanges(service as SyncService, database.state, batch);
|
||||
await internals.applyRemoteChanges(remote.manifest, remote);
|
||||
}
|
||||
},
|
||||
persistence,
|
||||
changes,
|
||||
contentCache
|
||||
|
|
|
|||
Loading…
Reference in a new issue