Files
Jyotisha/frontend/src/lib/boot-early-reads.ts
T
Jesse_ChenandClaude Opus 5.5 c2265dabb2 fix(home): early boot reads load as an async data: script so stylesheets no longer hold them back (BUG-1127)
An inline classic script after the stylesheets waited for them to download;
on staging the reads left at 2.4-3.1 s. Measurement script gains
--css-delay and --ua iphone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-10-01 01:36:35 +08:00

136 lines
5.4 KiB
TypeScript

/**
* Cold-boot reads sent by the HTML itself (TASK-home-first-load-20260930 T8,
* BUG-1127).
*
* The home loading screen used to wait for every first-screen chunk to download
* and run before asking for the account. The reads do not depend on the bundle,
* so an inline classic script in `<head>` sends them as soon as the HTML
* arrives, on `/` only, and parks the promises on one global. The boot code
* takes each one once (`takeBootRead`); after that, and on every other page,
* the normal requests run unchanged.
*
* Both sides are built from the constants below, so the early request and the
* normal one cannot drift apart (same path, query, `cache: "no-store"` and
* default same-origin credentials). Nothing here is written to any storage:
* the script only reads the person choice `current-subject.ts` already keeps.
*
* No React, no "use client": the root layout (a server component) imports the
* script string.
*/
import { SESSION_PAGE_SIZE } from "./session-cursor.ts";
export const BOOT_READS_GLOBAL = "__jyotishaBootReads";
export const BOOT_ACCOUNT_PATH = "/api/account";
export const BOOT_CHART_PROFILES_PATH = "/api/chart-profiles";
export const BOOT_MODELS_PATH = "/api/models";
export const BOOT_READ_INIT: RequestInit = { cache: "no-store" };
/** Where `current-subject.ts` keeps the last signed-in account and its chosen person. */
export const LAST_SUBJECT_ACCOUNT_KEY = "jyotisha_current_subject_account";
export const CURRENT_SUBJECT_KEY_PREFIX = "jyotisha_current_subject:";
/** An early read older than this is not trusted; the boot reads fresh instead. */
export const BOOT_READ_MAX_AGE_MS = 30_000;
export function bootSessionsPath(subjectId: string): string {
return `/api/sessions?limit=${SESSION_PAGE_SIZE}&subject=${encodeURIComponent(subjectId)}`;
}
type BootReads = {
startedAt: number;
account?: Promise<Response>;
chartProfiles?: Promise<Response>;
models?: Promise<Response>;
sessions?: Promise<Response>;
sessionsSubject?: string;
};
type BootReadKey = "account" | "chartProfiles" | "models";
function js(value: string): string {
return JSON.stringify(value).replace(/</g, "\\u003c");
}
/** Classic ES5, no modules: it runs before and independently of the bundle. */
export function bootEarlyReadsScript(): string {
const init = js(BOOT_READ_INIT.cache as string);
return `(function () {
try {
if (window.location.pathname !== "/" || typeof window.fetch !== "function" || typeof window.Promise !== "function") return;
var subject = "self";
try {
var account = window.localStorage.getItem(${js(LAST_SUBJECT_ACCOUNT_KEY)}) || "";
if (account) subject = window.localStorage.getItem(${js(CURRENT_SUBJECT_KEY_PREFIX)} + account) || "self";
} catch (storageError) { subject = "self"; }
var read = function (path) {
var request = window.fetch(path, { cache: ${init} });
request["catch"](function () {});
return request;
};
window[${js(BOOT_READS_GLOBAL)}] = {
startedAt: Date.now(),
account: read(${js(BOOT_ACCOUNT_PATH)}),
chartProfiles: read(${js(BOOT_CHART_PROFILES_PATH)}),
models: read(${js(BOOT_MODELS_PATH)}),
sessions: read(${js(`/api/sessions?limit=${SESSION_PAGE_SIZE}&subject=`)} + encodeURIComponent(subject)),
sessionsSubject: subject
};
} catch (error) {}
})();`;
}
/**
* The script as an async data: URL (BUG-1127 fix, 2026-10-01). An inline
* classic script placed after the page's stylesheets waits until they have
* downloaded (script-blocking stylesheets), which on staging held the early
* reads back 2.4-3.1 s. An `async` external classic script is not blocked by
* stylesheets, and a data: URL costs no round trip. Plain ASCII base64, so
* the attribute needs no escaping.
*/
export function bootEarlyReadsScriptSrc(): string {
return `data:text/javascript;base64,${Buffer.from(bootEarlyReadsScript(), "utf8").toString("base64")}`;
}
function bootReads(): BootReads | null {
if (typeof window === "undefined") return null;
const reads = (window as unknown as Record<string, BootReads | undefined>)[BOOT_READS_GLOBAL];
if (!reads || typeof reads !== "object") return null;
if (typeof reads.startedAt !== "number" || Date.now() - reads.startedAt > BOOT_READ_MAX_AGE_MS) {
clearBootReads();
return null;
}
return reads;
}
export function clearBootReads(): void {
if (typeof window === "undefined") return;
delete (window as unknown as Record<string, unknown>)[BOOT_READS_GLOBAL];
}
/** The early read for `key`, once; later calls get null. */
export function takeBootRead(key: BootReadKey): Promise<Response> | null {
const reads = bootReads();
const read = reads?.[key];
if (!reads || !read) return null;
delete reads[key];
return read;
}
/**
* The early sessions read, once, and only if it asked for `subjectId`. A read
* for another person is dropped, never returned (task red line 3).
*/
export function takeBootSessionsRead(subjectId: string): Promise<Response> | null {
const reads = bootReads();
if (!reads?.sessions) return null;
const read = reads.sessions;
const asked = reads.sessionsSubject;
delete reads.sessions;
delete reads.sessionsSubject;
return asked === subjectId ? read : null;
}
/** An early read that failed on the network falls back to the normal request. */
export function withBootFallback(early: Promise<Response> | null, fresh: () => Promise<Response>): Promise<Response> {
return early ? early.catch(() => fresh()) : fresh();
}