"use client"; import { useEffect, useSyncExternalStore } from "react"; import { fetchCloudChartLibrary } from "@/lib/home-cloud-sync"; import { readProfile, upsertSelfChart } from "@/lib/home-profile"; import type { Account, ChartLibraryRecord, Profile } from "@/lib/home-types"; const SELF_SUBJECT_ID = "self"; export const LAST_SUBJECT_ACCOUNT_KEY = "jyotisha_current_subject_account"; export function currentSubjectStorageKey(id: string): string { return `jyotisha_current_subject:${id}`; } function storedValue(key: string): string { try { return typeof localStorage === "undefined" ? "" : localStorage.getItem(key) || ""; } catch { return ""; } } function storeValue(key: string, value: string): void { try { if (typeof localStorage !== "undefined") localStorage.setItem(key, value); } catch { /* The in-memory selection still works when storage is unavailable. */ } } let accountId = storedValue(LAST_SUBJECT_ACCOUNT_KEY); let subjectId = (accountId && storedValue(currentSubjectStorageKey(accountId))) || SELF_SUBJECT_ID; let catalogReady = false; let catalog: ChartLibraryRecord[] | null = null; const listeners = new Set<() => void>(); const catalogListeners = new Set<() => void>(); export function readCurrentSubjectId(): string { return subjectId; } export function readCurrentSubjectAccountId(): string { return accountId; } export function peekSubjectCatalog(): readonly ChartLibraryRecord[] | null { return catalogReady ? catalog : null; } export function readCurrentSubjectScope(): string { return JSON.stringify([accountId, subjectId]); } export function useCurrentSubjectScope(): string { return useSyncExternalStore((listener) => { const offSubject = subscribeCurrentSubject(listener); const offCatalog = subscribeSubjectCatalog(listener); return () => { offSubject(); offCatalog(); }; }, readCurrentSubjectScope, () => ""); } export function currentSubjectCatalogReady(): boolean { return catalogReady; } export function subscribeCurrentSubject(listener: () => void): () => void { listeners.add(listener); return () => { listeners.delete(listener); }; } export function subscribeSubjectCatalog(listener: () => void): () => void { catalogListeners.add(listener); return () => { catalogListeners.delete(listener); }; } function emit(): void { for (const listener of listeners) listener(); } function emitCatalog(): void { for (const listener of catalogListeners) listener(); } export function setCurrentSubject(next: string): void { const id = next.trim() || SELF_SUBJECT_ID; if (id === subjectId) return; subjectId = id; if (accountId) storeValue(currentSubjectStorageKey(accountId), id); emit(); } /** A null list preserves the stored choice, but cannot open the request gate. */ export function bindCurrentSubjectAccount(nextAccountId: string, knownOtherIds: readonly string[] | null): void { const previousReady = catalogReady; const previousAccount = accountId; accountId = nextAccountId; const stored = nextAccountId ? storedValue(currentSubjectStorageKey(nextAccountId)) || SELF_SUBJECT_ID : SELF_SUBJECT_ID; const valid = stored === SELF_SUBJECT_ID || knownOtherIds === null || knownOtherIds.includes(stored); const next = valid ? stored : SELF_SUBJECT_ID; if (nextAccountId) { storeValue(LAST_SUBJECT_ACCOUNT_KEY, nextAccountId); if (!valid) storeValue(currentSubjectStorageKey(nextAccountId), SELF_SUBJECT_ID); try { localStorage.removeItem(`jyotisha_active_chart:${nextAccountId}`); } catch { /* Optional storage. */ } } catalogReady = Boolean(nextAccountId) && knownOtherIds !== null; if (next !== subjectId) { subjectId = next; emit(); } if (previousReady !== catalogReady || previousAccount !== accountId) emitCatalog(); } export function useCurrentSubject(): string { return useSyncExternalStore(subscribeCurrentSubject, readCurrentSubjectId, () => SELF_SUBJECT_ID); } export function useCurrentSubjectReady(): boolean { const ready = useSyncExternalStore(subscribeSubjectCatalog, currentSubjectCatalogReady, () => false); useEffect(() => { if (currentSubjectCatalogReady()) return; void loadSubjectCatalog().catch(() => undefined); }, []); return ready; } export function useCurrentSubjectProfile(self: Profile): Profile | null { const id = useCurrentSubject(); const rows = useSyncExternalStore(subscribeSubjectCatalog, () => catalog, () => null); return id === SELF_SUBJECT_ID ? self : rows?.find((row) => row.id === id)?.profile ?? null; } let catalogRequest: Promise | null = null; let catalogAccountId = ""; let catalogGeneration = 0; let othersUnavailable = false; let accountFlight: Promise<{ status: number; account: Account | null }> | null = null; let recoveryTimer: ReturnType | null = null; export function subjectCatalogOthersUnavailable(): boolean { return othersUnavailable; } export function invalidateAccountFlight(): void { accountFlight = null; } /** One in-flight account read shared by the session list and the person catalog. */ export function readBootAccount(): Promise<{ status: number; account: Account | null }> { if (accountFlight) return accountFlight; const published = (async () => { const response = await fetch("/api/account", { cache: "no-store" }); if (response.status === 401) return { status: 401, account: null }; const payload = await response.json().catch(() => null); if (!response.ok) throw new Error("account_unavailable"); return { status: response.status, account: payload as Account }; })().catch((error: unknown) => { if (accountFlight === published) accountFlight = null; throw error; }); accountFlight = published; return published; } function clearCatalogRecovery(): void { if (recoveryTimer === null) return; clearTimeout(recoveryTimer); recoveryTimer = null; } export function resetSubjectCatalogForTests(): void { catalogGeneration += 1; catalogRequest = null; catalogAccountId = ""; accountId = ""; subjectId = SELF_SUBJECT_ID; catalogReady = false; catalog = null; othersUnavailable = false; accountFlight = null; clearCatalogRecovery(); } export function invalidateSubjectCatalog(): void { catalogGeneration += 1; catalogRequest = null; catalogAccountId = ""; catalogReady = false; catalog = null; othersUnavailable = false; clearCatalogRecovery(); emitCatalog(); } function rememberCatalog(account: Account, library: ChartLibraryRecord[], degraded: boolean): ChartLibraryRecord[] { catalogAccountId = account.user.id; catalog = library; othersUnavailable = degraded; bindCurrentSubjectAccount(catalogAccountId, degraded ? [] : library.filter((row) => row.role === "other").map((row) => row.id)); emitCatalog(); return library; } function scheduleCatalogRecovery(account: Account): void { if (recoveryTimer !== null || !othersUnavailable) return; const generation = catalogGeneration; const timer = setTimeout(() => { recoveryTimer = null; if (generation !== catalogGeneration || !othersUnavailable) return; void fetchCloudChartLibrary().then((others) => { if (generation !== catalogGeneration) return; const self = readProfile(account.profile); const library = upsertSelfChart(others.filter((row) => row.role === "other"), self); clearCatalogRecovery(); catalogRequest = Promise.resolve(rememberCatalog(account, library, false)); }).catch(() => { if (generation === catalogGeneration && othersUnavailable) scheduleCatalogRecovery(account); }); }, 3000); if (typeof timer === "object" && timer && "unref" in timer) timer.unref(); recoveryTimer = timer; } type SettledRead = { ok: true; value: T } | { ok: false; error: unknown }; /** Starts a read now without an unhandled rejection; the caller rethrows on unwrap. */ function settledRead(promise: Promise): Promise> { return promise.then((value) => ({ ok: true as const, value }), (error: unknown) => ({ ok: false as const, error })); } function unwrapRead(read: SettledRead): T { if (!read.ok) throw read.error; return read.value; } export function loadSubjectCatalog(force = false): Promise { if (!force && catalogRequest) return catalogRequest; const generation = ++catalogGeneration; clearCatalogRecovery(); // The people list does not depend on the account payload, so it is read in the // same round as the account instead of after it (BUG-1127). const libraryRead = settledRead(fetchCloudChartLibrary()); catalogRequest = readBootAccount().then(async (result) => { if (generation !== catalogGeneration) return loadSubjectCatalog(); if (result.status === 401 || !result.account) throw new Error("account_unavailable"); const account = result.account; try { const others = unwrapRead(await libraryRead); if (generation !== catalogGeneration) return loadSubjectCatalog(); const self = readProfile(account.profile); const library = upsertSelfChart(others.filter((row) => row.role === "other"), self); return rememberCatalog(account, library, false); } catch (error) { if (generation !== catalogGeneration) throw error; const self = readProfile(account.profile); const library = upsertSelfChart([], self); const remembered = rememberCatalog(account, library, true); scheduleCatalogRecovery(account); return remembered; } }).catch((error: unknown) => { if (generation === catalogGeneration) catalogRequest = null; throw error; }); return catalogRequest; }