From aa5864c2eb958fbcdb0e25efc463e8c04f14feb5 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 3 Sep 2026 01:54:49 -0700 Subject: [PATCH] fix(diffs): own the Pierre worker pool singleton; document StrictMode blocker Vite dev served the worker as a raw file, so switch to the `?worker` import. Also bypass Pierre's WorkerPoolContextProvider: it terminates the pool singleton from an unmount cleanup but only recreates it in useState, so StrictMode's remount leaves consumers holding a terminated pool. Neither fixes rendering. @pierre/diffs 1.3.6 does not survive StrictMode's mount/unmount/mount at all - the shadow DOM is never committed on the second mount and every diff renders blank. Documented in PierreDiffProviders. --- .../pierre-diff/PierreDiffProviders.tsx | 7 ++- .../pierre-diff/pierre-diff-worker-pool.tsx | 55 +++++++++++-------- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/renderer/src/components/editor/pierre-diff/PierreDiffProviders.tsx b/src/renderer/src/components/editor/pierre-diff/PierreDiffProviders.tsx index 1cae8616d5f..3855fa7e75b 100644 --- a/src/renderer/src/components/editor/pierre-diff/PierreDiffProviders.tsx +++ b/src/renderer/src/components/editor/pierre-diff/PierreDiffProviders.tsx @@ -3,8 +3,11 @@ import { PierreDiffEditProvider } from './pierre-diff-editor-provider' /** * Wraps a diff surface with the Shiki worker pool and the editor factory. - * Pierre reference-counts the pool, so every diff view can mount this and they - * still share one set of workers. + * + * KNOWN BLOCKER: @pierre/diffs 1.3.6 does not survive React StrictMode's + * mount/unmount/mount. Its shadow DOM is never committed on the second mount, + * so every diff renders blank in dev. Verified by toggling StrictMode alone, + * with and without the worker pool. Production builds do not run StrictMode. */ export function PierreDiffProviders({ children diff --git a/src/renderer/src/components/editor/pierre-diff/pierre-diff-worker-pool.tsx b/src/renderer/src/components/editor/pierre-diff/pierre-diff-worker-pool.tsx index e40a2d701fb..063abbb94f1 100644 --- a/src/renderer/src/components/editor/pierre-diff/pierre-diff-worker-pool.tsx +++ b/src/renderer/src/components/editor/pierre-diff/pierre-diff-worker-pool.tsx @@ -1,6 +1,10 @@ -import { useMemo } from 'react' -import { WorkerPoolContextProvider } from '@pierre/diffs/react' -import type { WorkerInitializationRenderOptions, WorkerPoolOptions } from '@pierre/diffs/react' +import { useState } from 'react' +// Why: `?worker` makes Vite own the worker in dev too. A bare `new URL(...)` +// specifier is served as a raw file, so the worker's own imports of shiki and +// hast-util-to-html never get rewritten. +import PierreDiffHighlightWorker from '@pierre/diffs/worker/worker.js?worker' +import { WorkerPoolContext } from '@pierre/diffs/react' +import { getOrCreateWorkerPoolSingleton, type WorkerPoolManager } from '@pierre/diffs/worker' import { PIERRE_DIFF_THEMES } from './pierre-diff-theme' // Why: Shiki grammars are heavy per worker; cap the pool well under Pierre's @@ -10,34 +14,39 @@ function resolvePoolSize(): number { return Math.min(4, Math.max(1, cores - 2)) } -function createDiffHighlightWorker(): Worker { - // Why: electron.vite.config.ts pins `worker.format: 'es'`, which this URL form requires. - return new Worker(new URL('@pierre/diffs/worker/worker.js', import.meta.url), { type: 'module' }) +function createDiffHighlightPool(): WorkerPoolManager { + return getOrCreateWorkerPoolSingleton({ + poolOptions: { + workerFactory: () => new PierreDiffHighlightWorker(), + poolSize: resolvePoolSize() + }, + // Why: the pool owns `theme` for every component instance; per-file options are ignored. + highlighterOptions: { theme: PIERRE_DIFF_THEMES } + }) } /** - * Shares one Shiki worker pool across every mounted diff surface. Pierre - * reference-counts providers, so wrapping each lazy diff view keeps highlighting - * off the main thread without paying worker startup on app launch. + * Shares one Shiki worker pool across every mounted diff surface. + * + * Note: Pierre's own WorkerPoolContextProvider is bypassed. It terminates the + * pool singleton from an unmount cleanup but only recreates it in `useState`, + * so StrictMode's remount leaves every consumer holding a terminated pool. + * We own the singleton and never tear it down. + * + * Why not Pierre's `WorkerPoolContextProvider`: it terminates the singleton from + * an unmount cleanup, but recreates it only in `useState`. StrictMode's + * mount/unmount/mount then leaves every consumer holding a terminated pool, so + * nothing ever renders in dev. We own the singleton and never tear it down — + * the pool is process-wide and cheap to keep warm for an app that reopens diffs + * constantly. */ export function PierreDiffWorkerPoolProvider({ children }: { children: React.ReactNode }): React.JSX.Element { - const poolOptions = useMemo( - () => ({ workerFactory: createDiffHighlightWorker, poolSize: resolvePoolSize() }), - [] - ) - // Why: the pool owns `theme` for every component instance; per-file options are ignored. - const highlighterOptions = useMemo( - () => ({ theme: PIERRE_DIFF_THEMES }), - [] - ) + // Why: lazy initializer keeps worker startup off app launch until a diff opens. + const [pool] = useState(createDiffHighlightPool) - return ( - - {children} - - ) + return {children} }