fix(auth): send the queued changes before a logout deletes the offline database

Logging out deleted this user's offline database straight away, so a
change made offline and not yet sent was lost without a word. With a
connection, the logout now flushes the queue first, for at most five
seconds.
This commit is contained in:
Maurice
2026-10-07 16:32:57 +02:00
parent 38656eeffd
commit 461a63fc86
2 changed files with 44 additions and 1 deletions
+14 -1
View File
@@ -7,7 +7,8 @@ import { getApiErrorMessage } from '../types'
import { tripSyncManager } from '../sync/tripSyncManager'
import { reopenForUser, deleteCurrentUserDb } from '../db/offlineDb'
import { setAuthed } from '../sync/authGate'
import { setForcedOffline } from '../sync/networkMode'
import { isEffectivelyOffline, setForcedOffline } from '../sync/networkMode'
import { mutationQueue } from '../sync/mutationQueue'
import { registerSyncTriggers, unregisterSyncTriggers } from '../sync/syncTriggers'
import { useSystemNoticeStore } from './systemNoticeStore.js'
import { clearAppearanceSnapshot } from '../theme/applyAppearance'
@@ -18,6 +19,9 @@ import { forgetResumeRoute } from '../utils/resumeRoute'
import { markSignedOut, clearSignedOut } from '../utils/signedOut'
import { forgetPushDeviceOnLogout, resyncPushSubscription } from '../push/webPush'
/** How long a logout waits for the queued changes to go out. */
const LOGOUT_FLUSH_MS = 5000
interface AuthResponse {
user: User
token: string
@@ -238,6 +242,15 @@ export const useAuthStore = create<AuthState>()(
},
logout: async () => {
// 0. Send what is still queued while the session can. Step 6 deletes this
// user's offline database, and an edit made offline and not yet sent would
// go with it. Bounded, so a slow server never holds the logout.
if (!isEffectivelyOffline()) {
await Promise.race([
mutationQueue.flush().catch(() => {}),
new Promise(resolve => setTimeout(resolve, LOGOUT_FLUSH_MS)),
])
}
// 1. Gate first so any in-flight flush/syncAll bails before we wipe the DB.
setAuthed(false)
// Flagged in the same update that drops the session: clearing isAuthenticated
@@ -147,6 +147,36 @@ describe('authStore', () => {
expect(state.user).toBeNull();
expect(state.isAuthenticated).toBe(false);
});
it('sends the queued changes before the offline database is deleted', async () => {
const { mutationQueue } = await import('../../../src/sync/mutationQueue');
useAuthStore.setState({ user: buildUser(), isAuthenticated: true });
setAuthed(true);
let authedDuringFlush: boolean | null = null;
const flush = vi.spyOn(mutationQueue, 'flush').mockImplementation(async () => { authedDuringFlush = isAuthed(); });
await useAuthStore.getState().logout();
expect(flush).toHaveBeenCalledOnce();
expect(authedDuringFlush).toBe(true);
expect(isAuthed()).toBe(false);
});
it('does not wait on a flush that hangs', async () => {
vi.useFakeTimers();
try {
const { mutationQueue } = await import('../../../src/sync/mutationQueue');
vi.spyOn(mutationQueue, 'flush').mockImplementation(() => new Promise(() => {}));
useAuthStore.setState({ user: buildUser(), isAuthenticated: true });
const done = useAuthStore.getState().logout();
await vi.advanceTimersByTimeAsync(5001);
await vi.runAllTimersAsync();
await done;
expect(useAuthStore.getState().user).toBeNull();
} finally {
vi.useRealTimers();
}
});
});
describe('FE-AUTH-007: Register success', () => {