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, []);
|
||||
});
|
||||
|
||||
it("refreshes unchanged cursors on reconnect and heartbeat, and follows the document through renames", async (context) => {
|
||||
context.mock.timers.enable({ apis: ["setInterval"] });
|
||||
const content = new TextEncoder().encode("hello");
|
||||
let path = "note.md";
|
||||
const database = createDatabase(await hash(content));
|
||||
const sent: {
|
||||
documentsWithCursors: {
|
||||
relative_path: RelativePath;
|
||||
vault_update_id: VaultUpdateId | null;
|
||||
}[];
|
||||
}[] = [];
|
||||
const websocket = {
|
||||
onWebSocketStatusChanged: new EventListeners<
|
||||
(connected: boolean) => unknown
|
||||
>(),
|
||||
onRemoteCursorsUpdateReceived: new EventListeners(),
|
||||
updateLocalCursors: (value: (typeof sent)[number]) => {
|
||||
sent.push(value);
|
||||
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";
|
||||
const database = createDatabase(await hash(content));
|
||||
const sent: {
|
||||
documentsWithCursors: {
|
||||
relative_path: RelativePath;
|
||||
vault_update_id: VaultUpdateId | null;
|
||||
}[];
|
||||
}[] = [];
|
||||
let published = Promise.withResolvers<undefined>();
|
||||
const websocket = {
|
||||
onWebSocketStatusChanged: new EventListeners<
|
||||
(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) {
|
||||
it(`an in-flight cursor read cannot resurrect presence after ${interruption}`, async () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue