This commit is contained in:
Andras Schmelczer 2026-04-24 20:56:03 +01:00
parent a7b588da97
commit 19d5dc1999
11 changed files with 358 additions and 355 deletions

View file

@ -38,13 +38,13 @@ describe("SyncEventQueue", () => {
remoteHash: "hash-a"
});
queue.enqueue({ type: SyncEventType.SyncLocal, path: "a.md" });
queue.enqueue({ type: SyncEventType.SyncLocal, path: "a.md" });
queue.enqueue({ type: SyncEventType.Delete, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalUpdate, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalUpdate, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalDelete, path: "a.md" });
const event = await queue.next();
assert.strictEqual(event?.type, SyncEventType.Delete);
if (event?.type === SyncEventType.Delete) {
assert.strictEqual(event?.type, SyncEventType.LocalDelete);
if (event?.type === SyncEventType.LocalDelete) {
assert.strictEqual(event.documentId, "A");
}
assert.strictEqual(await queue.next(), undefined);
@ -58,34 +58,34 @@ describe("SyncEventQueue", () => {
remoteHash: "hash-a"
});
queue.enqueue({ type: SyncEventType.SyncLocal, path: "a.md" });
queue.enqueue({ type: SyncEventType.SyncLocal, path: "a.md" });
queue.enqueue({ type: SyncEventType.SyncLocal, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalUpdate, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalUpdate, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalUpdate, path: "a.md" });
const event = await queue.next();
assert.strictEqual(event?.type, SyncEventType.SyncLocal);
assert.strictEqual(event?.type, SyncEventType.LocalUpdate);
assert.strictEqual(await queue.next(), undefined);
});
it("sync-remote events for the same documentId coalesce to the last one", async () => {
it("sync-remote-content events for the same documentId coalesce to the last one", async () => {
const queue = createQueue();
queue.enqueue({
type: SyncEventType.SyncRemote,
type: SyncEventType.RemoteUpdate,
remoteVersion: fakeRemoteVersion("A", { vaultUpdateId: 1 })
});
queue.enqueue({
type: SyncEventType.SyncRemote,
type: SyncEventType.RemoteUpdate,
remoteVersion: fakeRemoteVersion("A", { vaultUpdateId: 2 })
});
queue.enqueue({
type: SyncEventType.SyncRemote,
type: SyncEventType.RemoteUpdate,
remoteVersion: fakeRemoteVersion("A", { vaultUpdateId: 3 })
});
const event = await queue.next();
assert.strictEqual(event?.type, SyncEventType.SyncRemote);
if (event?.type === SyncEventType.SyncRemote) {
assert.strictEqual(event?.type, SyncEventType.RemoteUpdate);
if (event?.type === SyncEventType.RemoteUpdate) {
assert.strictEqual(event.remoteVersion.vaultUpdateId, 3);
}
assert.strictEqual(await queue.next(), undefined);
@ -93,18 +93,18 @@ describe("SyncEventQueue", () => {
it("create events are returned FIFO", async () => {
const queue = createQueue();
queue.enqueue({ type: SyncEventType.Create, path: "a.md" });
queue.enqueue({ type: SyncEventType.Create, path: "b.md" });
queue.enqueue({ type: SyncEventType.LocalCreate, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalCreate, path: "b.md" });
const first = await queue.next();
assert.strictEqual(first?.type, SyncEventType.Create);
if (first?.type === SyncEventType.Create) {
assert.strictEqual(first?.type, SyncEventType.LocalCreate);
if (first?.type === SyncEventType.LocalCreate) {
assert.strictEqual(first.path, "a.md");
}
const second = await queue.next();
assert.strictEqual(second?.type, SyncEventType.Create);
if (second?.type === SyncEventType.Create) {
assert.strictEqual(second?.type, SyncEventType.LocalCreate);
if (second?.type === SyncEventType.LocalCreate) {
assert.strictEqual(second.path, "b.md");
}
});
@ -117,33 +117,33 @@ describe("SyncEventQueue", () => {
remoteHash: "hash-a"
});
queue.enqueue({ type: SyncEventType.Delete, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalDelete, path: "a.md" });
const event = await queue.next();
assert.strictEqual(event?.type, SyncEventType.Delete);
if (event?.type === SyncEventType.Delete) {
assert.strictEqual(event?.type, SyncEventType.LocalDelete);
if (event?.type === SyncEventType.LocalDelete) {
assert.strictEqual(event.documentId, "A");
}
});
it("delete for unknown path is silently ignored", () => {
const queue = createQueue();
queue.enqueue({ type: SyncEventType.Delete, path: "unknown.md" });
assert.strictEqual(queue.size, 0);
queue.enqueue({ type: SyncEventType.LocalDelete, path: "unknown.md" });
assert.strictEqual(queue.pendingUpdateCount, 0);
});
it("document store CRUD operations work correctly", () => {
const queue = createQueue();
assert.strictEqual(queue.getSettledDocumentByPath("a.md"), undefined);
assert.strictEqual(queue.documentCount, 0);
assert.strictEqual(queue.syncedDocumentCount, 0);
queue.setDocument("a.md", {
documentId: "A",
parentVersionId: 1,
remoteHash: "hash-a"
});
assert.strictEqual(queue.documentCount, 1);
assert.strictEqual(queue.syncedDocumentCount, 1);
assert.deepStrictEqual(queue.getSettledDocumentByPath("a.md"), {
documentId: "A",
parentVersionId: 1,
@ -155,7 +155,7 @@ describe("SyncEventQueue", () => {
assert.strictEqual(found?.record.documentId, "A");
queue.removeDocument("a.md");
assert.strictEqual(queue.documentCount, 0);
assert.strictEqual(queue.syncedDocumentCount, 0);
assert.strictEqual(queue.getSettledDocumentByPath("a.md"), undefined);
});
@ -167,7 +167,7 @@ describe("SyncEventQueue", () => {
remoteHash: "hash-a"
});
queue.enqueue({ type: SyncEventType.SyncLocal, path: "b.md", oldPath: "a.md" });
queue.enqueue({ type: SyncEventType.LocalUpdate, path: "b.md", oldPath: "a.md" });
assert.strictEqual(queue.getSettledDocumentByPath("a.md"), undefined);
assert.strictEqual(queue.getSettledDocumentByPath("b.md")?.documentId, "A");
});
@ -185,29 +185,29 @@ describe("SyncEventQueue", () => {
remoteHash: "hash-b"
});
queue.enqueue({ type: SyncEventType.SyncLocal, path: "a.md" });
queue.enqueue({ type: SyncEventType.SyncLocal, path: "b.md" });
queue.enqueue({ type: SyncEventType.Delete, path: "a.md" });
queue.enqueue({ type: SyncEventType.SyncLocal, path: "b.md" });
queue.enqueue({ type: SyncEventType.LocalUpdate, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalUpdate, path: "b.md" });
queue.enqueue({ type: SyncEventType.LocalDelete, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalUpdate, path: "b.md" });
// First next() should see the delete for A (coalescing sync-local + delete)
const first = await queue.next();
assert.strictEqual(first?.type, SyncEventType.Delete);
if (first?.type === SyncEventType.Delete) {
assert.strictEqual(first?.type, SyncEventType.LocalDelete);
if (first?.type === SyncEventType.LocalDelete) {
assert.strictEqual(first.documentId, "A");
}
// Remaining should be the coalesced sync-local for B
const second = await queue.next();
assert.strictEqual(second?.type, SyncEventType.SyncLocal);
if (second?.type === SyncEventType.SyncLocal) {
assert.strictEqual(second?.type, SyncEventType.LocalUpdate);
if (second?.type === SyncEventType.LocalUpdate) {
assert.strictEqual(second.documentId, "B");
}
assert.strictEqual(await queue.next(), undefined);
});
it("delete discards subsequent sync-remote events for the same document", async () => {
it("delete discards subsequent sync-remote-content events for the same document", async () => {
const queue = createQueue();
queue.setDocument("a.md", {
documentId: "A",
@ -215,18 +215,18 @@ describe("SyncEventQueue", () => {
remoteHash: "hash-a"
});
queue.enqueue({ type: SyncEventType.Delete, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalDelete, path: "a.md" });
queue.enqueue({
type: SyncEventType.SyncRemote,
type: SyncEventType.RemoteUpdate,
remoteVersion: fakeRemoteVersion("A", { vaultUpdateId: 5 })
});
const event = await queue.next();
assert.strictEqual(event?.type, SyncEventType.Delete);
assert.strictEqual(event?.type, SyncEventType.LocalDelete);
assert.strictEqual(await queue.next(), undefined);
});
it("delete discards subsequent sync-local and sync-remote for the same document", async () => {
it("delete discards subsequent sync-local and sync-remote-content for the same document", async () => {
const queue = createQueue();
queue.setDocument("a.md", {
documentId: "A",
@ -234,20 +234,20 @@ describe("SyncEventQueue", () => {
remoteHash: "hash-a"
});
queue.enqueue({ type: SyncEventType.Delete, path: "a.md" });
queue.enqueue({ type: SyncEventType.SyncLocal, path: "a.md" });
queue.enqueue({ type: SyncEventType.Create, path: "b.md" });
queue.enqueue({ type: SyncEventType.LocalDelete, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalUpdate, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalCreate, path: "b.md" });
queue.enqueue({
type: SyncEventType.SyncRemote,
type: SyncEventType.RemoteUpdate,
remoteVersion: fakeRemoteVersion("A", { vaultUpdateId: 5 })
});
const first = await queue.next();
assert.strictEqual(first?.type, SyncEventType.Delete);
assert.strictEqual(first?.type, SyncEventType.LocalDelete);
// Only the unrelated create should remain
const second = await queue.next();
assert.strictEqual(second?.type, SyncEventType.Create);
assert.strictEqual(second?.type, SyncEventType.LocalCreate);
assert.strictEqual(await queue.next(), undefined);
});
@ -260,30 +260,30 @@ describe("SyncEventQueue", () => {
});
// Create is pending — Delete for same path gets a promise documentId
queue.enqueue({ type: SyncEventType.Create, path: "unknown.md" });
queue.enqueue({ type: SyncEventType.Delete, path: "unknown.md" });
queue.enqueue({ type: SyncEventType.SyncLocal, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalCreate, path: "unknown.md" });
queue.enqueue({ type: SyncEventType.LocalDelete, path: "unknown.md" });
queue.enqueue({ type: SyncEventType.LocalUpdate, path: "a.md" });
// Dequeue and resolve the Create
const event = await queue.next();
assert.ok(event?.type === SyncEventType.Create);
assert.ok(event?.type === SyncEventType.LocalCreate);
event.resolvers!.resolve("NEW");
await queue.next(); // delete
const second = await queue.next();
assert.strictEqual(second?.type, SyncEventType.SyncLocal);
assert.strictEqual(second?.type, SyncEventType.LocalUpdate);
});
it("getCreatePromise returns a promise resolved by the event's resolvers", async () => {
const queue = createQueue();
queue.enqueue({ type: SyncEventType.Create, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalCreate, path: "a.md" });
const promise = queue.getCreatePromise("a.md");
assert.ok(promise !== undefined);
// The syncer resolves via event.resolvers after dequeuing
const event = await queue.next();
assert.ok(event?.type === SyncEventType.Create);
assert.ok(event?.type === SyncEventType.LocalCreate);
assert.ok(event.resolvers !== undefined);
event.resolvers.resolve("resolved-id");
@ -292,13 +292,13 @@ describe("SyncEventQueue", () => {
it("rejecting the event's resolvers rejects the create promise", async () => {
const queue = createQueue();
queue.enqueue({ type: SyncEventType.Create, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalCreate, path: "a.md" });
const promise = queue.getCreatePromise("a.md");
assert.ok(promise !== undefined);
const event = await queue.next();
assert.ok(event?.type === SyncEventType.Create);
assert.ok(event?.type === SyncEventType.LocalCreate);
assert.ok(event.resolvers !== undefined);
event.resolvers.promise.catch(() => { });
event.resolvers.reject(new Error("cancelled"));
@ -308,8 +308,8 @@ describe("SyncEventQueue", () => {
it("clear rejects all pending create promises", async () => {
const queue = createQueue();
queue.enqueue({ type: SyncEventType.Create, path: "a.md" });
queue.enqueue({ type: SyncEventType.Create, path: "b.md" });
queue.enqueue({ type: SyncEventType.LocalCreate, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalCreate, path: "b.md" });
const promiseA = queue.getCreatePromise("a.md");
const promiseB = queue.getCreatePromise("b.md");
@ -324,28 +324,28 @@ describe("SyncEventQueue", () => {
it("create can be re-enqueued after being dequeued", async () => {
const queue = createQueue();
queue.enqueue({ type: SyncEventType.Create, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalCreate, path: "a.md" });
await queue.next();
queue.enqueue({ type: SyncEventType.Create, path: "a.md" });
assert.strictEqual(queue.size, 1);
queue.enqueue({ type: SyncEventType.LocalCreate, path: "a.md" });
assert.strictEqual(queue.pendingUpdateCount, 1);
});
it("silently ignores create events matching ignore patterns", () => {
const queue = createQueue(["*.tmp", ".hidden/**"]);
queue.enqueue({ type: SyncEventType.Create, path: "scratch.tmp" });
queue.enqueue({ type: SyncEventType.Create, path: ".hidden/secret.md" });
assert.strictEqual(queue.size, 0);
queue.enqueue({ type: SyncEventType.LocalCreate, path: "scratch.tmp" });
queue.enqueue({ type: SyncEventType.LocalCreate, path: ".hidden/secret.md" });
assert.strictEqual(queue.pendingUpdateCount, 0);
queue.enqueue({ type: SyncEventType.Create, path: "notes-new.md" });
assert.strictEqual(queue.size, 1);
queue.enqueue({ type: SyncEventType.LocalCreate, path: "notes-new.md" });
assert.strictEqual(queue.pendingUpdateCount, 1);
queue.enqueue({
type: SyncEventType.SyncRemote,
type: SyncEventType.RemoteUpdate,
remoteVersion: fakeRemoteVersion("N")
});
assert.strictEqual(queue.size, 2);
assert.strictEqual(queue.pendingUpdateCount, 2);
});
it("clear removes events but keeps documents", () => {
@ -355,15 +355,15 @@ describe("SyncEventQueue", () => {
parentVersionId: 1,
remoteHash: "hash-a"
});
queue.enqueue({ type: SyncEventType.Create, path: "b.md" });
queue.enqueue({ type: SyncEventType.SyncLocal, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalCreate, path: "b.md" });
queue.enqueue({ type: SyncEventType.LocalUpdate, path: "a.md" });
assert.strictEqual(queue.size, 2);
assert.strictEqual(queue.pendingUpdateCount, 2);
queue.clear();
assert.strictEqual(queue.size, 0);
assert.strictEqual(queue.documentCount, 1);
assert.strictEqual(queue.pendingUpdateCount, 0);
assert.strictEqual(queue.syncedDocumentCount, 1);
assert.strictEqual(queue.getSettledDocumentByPath("a.md")?.documentId, "A");
});
@ -407,7 +407,7 @@ describe("SyncEventQueue", () => {
lastSeenUpdateId: 4
}, async () => { });
assert.strictEqual(queue.documentCount, 2);
assert.strictEqual(queue.syncedDocumentCount, 2);
assert.strictEqual(queue.getSettledDocumentByPath("a.md")?.documentId, "A");
assert.strictEqual(queue.getSettledDocumentByPath("b.md")?.documentId, "B");
assert.strictEqual(queue.lastSeenUpdateId, 5);
@ -427,9 +427,9 @@ describe("SyncEventQueue", () => {
});
// Pending create adds a path
queue.enqueue({ type: SyncEventType.Create, path: "c.md" });
queue.enqueue({ type: SyncEventType.LocalCreate, path: "c.md" });
// Pending delete removes a path
queue.enqueue({ type: SyncEventType.Delete, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalDelete, path: "a.md" });
const paths = queue.trackedPaths();
assert.deepStrictEqual(
@ -441,10 +441,10 @@ describe("SyncEventQueue", () => {
it("trackedPaths handles create-delete-create for the same path", () => {
const queue = createQueue();
queue.enqueue({ type: SyncEventType.Create, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalCreate, path: "a.md" });
// Delete gets promise documentId from pending Create
queue.enqueue({ type: SyncEventType.Delete, path: "a.md" });
queue.enqueue({ type: SyncEventType.Create, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalDelete, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalCreate, path: "a.md" });
const paths = queue.trackedPaths();
assert.ok(paths.has("a.md"));
@ -453,10 +453,10 @@ describe("SyncEventQueue", () => {
it("trackedPaths applies moves for pending SyncLocal events", () => {
const queue = createQueue();
queue.enqueue({ type: SyncEventType.Create, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalCreate, path: "a.md" });
// File was renamed from a.md to b.md
queue.enqueue({ type: SyncEventType.SyncLocal, path: "b.md", oldPath: "a.md" });
queue.enqueue({ type: SyncEventType.LocalUpdate, path: "b.md", oldPath: "a.md" });
const paths = queue.trackedPaths();
assert.ok(!paths.has("a.md"));
@ -466,10 +466,10 @@ describe("SyncEventQueue", () => {
it("trackedPaths tracks multiple moves for the same pending create", () => {
const queue = createQueue();
queue.enqueue({ type: SyncEventType.Create, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalCreate, path: "a.md" });
queue.enqueue({ type: SyncEventType.SyncLocal, path: "b.md", oldPath: "a.md" });
queue.enqueue({ type: SyncEventType.SyncLocal, path: "c.md", oldPath: "b.md" });
queue.enqueue({ type: SyncEventType.LocalUpdate, path: "b.md", oldPath: "a.md" });
queue.enqueue({ type: SyncEventType.LocalUpdate, path: "c.md", oldPath: "b.md" });
const paths = queue.trackedPaths();
assert.ok(!paths.has("a.md"));
@ -480,15 +480,15 @@ describe("SyncEventQueue", () => {
it("resolveCreate settles the document and replaces promise documentIds in the queue", async () => {
const queue = createQueue();
queue.enqueue({ type: SyncEventType.Create, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalCreate, path: "a.md" });
const createPromise = queue.getCreatePromise("a.md")!;
// Dependent events enqueued while create is still pending
queue.enqueue({ type: SyncEventType.SyncLocal, path: "a.md" });
queue.enqueue({ type: SyncEventType.Delete, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalUpdate, path: "a.md" });
queue.enqueue({ type: SyncEventType.LocalDelete, path: "a.md" });
const event = await queue.next(); // dequeue the create
assert.ok(event?.type === SyncEventType.Create);
assert.ok(event?.type === SyncEventType.LocalCreate);
queue.resolveCreate(event, {
documentId: "DOC-1",
@ -506,7 +506,7 @@ describe("SyncEventQueue", () => {
// The SyncLocal + Delete for "DOC-1" coalesce: sync-local is
// discarded and the delete is returned (standard coalescing).
const deleteEvt = await queue.next();
assert.ok(deleteEvt?.type === SyncEventType.Delete);
assert.ok(deleteEvt?.type === SyncEventType.LocalDelete);
assert.strictEqual(deleteEvt.documentId, "DOC-1");
assert.strictEqual(await queue.next(), undefined);