~james/core

core/web/src/lib/ssr/render.ts -rw-r--r-- 2.5 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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;
};