import { getContext, setContext } from "svelte"; import type { FileOptions } from "@pierre/diffs"; import type { MarkupContext } from "#lib/markup/paths.ts"; // past any of these limits the browser renders it instead const MAX_DOCUMENT_CHARS = 256 * 1024; const MAX_PAGE_CHARS = 1024 * 1024; // html gets its own cap because highlighted code comes out around 15x its source const MAX_PAGE_HTML_CHARS = 1024 * 1024; // counts the wait for a worker too. a render past it gets its worker replaced const RENDER_TIMEOUT_MS = 2_000; // the full source already ships in the page data export const PLAIN_PREVIEW_CHARS = 2 * 1024; export const RENDER_QUEUE_WAIT_MS = 500; export const MAX_RENDER_WORKERS = 4; export const RENDER_WORKER_HEAP_MB = 128; // in characters, which is 32 to 64 mib of heap export const RENDER_CACHE_CHARS = 32 * 1024 * 1024; export const RENDER_CACHE_MAX_HEAP_SHARE = 0.7; // a document that hangs the renderer hangs it once per ttl, not once per request export const FAILED_RENDER_TTL_MS = 10 * 60 * 1000; export type RenderTask = | { kind: "markup"; source: string; ctx: MarkupContext } | { kind: "file"; name: string; contents: string; options: FileOptions }; export interface SsrBudget { chars: number; html: number; } const BUDGET_KEY = Symbol("ssr-budget"); const freshBudget = (): SsrBudget => ({ chars: MAX_PAGE_CHARS, html: MAX_PAGE_HTML_CHARS }); export const setSsrBudget = (): void => { setContext(BUDGET_KEY, freshBudget()); }; /** a tree rendered without the root layout, like a component test, gets a fresh budget */ export const getSsrBudget = (): SsrBudget | undefined => import.meta.env.SSR ? (getContext(BUDGET_KEY) ?? freshBudget()) : undefined; export const fitsServerRender = (size: number): boolean => size <= MAX_DOCUMENT_CHARS; export const renderOnServer = async ( budget: SsrBudget, size: number, task: () => RenderTask ): Promise => { // keeps node:worker_threads out of the client bundle if (!import.meta.env.SSR || !fitsServerRender(size) || size > budget.chars) return null; budget.chars -= size; const { render } = await import("#lib/ssr/pool.ts"); // html rarely comes out shorter than its source. skip it once the budget can't fit that const html = await render(task(), RENDER_TIMEOUT_MS, budget, () => budget.html >= size); // which render gets dropped here depends on the order they finish in if (html === null || html.length > budget.html) return null; budget.html -= html.length; return html; };