From e098b9184bd1b70fd25f3f08d7e146c5ec18fe39 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 18:22:34 +0000 Subject: [PATCH] Add PersistenceQueue mid-flight flush test --- .../middleware/persistenceMiddleware.test.ts | 103 ++++++++++++++++++ src/store/middleware/persistenceMiddleware.ts | 5 +- 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/store/middleware/persistenceMiddleware.test.ts diff --git a/src/store/middleware/persistenceMiddleware.test.ts b/src/store/middleware/persistenceMiddleware.test.ts new file mode 100644 index 0000000..3e91cd6 --- /dev/null +++ b/src/store/middleware/persistenceMiddleware.test.ts @@ -0,0 +1,103 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { PersistenceQueue } from './persistenceMiddleware' + +const { putMock, deleteMock, syncMock } = vi.hoisted(() => ({ + putMock: vi.fn<[string, unknown], Promise>(), + deleteMock: vi.fn<[string, string], Promise>(), + syncMock: vi.fn<[string, string, unknown, string], Promise>() +})) + +vi.mock('@/lib/db', () => ({ + db: { + put: putMock, + delete: deleteMock + } +})) + +vi.mock('./flaskSync', () => ({ + syncToFlask: syncMock +})) + +const nextTick = () => new Promise(resolve => setTimeout(resolve, 0)) + +const waitFor = async (assertion: () => void, attempts = 5) => { + let lastError: unknown + + for (let i = 0; i < attempts; i += 1) { + await nextTick() + + try { + assertion() + return + } catch (error) { + lastError = error + } + } + + throw lastError +} + +const createControlledPromise = () => { + let resolve: () => void + + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise + }) + + return { + promise, + resolve: resolve! + } +} + +describe('PersistenceQueue', () => { + beforeEach(() => { + putMock.mockReset() + deleteMock.mockReset() + syncMock.mockReset() + syncMock.mockResolvedValue(undefined) + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('flushes new operations enqueued while processing after the first batch finishes', async () => { + const queue = new PersistenceQueue() + const controlled = createControlledPromise() + + putMock + .mockReturnValueOnce(controlled.promise) + .mockResolvedValueOnce(undefined) + + queue.enqueue({ + type: 'put', + storeName: 'files', + key: 'file-1', + value: { id: 'file-1' }, + timestamp: Date.now(), + }, 0) + + await waitFor(() => { + expect(putMock).toHaveBeenCalledTimes(1) + }) + + queue.enqueue({ + type: 'put', + storeName: 'files', + key: 'file-2', + value: { id: 'file-2' }, + timestamp: Date.now(), + }, 0) + + await nextTick() + expect(putMock).toHaveBeenCalledTimes(1) + + controlled.resolve() + + await waitFor(() => { + expect(putMock).toHaveBeenCalledTimes(2) + }) + }) +}) diff --git a/src/store/middleware/persistenceMiddleware.ts b/src/store/middleware/persistenceMiddleware.ts index 7d20c2e..0039383 100644 --- a/src/store/middleware/persistenceMiddleware.ts +++ b/src/store/middleware/persistenceMiddleware.ts @@ -38,7 +38,7 @@ type PendingOperation = { timestamp: number } -class PersistenceQueue { +export class PersistenceQueue { private queue: Map = new Map() private processing = false private debounceTimers: Map> = new Map() @@ -97,6 +97,9 @@ class PersistenceQueue { } } finally { this.processing = false + if (this.queue.size > 0) { + await this.processQueue() + } } }