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<undefined, undefined> };
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<SsrBudget>(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<SsrBudget | undefined>(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<string | null> => {
// 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;
};