fix
This commit is contained in:
parent
c869f0a880
commit
25f64ec690
1 changed files with 81 additions and 61 deletions
|
|
@ -88,69 +88,89 @@ 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(
|
||||||
context.mock.timers.enable({ apis: ["setInterval"] });
|
"refreshes unchanged cursors on reconnect and heartbeat, and follows the document through renames",
|
||||||
const content = new TextEncoder().encode("hello");
|
{ timeout: 5_000 },
|
||||||
let path = "note.md";
|
async (context) => {
|
||||||
const database = createDatabase(await hash(content));
|
context.mock.timers.enable({ apis: ["setInterval"] });
|
||||||
const sent: {
|
const content = new TextEncoder().encode("hello");
|
||||||
documentsWithCursors: {
|
let path = "note.md";
|
||||||
relative_path: RelativePath;
|
const database = createDatabase(await hash(content));
|
||||||
vault_update_id: VaultUpdateId | null;
|
const sent: {
|
||||||
}[];
|
documentsWithCursors: {
|
||||||
}[] = [];
|
relative_path: RelativePath;
|
||||||
const websocket = {
|
vault_update_id: VaultUpdateId | null;
|
||||||
onWebSocketStatusChanged: new EventListeners<
|
}[];
|
||||||
(connected: boolean) => unknown
|
}[] = [];
|
||||||
>(),
|
let published = Promise.withResolvers<undefined>();
|
||||||
onRemoteCursorsUpdateReceived: new EventListeners(),
|
const websocket = {
|
||||||
updateLocalCursors: (value: (typeof sent)[number]) => {
|
onWebSocketStatusChanged: new EventListeners<
|
||||||
sent.push(value);
|
(connected: boolean) => unknown
|
||||||
|
>(),
|
||||||
|
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 } as unknown as FileSystemOperations,
|
||||||
|
notifier
|
||||||
|
);
|
||||||
|
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({
|
||||||
|
"note.md": [{ start: 1, end: 2 }]
|
||||||
|
});
|
||||||
|
assert.equal(sent.length, 1);
|
||||||
|
await tracker.sendLocalCursorsToServer({
|
||||||
|
"note.md": [{ start: 1, end: 2 }]
|
||||||
|
});
|
||||||
|
assert.equal(sent.length, 1);
|
||||||
|
await waitForSend(() => {
|
||||||
|
websocket.onWebSocketStatusChanged.trigger(true);
|
||||||
|
});
|
||||||
|
assert.equal(sent.length, 2);
|
||||||
|
await waitForSend(() => {
|
||||||
|
context.mock.timers.tick(15_000);
|
||||||
|
});
|
||||||
|
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
|
||||||
|
);
|
||||||
|
websocket.onWebSocketStatusChanged.trigger(false);
|
||||||
|
const count = sent.length;
|
||||||
|
const readCount = read.mock.callCount();
|
||||||
|
context.mock.timers.tick(30_000);
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
assert.equal(sent.length, count + 1);
|
||||||
|
} finally {
|
||||||
|
tracker.reset();
|
||||||
|
context.mock.timers.reset();
|
||||||
}
|
}
|
||||||
} as unknown as WebSocketManager;
|
|
||||||
const notifier = new EventListeners<(path: RelativePath) => unknown>();
|
|
||||||
const tracker = new CursorTracker(
|
|
||||||
database,
|
|
||||||
websocket,
|
|
||||||
{ read: async () => ({ content }) } as unknown as FileSystemOperations,
|
|
||||||
notifier
|
|
||||||
);
|
|
||||||
const drain = async () => {
|
|
||||||
for (let i = 0; i < 20; i++)
|
|
||||||
await new Promise((resolve) => setImmediate(resolve));
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
await tracker.sendLocalCursorsToServer({
|
|
||||||
"note.md": [{ start: 1, end: 2 }]
|
|
||||||
});
|
|
||||||
assert.equal(sent.length, 1);
|
|
||||||
await tracker.sendLocalCursorsToServer({
|
|
||||||
"note.md": [{ start: 1, end: 2 }]
|
|
||||||
});
|
|
||||||
assert.equal(sent.length, 1);
|
|
||||||
websocket.onWebSocketStatusChanged.trigger(true);
|
|
||||||
await drain();
|
|
||||||
assert.equal(sent.length, 2);
|
|
||||||
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);
|
|
||||||
websocket.onWebSocketStatusChanged.trigger(false);
|
|
||||||
const count = sent.length;
|
|
||||||
context.mock.timers.tick(30_000);
|
|
||||||
await drain();
|
|
||||||
assert.equal(sent.length, count);
|
|
||||||
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) {
|
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 () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue