mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
fix(client): let a flush called mid-pass resolve with that pass
flush() returned at once when this tab already had a flush going, and that includes one still waiting for another tab's Web Lock. The socket's reconnect hook chains a refetch onto flush(), so a reconnect landing in that window rehydrated the trip while a replay was still on its way, the race the lock wait was meant to close. sendSoon and the online, periodic and visibility triggers make such an overlap common. flush() now keeps the running flush as a promise. A second call still asks it for one more pass and then gets that same promise back, so it resolves only once every pass, including the one it asked for, is done. The pass itself moves to _flushPass, and the repeat runs as a loop instead of a nested flush() call. The client CLAUDE.md sync bullet now says so, and loses its dash.
This commit is contained in:
@@ -810,6 +810,57 @@ describe('mutationQueue.flush: one tab at a time', () => {
|
||||
expect(await offlineDb.mutationQueue.get(id)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('a second flush while the first waits for the lock resolves only with it', async () => {
|
||||
const locks = fakeLocks();
|
||||
const releaseOther = locks.holdElsewhere();
|
||||
const id = generateUUID();
|
||||
await mutationQueue.enqueue(makeMutation({ id }));
|
||||
let sent = 0;
|
||||
server.use(http.post('/api/trips/1/places', () => { sent++; return HttpResponse.json({ place: buildPlace({ trip_id: 1, id: 64 }) }); }));
|
||||
|
||||
const first = mutationQueue.flush();
|
||||
await settle();
|
||||
// The socket's reconnect hook calls in here and refetches once this resolves.
|
||||
let secondDone = false;
|
||||
const second = mutationQueue.flush().then(() => { secondDone = true; });
|
||||
await settle();
|
||||
expect(secondDone).toBe(false);
|
||||
|
||||
releaseOther();
|
||||
await Promise.all([first, second]);
|
||||
|
||||
expect(secondDone).toBe(true);
|
||||
expect(sent).toBe(1);
|
||||
// One lock request for the waiting pass and one for the extra pass it was asked for.
|
||||
expect(locks.request).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('a write queued during a pass goes out before the second caller resolves', async () => {
|
||||
const first = generateUUID();
|
||||
const later = generateUUID();
|
||||
await mutationQueue.enqueue(makeMutation({ id: first }));
|
||||
const sent: string[] = [];
|
||||
let second: Promise<string[]> | undefined;
|
||||
server.use(http.post('/api/trips/1/places', async ({ request }) => {
|
||||
const { name } = await request.json() as { name: string };
|
||||
sent.push(name);
|
||||
if (name === 'Eiffel Tower') {
|
||||
await mutationQueue.enqueue(makeMutation({ id: later, body: { name: 'Louvre' } }));
|
||||
// What had been sent at the moment this second call resolved.
|
||||
second = mutationQueue.flush().then(() => [...sent]);
|
||||
}
|
||||
return HttpResponse.json({ place: buildPlace({ trip_id: 1, id: sent.length + 70 }) });
|
||||
}));
|
||||
|
||||
const firstRun = mutationQueue.flush();
|
||||
await vi.waitFor(() => expect(second).toBeDefined());
|
||||
|
||||
// Resolved only after the extra pass sent the later write, not when it was asked for.
|
||||
expect(await second).toEqual(['Eiffel Tower', 'Louvre']);
|
||||
expect(await offlineDb.mutationQueue.count()).toBe(0);
|
||||
await firstRun;
|
||||
});
|
||||
|
||||
it('flushes under the flag alone when the lock request is refused', async () => {
|
||||
Object.defineProperty(navigator, 'locks', {
|
||||
value: { request: vi.fn().mockRejectedValue(new DOMException('denied', 'SecurityError')) },
|
||||
|
||||
Reference in New Issue
Block a user