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, []);
});
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"] });
const content = new TextEncoder().encode("hello");
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;
}[];
}[] = [];
let published = Promise.withResolvers<undefined>();
const websocket = {
onWebSocketStatusChanged: new EventListeners<
(connected: boolean) => unknown
@ -106,18 +110,23 @@ it("refreshes unchanged cursors on reconnect and heartbeat, and follows the docu
onRemoteCursorsUpdateReceived: new EventListeners(),
updateLocalCursors: (value: (typeof sent)[number]) => {
sent.push(value);
published.resolve(undefined);
}
} as unknown as WebSocketManager;
const notifier = new EventListeners<(path: RelativePath) => unknown>();
const read = context.mock.fn(async () => ({ content }));
const tracker = new CursorTracker(
database,
websocket,
{ read: async () => ({ content }) } as unknown as FileSystemOperations,
{ read } as unknown as FileSystemOperations,
notifier
);
const drain = async () => {
for (let i = 0; i < 20; i++)
await new Promise((resolve) => setImmediate(resolve));
const waitForSend = async (trigger: () => void): Promise<void> => {
published = Promise.withResolvers<undefined>();
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 {
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 }]
});
assert.equal(sent.length, 1);
await waitForSend(() => {
websocket.onWebSocketStatusChanged.trigger(true);
await drain();
});
assert.equal(sent.length, 2);
await waitForSend(() => {
context.mock.timers.tick(15_000);
await drain();
});
assert.equal(sent.length, 3);
path = "renamed.md";
database.state.local.document = 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);
const count = sent.length;
const readCount = read.mock.callCount();
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);
await waitForSend(() => {
websocket.onWebSocketStatusChanged.trigger(true);
await drain();
});
assert.equal(sent.length, count + 1);
} finally {
tracker.reset();
context.mock.timers.reset();
}
});
}
);
for (const interruption of ["disconnect", "reset"] as const) {
it(`an in-flight cursor read cannot resurrect presence after ${interruption}`, async () => {