refactor(web): break auth ↔ player import cycle via session-end signal (#375)

auth/store.svelte.ts no longer imports from $lib/player. Logout
broadcasts via auth/sessionEnd.svelte.ts (a tiny tick + outgoing
userId pair); player subscribes through $effect.root and owns the
player-specific teardown (clearPersistedQueue + playQueue([]) +
closeQueueDrawer()).

Cycle fully inverted — auth is now a pure leaf for player; future
session-end consumers (download cache, recent-search history) plug
in by adding their own effect on the same signal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 15:29:30 -04:00
parent 94727f40bb
commit 65057473c6
3 changed files with 37 additions and 8 deletions
+17
View File
@@ -0,0 +1,17 @@
// Logout broadcast for cross-domain teardown. Auth bumps the tick
// (with the outgoing user id) and downstream modules react via $effect
// on `sessionEnd.tick`. Keeps auth from importing player — the cycle
// stays inverted (player imports auth, never the reverse).
let _tick = $state(0);
let _userId = $state<string | null>(null);
export const sessionEnd = {
get tick() { return _tick; },
get userId() { return _userId; },
};
export function signalSessionEnd(userId: string | null): void {
_userId = userId;
_tick++;
}
+2 -7
View File
@@ -1,7 +1,6 @@
import { api, type User, type LoginResponse } from '$lib/api/client';
import { queryClient } from '$lib/query/client';
import { clearPersistedQueue } from '$lib/player/persisted';
import { playQueue, closeQueueDrawer } from '$lib/player/store.svelte';
import { signalSessionEnd } from './sessionEnd.svelte';
import { user, setUser } from './user.svelte';
// Re-export so existing `import { user } from '$lib/auth/store.svelte'`
@@ -55,11 +54,7 @@ export async function logout(opts: { silent?: boolean } = {}): Promise<void> {
// best effort — server-side session may already be gone
}
}
signalSessionEnd(userId ?? null);
setUser(null);
queryClient.clear();
// M7 #364: clear queue persistence + reset in-memory queue + close drawer
// so a subsequent login doesn't inherit the previous user's playback state.
if (userId) clearPersistedQueue(userId);
playQueue([]);
closeQueueDrawer();
}
+18 -1
View File
@@ -2,7 +2,8 @@ import type { TrackRef, RadioResponse } from '$lib/api/types';
import { untrack } from 'svelte';
import { api } from '$lib/api/client';
import { user } from '$lib/auth/user.svelte';
import { readPersistedQueue, writePersistedQueue } from './persisted';
import { sessionEnd } from '$lib/auth/sessionEnd.svelte';
import { readPersistedQueue, writePersistedQueue, clearPersistedQueue } from './persisted';
export type PlayerState = 'idle' | 'loading' | 'playing' | 'paused' | 'error';
export type RepeatMode = 'off' | 'all' | 'one';
@@ -385,6 +386,22 @@ $effect.root(() => {
});
});
// Session-end teardown (#375): auth signals via sessionEnd; player
// owns the player-specific cleanup (clear persisted queue for the
// outgoing user, reset in-memory queue, close drawer) without auth
// having to import any player APIs.
$effect.root(() => {
let lastTick = sessionEnd.tick;
$effect(() => {
if (sessionEnd.tick === lastTick) return;
lastTick = sessionEnd.tick;
const prevId = sessionEnd.userId;
if (prevId) clearPersistedQueue(prevId);
playQueue([]);
closeQueueDrawer();
});
});
// Persistence (M7 #364): split-effect shape. Queue/index changes write
// immediately (rare, user-driven); position changes throttle to once
// per 5s during steady playback. See spec section "Persistence write