// Who this browser is signed in as, in one place. // // Three screens ask the same question — the studio (its account menu and its // tier), the library (whether to open at all) and the top bar (whether the // LIBRARY link is a link). Read separately, each of them answers it once and then // keeps that answer for the life of the page — the library is mounted once and // never asked again — so a visitor who signs in at the studio left the library // behind holding "guest" // — the next click on LIBRARY handed them a panel asking for a login they had just // given. One record, one reader. import { useSyncExternalStore } from 'react'; import { api, type User } from './api'; export interface AuthState { user: User | null; // The API has answered once: the account is known, guest or not. Before that // `user` is null because nobody has said — which is not the same as "guest", and // is why the LIBRARY link is held back rather than switched off, and why opening // `/library` on a record nobody has answered for yet is not a reason to send the // visitor home. known: boolean; } let state: AuthState = { user: null, known: false }; let inFlight: Promise | null = null; const listeners = new Set<() => void>(); export function authState(): AuthState { return state; } export function subscribeAuth(fn: () => void): () => void { listeners.add(fn); return () => listeners.delete(fn); } // The snapshot is a value the store replaces only when it changes, so React can // compare it by identity — one object per answer, not one per render. export function useAuth(): AuthState { return useSyncExternalStore(subscribeAuth, authState); } // Take the answer the caller already has — a login, a logout, `/me` on the studio's // own mount — so every subscribed screen moves at once. export function setAuthUser(user: User | null): void { if (state.known && state.user === user) return; state = { user, known: true }; for (const fn of listeners) fn(); } // Ask the API again. One request at a time: the studio and the library both ask as // they mount, and one page load is one round trip. A call that fails leaves the // record as it was — the API being away is not the visitor's account ending. export function refreshAuth(): Promise { if (!inFlight) { inFlight = api .me() .then((me) => { setAuthUser(me?.user ?? null); return me?.user ?? null; }) .catch(() => null) .finally(() => { inFlight = null; }); } return inFlight; }