Skip to content
Source
Sign inSign up

DiffWorkerPoolProvider.tsx

157 lines5.1 KB

1import { WorkerPoolContext, useWorkerPool } from "@pierre/diffs/react";
2import { WorkerPoolManager } from "@pierre/diffs/worker";
3import DiffsWorker from "@pierre/diffs/worker/worker.js?worker";
4import * as Schema from "effect/Schema";
5import { useEffect, useState, useSyncExternalStore, type ReactNode } from "react";
6import { getRenderedAppearance, subscribeRenderedAppearance } from "../hooks/useTheme";
7import { resolveDiffThemeName } from "../lib/diffRendering";
8import { PREFERRED_HIGHLIGHTER } from "../lib/syntaxHighlighting";
9​
10export class DiffWorkerError extends Schema.TaggedError<DiffWorkerError>()("DiffWorkerError", {
11 operation: Schema.Literals(["create-worker", "get-render-options", "set-render-options"]),
12 themeName: Schema.Literals(["pierre-dark", "t3-light"]),
13 cause: Schema.Defect(),
14}) {
15 override get message(): string {
16 return `Diff worker operation ${this.operation} failed for theme ${this.themeName}.`;
17 }
19​
20const DIFF_WORKER_IDLE_TTL_MS = 30_000;
21function resolveDiffWorkerPoolSize() {
22 const cores =
23 typeof navigator === "undefined" ? 4 : Math.max(1, navigator.hardwareConcurrency || 4);
24 return Math.max(2, Math.min(6, Math.floor(cores / 2)));
26let sharedWorkerPool:
27 | {
28 readonly pool: WorkerPoolManager;
29 readonly unsubscribeTheme: () => void;
30 consumers: number;
31 idleTimer: ReturnType<typeof setTimeout> | undefined;
32 }
33 | undefined;
34​
35function applyDiffWorkerTheme(pool: WorkerPoolManager) {
36 const themeName = resolveDiffThemeName(getRenderedAppearance());
37 let operation: DiffWorkerError["operation"] = "get-render-options";
38 void (async () => {
39 try {
40 const current = pool.getDiffRenderOptions();
41 if (current.theme === themeName) return;
42 operation = "set-render-options";
43 await pool.setRenderOptions({ ...current, theme: themeName });
44 } catch (cause) {
45 console.error(new DiffWorkerError({ operation, themeName, cause }));
46 }
47 })();
49​
50// The pool follows the rendered appearance for as long as it lives, so it never depends on a
51// mounted provider to carry a theme change across.
52function createDiffWorkerPool(poolSize: number) {
53 const themeName = resolveDiffThemeName(getRenderedAppearance());
54 const pool = new WorkerPoolManager(
55 {
56 workerFactory: () => {
57 try {
58 return new DiffsWorker();
59 } catch (cause) {
60 throw new DiffWorkerError({ operation: "create-worker", themeName, cause });
61 }
62 },
63 poolSize,
64 totalASTLRUCacheSize: 240,
65 },
66 {
67 theme: themeName,
68 preferredHighlighter: PREFERRED_HIGHLIGHTER,
69 tokenizeMaxLineLength: 1_000,
70 useTokenTransformer: true,
71 },
72 );
73 return {
74 pool,
75 unsubscribeTheme: subscribeRenderedAppearance(() => applyDiffWorkerTheme(pool)),
76 consumers: 0,
77 idleTimer: undefined,
78 };
80​
81function acquireDiffWorkerPool(poolSize: number) {
82 const entry = (sharedWorkerPool ??= createDiffWorkerPool(poolSize));
83 clearTimeout(entry.idleTimer);
84 entry.idleTimer = undefined;
85 entry.consumers += 1;
86 return entry;
88​
89// Plain-text views do not queue a highlight task that could retry a blank first render.
90function DiffWorkerReady({ children }: { children?: ReactNode }) {
91 const workerPool = useWorkerPool();
92 const [readyPool, setReadyPool] = useState<WorkerPoolManager>();
93 const ready = workerPool
94 ? readyPool === workerPool || workerPool.isInitialized() || !workerPool.isWorkingPool()
95 : typeof window === "undefined";
96​
97 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- initializes the diff worker pool and marks it ready when its async startup settles
98 useEffect(() => {
99 if (ready || !workerPool) return;
100​
101 let mounted = true;
102 const finish = () => {
103 if (mounted) setReadyPool(workerPool);
104 };
105 void workerPool.initialize().then(finish, finish);
106 return () => {
107 mounted = false;
108 };
109 }, [ready, workerPool]);
110​
111 return ready ? (
112 children
113 ) : (
114 <div
115 role="status"
116 className="flex min-h-0 flex-1 items-center justify-center p-4 text-xs text-muted-foreground"
117 >
118 Loading code...
119 </div>
120 );
122​
123// Module-level so the subscribe identity is stable: a new one would release and reacquire
124// the shared pool on every render.
125function subscribeSharedWorkerPool(onStoreChange: () => void) {
126 if (typeof window === "undefined") return () => {};
127 const entry = acquireDiffWorkerPool(resolveDiffWorkerPoolSize());
128 onStoreChange();
129 return () => {
130 entry.consumers -= 1;
131 if (entry.consumers !== 0) return;
132 entry.idleTimer = setTimeout(() => {
133 entry.idleTimer = undefined;
134 entry.unsubscribeTheme();
135 entry.pool.terminate();
136 if (sharedWorkerPool === entry) sharedWorkerPool = undefined;
137 }, DIFF_WORKER_IDLE_TTL_MS);
138 };
140​
141export function useDiffWorkerPool() {
142 return useSyncExternalStore(
143 subscribeSharedWorkerPool,
144 () => sharedWorkerPool?.pool,
145 () => undefined,
146 );
148​
149export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) {
150 const workerPool = useDiffWorkerPool();
151​
152 return (
153 <WorkerPoolContext value={workerPool}>
154 <DiffWorkerReady>{children}</DiffWorkerReady>
155 </WorkerPoolContext>
156 );