Coming back to / from /chart, /ephemeris, /reports or /people by client navigation remounted Home from hydrated=false and replayed the whole bootstrap: model catalog, consult status, entry summary and daily card, 3-5 round trips plus up to 4 s of reveal budget, every time. The reveal now counts per document load, not per mount: - lib/home-warm-snapshot.ts: memory-only, per-account snapshot of the model catalog, rectification entry summary, session cursor and today's card (keyed by person + birth fingerprint + day). Account, profile and the session list already survive in SessionListProvider. Cleared on account change, sign-out, provider 401 and redirectToLogin. - lib/home-warm-start.ts: Home's useState initializers take one warm decision per mount. Complete snapshot + settled list + complete profile + a landing computable in memory -> start ready, landing resolved with the cold-path functions (?new=1, ?c=, login-return stash and its person scope from BUG-1038). Anything missing or needing a lookup -> the unchanged cold path (no half-reveal, BUG-1021). - runHomeWarmRefresh commits the landing, then refreshes catalog, account and background-consultation recovery in the background (recovery shares resolveReservedConsultation with the cold path). Summary and daily card refresh through their existing effects. - Next renders the new page before it writes the address bar, so AppLink / navigateAppPath note the target href (lib/client-navigation-target.ts); unknown target -> cold path. Home() useState 33 / useRef 37 unchanged; page.tsx 1329 -> 1373 lines. Tests: home-warm-return-lifecycle (13, real Home + sidebar + provider, navigating in Next's render-then-write-URL order; 11 fail with warm start disabled) and home-warm-snapshot (12). Full suite 3953 / 61 failing, failure names identical to the 3928 / 61 baseline. Build: / stays Static, rootMainFiles gzip 130933 B unchanged. Local Chrome with every /api/* held 1.5 s: /chart -> 新建对话 interactive in 18-23 ms with no loading ring (baseline 5530 ms with ring). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
100 lines
3.7 KiB
TypeScript
100 lines
3.7 KiB
TypeScript
/**
|
|
* Home warm snapshot (BUG-1040).
|
|
*
|
|
* The loading ring plays once per document load, not once per Home mount.
|
|
* After the first cold bootstrap succeeds, Home keeps here what it would
|
|
* otherwise re-fetch on every client navigation back to `/`: the model
|
|
* catalog, the rectification entry summary, the session-page cursor and
|
|
* today's starlanguage card. Account, profile and the session list are not
|
|
* copied: they already live in the layout-level SessionListProvider, which
|
|
* survives client navigation.
|
|
*
|
|
* Memory only, one account at a time. Nothing here is written to
|
|
* localStorage / sessionStorage / IndexedDB; a document load starts empty.
|
|
* Cleared on account change, sign-out and any 401 login redirect.
|
|
*
|
|
* Pure module state, no React. Callers: `home-warm-start.ts` (read),
|
|
* Home's sync effect (write), `DailyStarlanguageBinder` (daily card),
|
|
* `redirectToLogin` / `signOut` / the provider's signed-out branch (clear).
|
|
*/
|
|
|
|
import type { DailyStarlanguageCard } from "./home-types.ts";
|
|
import type { PublicLanguageModelCatalog } from "./public-models.ts";
|
|
import type { RectificationEntrySummary } from "./rectification-entry.ts";
|
|
|
|
export type HomeWarmSnapshot = Readonly<{
|
|
accountId: string;
|
|
modelCatalog: PublicLanguageModelCatalog;
|
|
entrySummary: RectificationEntrySummary | null;
|
|
entrySummarySettled: boolean;
|
|
sessionsCursor: string | null;
|
|
/**
|
|
* Identity of the provider's boot object when the cursor was taken. A
|
|
* provider reload (person switch while away) replaces it, and its own
|
|
* cursor then wins over ours.
|
|
*/
|
|
listBoot: object | null;
|
|
}>;
|
|
|
|
/** Today's card, keyed by person + birth-data fingerprint + calendar day. */
|
|
export type HomeWarmDaily = Readonly<{
|
|
accountId: string;
|
|
subjectId: string;
|
|
fingerprint: string;
|
|
day: string;
|
|
card: DailyStarlanguageCard;
|
|
}>;
|
|
|
|
let snapshot: HomeWarmSnapshot | null = null;
|
|
let daily: HomeWarmDaily | null = null;
|
|
|
|
export function clearHomeWarmSnapshot(): void {
|
|
snapshot = null;
|
|
daily = null;
|
|
}
|
|
|
|
/** Written after a successful cold bootstrap and kept current while Home is mounted. */
|
|
export function writeHomeWarmSnapshot(next: HomeWarmSnapshot): void {
|
|
if (!next.accountId) return;
|
|
if (snapshot && snapshot.accountId !== next.accountId) clearHomeWarmSnapshot();
|
|
if (daily && daily.accountId !== next.accountId) daily = null;
|
|
snapshot = next;
|
|
}
|
|
|
|
/**
|
|
* The snapshot for this account, or null. Another account's snapshot is
|
|
* dropped on sight: an account change must never paint the previous
|
|
* account's catalog, summary or card.
|
|
*/
|
|
export function readHomeWarmSnapshot(accountId: string | null | undefined): HomeWarmSnapshot | null {
|
|
if (!accountId) return null;
|
|
if (snapshot && snapshot.accountId !== accountId) {
|
|
clearHomeWarmSnapshot();
|
|
return null;
|
|
}
|
|
return snapshot;
|
|
}
|
|
|
|
/** Complete means a warm return can paint without waiting for anything. */
|
|
export function isCompleteHomeWarmSnapshot(value: HomeWarmSnapshot | null): value is HomeWarmSnapshot {
|
|
return Boolean(value && value.accountId && value.modelCatalog && value.modelCatalog.models.length > 0 && value.entrySummarySettled);
|
|
}
|
|
|
|
export function rememberHomeWarmDaily(next: HomeWarmDaily): void {
|
|
if (!next.accountId) return;
|
|
if (snapshot && snapshot.accountId !== next.accountId) clearHomeWarmSnapshot();
|
|
daily = next;
|
|
}
|
|
|
|
/** The remembered card only when person, fingerprint and day all still match. */
|
|
export function readHomeWarmDaily(key: Readonly<{
|
|
accountId: string;
|
|
subjectId: string;
|
|
fingerprint: string;
|
|
day: string;
|
|
}>): DailyStarlanguageCard | null {
|
|
if (!daily || daily.accountId !== key.accountId) return null;
|
|
if (daily.subjectId !== key.subjectId || daily.fingerprint !== key.fingerprint || daily.day !== key.day) return null;
|
|
return daily.card;
|
|
}
|