This commit is contained in:
Andras Schmelczer 2026-09-27 08:29:17 +01:00
commit 3dab48f58b

View file

@ -1,71 +1,79 @@
/* eslint-disable @typescript-eslint/no-unsafe-type-assertion */
import assert from "node:assert/strict";
import { it } from "node:test";
import type { Database } from "../persistence/database";
import type {
Database,
RelativePath,
VaultUpdateId
} from "../persistence/database";
import type { FileOperations } from "../file-operations/file-operations";
import type { WebSocketManager } from "../services/websocket-manager";
import type { ClientCursors } from "../services/types/ClientCursors";
import { DocumentUpToDateness } from "../types/document-up-to-dateness";
import { EventListeners } from "../utils/data-structures/event-listeners";
import { hash } from "../utils/hash";
import { CursorTracker } from "./cursor-tracker";
import { FileChangeNotifier } from "./file-change-notifier";
it("retains future cursors and publishes them when content catches up", async () => {
it("keeps the last usable cursor while retaining only the latest future update", async (context) => {
const content = new TextEncoder().encode("current");
const contentHash = await hash(content);
let version = 1;
const database = {
getDocumentByDocumentId: () => ({
documentId: "document",
relativePath: "note.md",
metadata: { parentVersionId: 2, hash: contentHash }
metadata: { parentVersionId: version, hash: contentHash }
})
} as unknown as Database;
const websocket = {
onWebSocketStatusChanged: new EventListeners<
(connected: boolean) => unknown
>(),
onWebSocketStatusChanged: new EventListeners(),
onRemoteCursorsUpdateReceived: new EventListeners<
(cursors: ClientCursors[]) => Promise<void>
>(),
updateLocalCursors: () => {}
} as unknown as WebSocketManager;
const files = { read: async () => content } as unknown as FileOperations;
const notifier = new FileChangeNotifier();
const tracker = new CursorTracker(database, websocket, files, notifier);
const future = {
const notifier = new EventListeners<(path: RelativePath) => unknown>();
const tracker = new CursorTracker(
database,
websocket,
{ read: async () => content } as unknown as FileOperations,
notifier
);
context.after(() => tracker.reset());
let received: ClientCursors[] = [];
tracker.onRemoteCursorsUpdated.add((cursors) => {
received = cursors;
});
const cursor = (version: number): ClientCursors => ({
userName: "Other",
deviceId: "device",
documentsWithCursors: [
{
document_id: "document",
relative_path: "old-name.md",
vault_update_id: 2,
cursors: [{ start: 0, end: 1 }]
vault_update_id: version,
cursors: [{ start: version, end: version }]
}
],
upToDateness: DocumentUpToDateness.Later
};
const internals = tracker as unknown as {
knownRemoteCursors: (typeof future)[];
getRelevantAndPruneKnownClientCursors: () => ClientCursors[];
};
internals.knownRemoteCursors = [future];
assert.deepEqual(internals.getRelevantAndPruneKnownClientCursors(), []);
assert.strictEqual(internals.knownRemoteCursors.length, 1);
let received: ClientCursors[] = [];
tracker.onRemoteCursorsUpdated.add((cursors) => {
received = cursors;
]
});
await notifier.onFileChanged.triggerAsync("note.md");
assert.strictEqual(received.length, 1);
assert.strictEqual(
received[0].documentsWithCursors[0].relative_path,
"note.md"
for (const remoteVersion of [1, 2, 3]) {
await websocket.onRemoteCursorsUpdateReceived.triggerAsync([
cursor(remoteVersion)
]);
assert.equal(received[0].documentsWithCursors[0].vault_update_id, 1);
}
version = 2;
await notifier.triggerAsync("note.md");
assert.equal(
received[0].documentsWithCursors[0].vault_update_id,
1,
"superseded future cursors must not reappear"
);
version = 3;
await notifier.triggerAsync("note.md");
assert.equal(received[0].documentsWithCursors[0].vault_update_id, 3);
assert.equal(received[0].documentsWithCursors[0].relative_path, "note.md");
await websocket.onRemoteCursorsUpdateReceived.triggerAsync([]);
assert.deepEqual(received, []);
});
it("refreshes unchanged cursors on reconnect and heartbeat, and follows the document through renames", async (context) => {
@ -79,14 +87,14 @@ it("refreshes unchanged cursors on reconnect and heartbeat, and follows the docu
});
const contentHash = await hash(content);
const database = {
getLatestDocumentByRelativePath: (candidate: string) =>
getLatestDocumentByRelativePath: (candidate: RelativePath) =>
candidate === path ? record() : undefined,
getDocumentByDocumentId: () => record()
} as unknown as Database;
const sent: {
documentsWithCursors: {
relative_path: string;
vault_update_id: number | null;
relative_path: RelativePath;
vault_update_id: VaultUpdateId | null;
}[];
}[] = [];
const websocket = {
@ -98,7 +106,7 @@ it("refreshes unchanged cursors on reconnect and heartbeat, and follows the docu
sent.push(value);
}
} as unknown as WebSocketManager;
const notifier = new FileChangeNotifier();
const notifier = new EventListeners<(path: RelativePath) => unknown>();
const tracker = new CursorTracker(
database,
websocket,
@ -125,7 +133,7 @@ it("refreshes unchanged cursors on reconnect and heartbeat, and follows the docu
await drain();
assert.equal(sent.length, 3);
path = "renamed.md";
await notifier.onFileChanged.triggerAsync(path);
await notifier.triggerAsync(path);
assert.equal(sent.at(-1)!.documentsWithCursors[0].relative_path, path);
websocket.onWebSocketStatusChanged.trigger(false);
const count = sent.length;
@ -173,7 +181,7 @@ for (const interruption of ["disconnect", "reset"] as const) {
return content;
}
} as unknown as FileOperations,
new FileChangeNotifier()
new EventListeners<(path: RelativePath) => unknown>()
);
let received: ClientCursors[] = [];
tracker.onRemoteCursorsUpdated.add((cursors) => {
@ -243,7 +251,7 @@ it("a remote cursor becomes outdated if the document advances during its snapsho
return content;
}
} as unknown as FileOperations,
new FileChangeNotifier()
new EventListeners<(path: RelativePath) => unknown>()
);
let outdated: boolean | undefined;
tracker.onRemoteCursorsUpdated.add((cursors) => {