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
136 lines
5.4 KiB
TypeScript
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();
|
|
}
|