This commit is contained in:
Andras Schmelczer 2026-09-27 09:13:25 +01:00
commit 25f64ec690

View file

@ -88,7 +88,10 @@ it("keeps the last usable cursor while retaining only the latest future update",
assert.deepEqual(received, []); assert.deepEqual(received, []);
}); });
it("refreshes unchanged cursors on reconnect and heartbeat, and follows the document through renames", async (context) => { it(
"refreshes unchanged cursors on reconnect and heartbeat, and follows the document through renames",
{ timeout: 5_000 },
async (context) => {
context.mock.timers.enable({ apis: ["setInterval"] }); context.mock.timers.enable({ apis: ["setInterval"] });
const content = new TextEncoder().encode("hello"); const content = new TextEncoder().encode("hello");
let path = "note.md"; let path = "note.md";
@ -99,6 +102,7 @@ it("refreshes unchanged cursors on reconnect and heartbeat, and follows the docu
vault_update_id: VaultUpdateId | null; vault_update_id: VaultUpdateId | null;
}[]; }[];
}[] = []; }[] = [];
let published = Promise.withResolvers<undefined>();
const websocket = { const websocket = {
onWebSocketStatusChanged: new EventListeners< onWebSocketStatusChanged: new EventListeners<
(connected: boolean) => unknown (connected: boolean) => unknown
@ -106,18 +110,23 @@ it("refreshes unchanged cursors on reconnect and heartbeat, and follows the docu
onRemoteCursorsUpdateReceived: new EventListeners(), onRemoteCursorsUpdateReceived: new EventListeners(),
updateLocalCursors: (value: (typeof sent)[number]) => { updateLocalCursors: (value: (typeof sent)[number]) => {
sent.push(value); sent.push(value);
published.resolve(undefined);
} }
} as unknown as WebSocketManager; } as unknown as WebSocketManager;
const notifier = new EventListeners<(path: RelativePath) => unknown>(); const notifier = new EventListeners<(path: RelativePath) => unknown>();
const read = context.mock.fn(async () => ({ content }));
const tracker = new CursorTracker( const tracker = new CursorTracker(
database, database,
websocket, websocket,
{ read: async () => ({ content }) } as unknown as FileSystemOperations, { read } as unknown as FileSystemOperations,
notifier notifier
); );
const drain = async () => { const waitForSend = async (trigger: () => void): Promise<void> => {
for (let i = 0; i < 20; i++) published = Promise.withResolvers<undefined>();
await new Promise((resolve) => setImmediate(resolve)); trigger();
// Hashing finishes outside the event loop; a fixed number of turns
// cannot guarantee that the cursor snapshot has been published.
await published.promise;
}; };
try { try {
await tracker.sendLocalCursorsToServer({ await tracker.sendLocalCursorsToServer({
@ -128,29 +137,40 @@ it("refreshes unchanged cursors on reconnect and heartbeat, and follows the docu
"note.md": [{ start: 1, end: 2 }] "note.md": [{ start: 1, end: 2 }]
}); });
assert.equal(sent.length, 1); assert.equal(sent.length, 1);
await waitForSend(() => {
websocket.onWebSocketStatusChanged.trigger(true); websocket.onWebSocketStatusChanged.trigger(true);
await drain(); });
assert.equal(sent.length, 2); assert.equal(sent.length, 2);
await waitForSend(() => {
context.mock.timers.tick(15_000); context.mock.timers.tick(15_000);
await drain(); });
assert.equal(sent.length, 3); assert.equal(sent.length, 3);
path = "renamed.md"; path = "renamed.md";
database.state.local.document = path; database.state.local.document = path;
await notifier.triggerAsync(path); await notifier.triggerAsync(path);
assert.equal(sent.at(-1)!.documentsWithCursors[0].relative_path, path); assert.equal(
sent.at(-1)!.documentsWithCursors[0].relative_path,
path
);
websocket.onWebSocketStatusChanged.trigger(false); websocket.onWebSocketStatusChanged.trigger(false);
const count = sent.length; const count = sent.length;
const readCount = read.mock.callCount();
context.mock.timers.tick(30_000); context.mock.timers.tick(30_000);
await drain(); // A surviving heartbeat would start a snapshot read in the send lock's
// next microtask, even if hashing that snapshot has not finished yet.
await Promise.resolve();
assert.equal(read.mock.callCount(), readCount);
assert.equal(sent.length, count); assert.equal(sent.length, count);
await waitForSend(() => {
websocket.onWebSocketStatusChanged.trigger(true); websocket.onWebSocketStatusChanged.trigger(true);
await drain(); });
assert.equal(sent.length, count + 1); assert.equal(sent.length, count + 1);
} finally { } finally {
tracker.reset(); tracker.reset();
context.mock.timers.reset(); context.mock.timers.reset();
} }
}); }
);
for (const interruption of ["disconnect", "reset"] as const) { for (const interruption of ["disconnect", "reset"] as const) {
it(`an in-flight cursor read cannot resurrect presence after ${interruption}`, async () => { it(`an in-flight cursor read cannot resurrect presence after ${interruption}`, async () => {