This commit is contained in:
Andras Schmelczer 2026-04-06 13:01:47 +01:00
parent 0e3e5a99cd
commit d034ad5cb3
50 changed files with 6515 additions and 1492 deletions

View file

@ -1,46 +1,443 @@
import { describe, it } from "node:test";
import assert from "node:assert";
import { SyncEventQueue, type SyncEvent } from "./sync-event-queue";
import { SyncEventQueue } from "./sync-event-queue";
import { Settings } from "../persistence/settings";
import { Logger } from "../tracing/logger";
import type { DocumentVersionWithoutContent } from "../services/types/DocumentVersionWithoutContent";
import { SyncEventType } from "./types";
function createQueue(ignorePatterns: string[] = []): SyncEventQueue {
const logger = new Logger();
const settings = new Settings(logger, { ignorePatterns }, async () => {});
return new SyncEventQueue(settings, logger, undefined, async () => {});
}
function fakeRemoteVersion(
documentId: string,
overrides: Partial<DocumentVersionWithoutContent> = {}
): DocumentVersionWithoutContent {
return {
vaultUpdateId: 1,
documentId,
relativePath: `${documentId}.md`,
updatedDate: "2026-01-01",
isDeleted: false,
userId: "user",
deviceId: "device",
contentSize: 100,
...overrides
};
}
describe("SyncEventQueue", () => {
it("delete collapses interleaved events for one document while leaving the other intact", () => {
const queue = new SyncEventQueue();
queue.enqueue({ type: "local-content-update", documentId: "A" });
queue.enqueue({ type: "remote-content-update", documentId: "B" });
queue.enqueue({ type: "local-content-update", documentId: "A" });
queue.enqueue({ type: "move", documentId: "A" });
queue.enqueue({ type: "remote-content-update", documentId: "A" });
queue.enqueue({ type: "delete", documentId: "A" });
queue.enqueue({ type: "local-content-update", documentId: "B" });
assert.deepStrictEqual(queue.next(), { type: "delete", documentId: "A" });
assert.deepStrictEqual(queue.next(), {
type: "local-content-update",
documentId: "B"
it("sync-local followed by delete for the same document returns only the delete", () => {
const queue = createQueue();
queue.setDocument("a.md", {
documentId: "A",
parentVersionId: 1,
hash: "hash-a"
});
queue.enqueue({ type: SyncEventType.SyncLocal, documentId: "A" });
queue.enqueue({ type: SyncEventType.SyncLocal, documentId: "A" });
queue.enqueue({
type: SyncEventType.Delete,
documentId: "A",
path: "a.md",
});
const event = queue.next();
assert.strictEqual(event?.type, SyncEventType.Delete);
if (event?.type === SyncEventType.Delete) {
assert.strictEqual(event.documentId, "A");
}
assert.strictEqual(queue.next(), undefined);
});
it("updates coalesce up to a move boundary then post-move events are processed separately", () => {
const queue = new SyncEventQueue();
queue.enqueue({ type: "local-content-update", documentId: "X" });
queue.enqueue({ type: "remote-content-update", documentId: "X" });
queue.enqueue({ type: "file-create", path: "new.md" });
queue.enqueue({ type: "local-content-update", documentId: "X" });
queue.enqueue({ type: "move", documentId: "X" });
queue.enqueue({ type: "remote-content-update", documentId: "X" });
queue.enqueue({ type: "local-content-update", documentId: "X" });
it("sync-local events for the same document coalesce to one", () => {
const queue = createQueue();
queue.setDocument("a.md", {
documentId: "A",
parentVersionId: 1,
hash: "hash-a"
});
assert.deepStrictEqual(queue.next(), {
type: "local-content-update",
documentId: "X"
});
assert.deepStrictEqual(queue.next(), { type: "file-create", path: "new.md" });
assert.deepStrictEqual(queue.next(), { type: "move", documentId: "X" });
assert.deepStrictEqual(queue.next(), {
type: "local-content-update",
documentId: "X"
});
queue.enqueue({ type: SyncEventType.SyncLocal, documentId: "A" });
queue.enqueue({ type: SyncEventType.SyncLocal, documentId: "A" });
queue.enqueue({ type: SyncEventType.SyncLocal, documentId: "A" });
const event = queue.next();
assert.strictEqual(event?.type, SyncEventType.SyncLocal);
assert.strictEqual(queue.next(), undefined);
});
it("sync-remote events for the same documentId coalesce to the last one", () => {
const queue = createQueue();
queue.enqueue({
type: SyncEventType.SyncRemote,
remoteVersion: fakeRemoteVersion("A", { vaultUpdateId: 1 })
});
queue.enqueue({
type: SyncEventType.SyncRemote,
remoteVersion: fakeRemoteVersion("A", { vaultUpdateId: 2 })
});
queue.enqueue({
type: SyncEventType.SyncRemote,
remoteVersion: fakeRemoteVersion("A", { vaultUpdateId: 3 })
});
const event = queue.next();
assert.strictEqual(event?.type, SyncEventType.SyncRemote);
if (event?.type === SyncEventType.SyncRemote) {
assert.strictEqual(event.remoteVersion.vaultUpdateId, 3);
}
assert.strictEqual(queue.next(), undefined);
});
it("create events are returned FIFO", () => {
const queue = createQueue();
queue.enqueue({ type: SyncEventType.Create, path: "a.md" });
queue.enqueue({ type: SyncEventType.Create, path: "b.md" });
const first = queue.next();
assert.strictEqual(first?.type, SyncEventType.Create);
if (first?.type === SyncEventType.Create) {
assert.strictEqual(first.path, "a.md");
}
const second = queue.next();
assert.strictEqual(second?.type, SyncEventType.Create);
if (second?.type === SyncEventType.Create) {
assert.strictEqual(second.path, "b.md");
}
});
it("duplicate creates for the same path are skipped", () => {
const queue = createQueue();
queue.enqueue({ type: SyncEventType.Create, path: "a.md" });
queue.enqueue({ type: SyncEventType.Create, path: "a.md" });
assert.strictEqual(queue.size, 1);
});
it("create is skipped if the path already has a tracked document", () => {
const queue = createQueue();
queue.setDocument("a.md", {
documentId: "A",
parentVersionId: 1,
hash: "hash-a"
});
queue.enqueue({ type: SyncEventType.Create, path: "a.md" });
assert.strictEqual(queue.size, 0);
});
it("delete uses the provided documentId", () => {
const queue = createQueue();
queue.enqueue({
type: SyncEventType.Delete,
documentId: "A",
path: "a.md",
});
const event = queue.next();
assert.strictEqual(event?.type, SyncEventType.Delete);
if (event?.type === SyncEventType.Delete) {
assert.strictEqual(event.documentId, "A");
}
});
it("updateCreatePath updates the path of a create event in the queue", () => {
const queue = createQueue();
queue.enqueue({ type: SyncEventType.Create, path: "old.md" });
const updated = queue.updateCreatePath("old.md", "new.md");
assert.strictEqual(updated, true);
assert.strictEqual(queue.hasCreateEvent("old.md"), false);
assert.strictEqual(queue.hasCreateEvent("new.md"), true);
const event = queue.next();
assert.strictEqual(event?.type, SyncEventType.Create);
if (event?.type === SyncEventType.Create) {
assert.strictEqual(event.path, "new.md");
}
});
it("updateCreatePath returns false when no create event exists", () => {
const queue = createQueue();
const updated = queue.updateCreatePath("old.md", "new.md");
assert.strictEqual(updated, false);
});
it("hasCreateEvent detects pending creates", () => {
const queue = createQueue();
assert.strictEqual(queue.hasCreateEvent("a.md"), false);
queue.enqueue({ type: SyncEventType.Create, path: "a.md" });
assert.strictEqual(queue.hasCreateEvent("a.md"), true);
queue.next();
assert.strictEqual(queue.hasCreateEvent("a.md"), false);
});
it("document store CRUD operations work correctly", () => {
const queue = createQueue();
assert.strictEqual(queue.getDocument("a.md"), undefined);
assert.strictEqual(queue.documentCount, 0);
queue.setDocument("a.md", {
documentId: "A",
parentVersionId: 1,
hash: "hash-a"
});
assert.strictEqual(queue.documentCount, 1);
assert.deepStrictEqual(queue.getDocument("a.md"), {
documentId: "A",
parentVersionId: 1,
hash: "hash-a"
});
const found = queue.getDocumentByDocumentId("A");
assert.strictEqual(found?.path, "a.md");
assert.strictEqual(found?.record.documentId, "A");
queue.removeDocument("a.md");
assert.strictEqual(queue.documentCount, 0);
assert.strictEqual(queue.getDocument("a.md"), undefined);
});
it("moveDocument moves a document and returns displaced documentId", () => {
const queue = createQueue();
queue.setDocument("a.md", {
documentId: "A",
parentVersionId: 1,
hash: "hash-a"
});
queue.setDocument("b.md", {
documentId: "B",
parentVersionId: 2,
hash: "hash-b"
});
const displacedId = queue.moveDocument("a.md", "b.md");
assert.strictEqual(displacedId, "B");
assert.strictEqual(queue.getDocument("a.md"), undefined);
assert.strictEqual(queue.getDocument("b.md")?.documentId, "A");
assert.strictEqual(queue.documentCount, 1);
});
it("moveDocument returns undefined when target is unoccupied", () => {
const queue = createQueue();
queue.setDocument("a.md", {
documentId: "A",
parentVersionId: 1,
hash: "hash-a"
});
const displacedId = queue.moveDocument("a.md", "b.md");
assert.strictEqual(displacedId, undefined);
assert.strictEqual(queue.getDocument("b.md")?.documentId, "A");
});
it("interleaved events for different documents are not confused", () => {
const queue = createQueue();
queue.setDocument("a.md", {
documentId: "A",
parentVersionId: 1,
hash: "hash-a"
});
queue.setDocument("b.md", {
documentId: "B",
parentVersionId: 2,
hash: "hash-b"
});
queue.enqueue({ type: SyncEventType.SyncLocal, documentId: "A" });
queue.enqueue({ type: SyncEventType.SyncLocal, documentId: "B" });
queue.enqueue({
type: SyncEventType.Delete,
documentId: "A",
path: "a.md",
});
queue.enqueue({ type: SyncEventType.SyncLocal, documentId: "B" });
// First next() should see the delete for A (coalescing sync-local + delete)
const first = queue.next();
assert.strictEqual(first?.type, SyncEventType.Delete);
if (first?.type === SyncEventType.Delete) {
assert.strictEqual(first.documentId, "A");
}
// Remaining should be the coalesced sync-local for B
const second = queue.next();
assert.strictEqual(second?.type, SyncEventType.SyncLocal);
if (second?.type === SyncEventType.SyncLocal) {
assert.strictEqual(second.documentId, "B");
}
assert.strictEqual(queue.next(), undefined);
});
it("delete discards subsequent sync-remote events for the same document", () => {
const queue = createQueue();
queue.enqueue({
type: SyncEventType.Delete,
documentId: "A",
path: "a.md",
});
queue.enqueue({
type: SyncEventType.SyncRemote,
remoteVersion: fakeRemoteVersion("A", { vaultUpdateId: 5 })
});
const event = queue.next();
assert.strictEqual(event?.type, SyncEventType.Delete);
assert.strictEqual(queue.next(), undefined);
});
it("delete discards subsequent sync-local and sync-remote for the same document", () => {
const queue = createQueue();
queue.setDocument("a.md", {
documentId: "A",
parentVersionId: 1,
hash: "hash-a"
});
queue.enqueue({
type: SyncEventType.Delete,
documentId: "A",
path: "a.md",
});
queue.enqueue({ type: SyncEventType.SyncLocal, documentId: "A" });
queue.enqueue({ type: SyncEventType.Create, path: "b.md" });
queue.enqueue({
type: SyncEventType.SyncRemote,
remoteVersion: fakeRemoteVersion("A", { vaultUpdateId: 5 })
});
const first = queue.next();
assert.strictEqual(first?.type, SyncEventType.Delete);
// Only the unrelated create should remain
const second = queue.next();
assert.strictEqual(second?.type, SyncEventType.Create);
assert.strictEqual(queue.next(), undefined);
});
it("delete with empty documentId does not discard other events", () => {
const queue = createQueue();
queue.setDocument("a.md", {
documentId: "A",
parentVersionId: 1,
hash: "hash-a"
});
queue.enqueue({
type: SyncEventType.Delete,
documentId: "",
path: "unknown.md",
});
queue.enqueue({ type: SyncEventType.SyncLocal, documentId: "A" });
queue.next();
const second = queue.next();
assert.strictEqual(second?.type, SyncEventType.SyncLocal);
});
it("create can be re-enqueued after being dequeued", () => {
const queue = createQueue();
queue.enqueue({ type: SyncEventType.Create, path: "a.md" });
queue.next();
queue.enqueue({ type: SyncEventType.Create, path: "a.md" });
assert.strictEqual(queue.size, 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.Create, path: "notes-new.md" });
assert.strictEqual(queue.size, 1);
queue.enqueue({
type: SyncEventType.SyncRemote,
remoteVersion: fakeRemoteVersion("N")
});
assert.strictEqual(queue.size, 2);
});
it("clear removes events but keeps documents", () => {
const queue = createQueue();
queue.setDocument("a.md", {
documentId: "A",
parentVersionId: 1,
hash: "hash-a"
});
queue.enqueue({ type: SyncEventType.Create, path: "b.md" });
queue.enqueue({ type: SyncEventType.SyncLocal, documentId: "A" });
assert.strictEqual(queue.size, 2);
queue.clear();
assert.strictEqual(queue.size, 0);
assert.strictEqual(queue.documentCount, 1);
assert.strictEqual(queue.getDocument("a.md")?.documentId, "A");
});
it("allDocuments returns all tracked documents", () => {
const queue = createQueue();
queue.setDocument("a.md", {
documentId: "A",
parentVersionId: 1,
hash: "hash-a"
});
queue.setDocument("b.md", {
documentId: "B",
parentVersionId: 2,
hash: "hash-b"
});
const docs = queue.allDocuments();
assert.strictEqual(docs.length, 2);
const paths = docs.map(([p]) => p).sort();
assert.deepStrictEqual(paths, ["a.md", "b.md"]);
});
it("loads initial state from persistence", () => {
const logger = new Logger();
const settings = new Settings(logger, {}, async () => {});
const queue = new SyncEventQueue(settings, logger, {
documents: [
{
relativePath: "a.md",
documentId: "A",
parentVersionId: 5,
hash: "hash-a"
},
{
relativePath: "b.md",
documentId: "B",
parentVersionId: 3,
hash: "hash-b"
}
],
lastSeenUpdateId: 4
}, async () => {});
assert.strictEqual(queue.documentCount, 2);
assert.strictEqual(queue.getDocument("a.md")?.documentId, "A");
assert.strictEqual(queue.getDocument("b.md")?.documentId, "B");
assert.strictEqual(queue.getLastSeenUpdateId(), 5);
});
});