fix(sync): keep queued catch-record writes from crossing an account change

The queue held whole records and flushed whatever was in it, so edits made
before a sign-out could be written against the account that signed in next.

Queue patches and merge them per key, so two edits to one entry combine instead
of one replacing the other; drop everything when the owning account is no longer
the current one; and stop scheduling flushes while offline, where they could only
fail and back off.
This commit is contained in:
Josh Creek
2026-09-15 17:48:34 +01:00
parent 54eaa6d776
commit 19abb34693
2 changed files with 55 additions and 8 deletions
+36
View File
@@ -28,6 +28,27 @@ describe('createCatchRecordWriteQueue()', () => {
vi.unstubAllGlobals();
});
it('merges partial notes/status patches without inventing omitted fields', async () => {
const fetchFn = vi.fn(async () => new Response('[]'));
const queue = createCatchRecordWriteQueue({ endpointUrl: '/api/catches', fetchFn });
const identity = { userId: 'u', pokedexId: 'd', pokemonId: '1' };
queue.enqueue({ ...identity, personalNotes: 'Keep' }, { flushSoon: false });
queue.enqueue({ ...identity, inHome: true }, { flushSoon: false });
expect(queue.getPendingPatch('1')).toEqual({
...identity,
personalNotes: 'Keep',
inHome: true
});
await queue.flushNow();
const payload = JSON.parse(
String((fetchFn.mock.calls[0] as unknown as [string, RequestInit])[1].body)
);
expect(payload).toEqual([{ ...identity, personalNotes: 'Keep', inHome: true }]);
queue.enqueue({ ...identity, personalNotes: '' }, { flushSoon: false });
expect(queue.getPendingPatch('1')?.personalNotes).toBe('');
await queue.flushNow();
});
it('coalesces multiple updates for the same key and flushes only the latest state', async () => {
const fetchFn = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
return new Response(init?.body as string, { status: 200 });
@@ -141,6 +162,21 @@ describe('createCatchRecordWriteQueue()', () => {
expect(queue.getPendingCount()).toBe(1);
});
it('discards queued edits after the owning account changes', async () => {
const fetchFn = vi.fn<typeof fetch>();
let current = true;
const queue = createCatchRecordWriteQueue({
endpointUrl: '/catch-records',
fetchFn,
isCurrentUser: () => current
});
queue.enqueue(mkRecord(), { debounceMs: 100, flushSoon: false });
current = false;
await queue.flushNow();
expect(fetchFn).not.toHaveBeenCalled();
expect(queue.getPendingCount()).toBe(0);
});
it('does not discard a newer version enqueued during an in-flight request', async () => {
let resolveFirst: ((response: Response) => void) | undefined;
const firstResponse = new Promise<Response>((resolve) => (resolveFirst = resolve));