diff --git a/.changeset/fix-bundler-worker-dataclone.md b/.changeset/fix-bundler-worker-dataclone.md new file mode 100644 index 000000000..0ca6213df --- /dev/null +++ b/.changeset/fix-bundler-worker-dataclone.md @@ -0,0 +1,6 @@ +--- +'@doc-kit/core': patch +'@doc-kit/generator-react': patch +--- + +Exclude main-thread-only bundler options from worker thread configuration to prevent DataCloneError. diff --git a/packages/core/src/generators/types.d.ts b/packages/core/src/generators/types.d.ts index 7bac2a412..baf2edbcf 100644 --- a/packages/core/src/generators/types.d.ts +++ b/packages/core/src/generators/types.d.ts @@ -70,6 +70,9 @@ declare global { hasParallelProcessor?: boolean; + /** Configuration keys excluded from worker-thread messages. */ + workerConfigOmit?: string[]; + /** * Receive the dependency's output as an async iterable of its chunks, as * they are produced, instead of one collected array. With one thread, the diff --git a/packages/core/src/threading/__tests__/parallel.test.mjs b/packages/core/src/threading/__tests__/parallel.test.mjs index 0f6cf1200..fee3194d0 100644 --- a/packages/core/src/threading/__tests__/parallel.test.mjs +++ b/packages/core/src/threading/__tests__/parallel.test.mjs @@ -302,6 +302,47 @@ describe('createParallelWorker', () => { deepStrictEqual(chunks, [[1, 2], [3, 4], [5]]); }); + it('does not clone main-thread-only HTML bundler functions', async () => { + const htmlGenerator = await loadGenerator('html'); + const transferred = []; + const pool = { + run(task) { + const copy = structuredClone(task); + transferred.push(copy); + + return Promise.resolve(copy.input); + }, + }; + const bundler = { + buildServer: async () => 'file:///library.mjs', + buildClient: async () => ({}), + compile: async code => code, + }; + const configuration = { + threads: 2, + chunkSize: 1, + html: { title: 'Worker regression test', bundler }, + }; + const worker = createParallelWorker( + 'html', + htmlGenerator, + pool, + configuration + ); + const results = await collectStream(worker.stream([1, 2, 3], {})); + + deepStrictEqual(results, [1, 2, 3]); + strictEqual(transferred.length, 3); + + for (const task of transferred) { + deepStrictEqual(task.configuration.html, { + title: 'Worker regression test', + }); + } + + strictEqual(configuration.html.bundler, bundler); + }); + describe('with streamed batches', () => { it( 'starts a batch before the next one arrives', diff --git a/packages/core/src/threading/parallel.mjs b/packages/core/src/threading/parallel.mjs index 494264369..b6b22c1c1 100644 --- a/packages/core/src/threading/parallel.mjs +++ b/packages/core/src/threading/parallel.mjs @@ -1,7 +1,7 @@ 'use strict'; import logger from '#logger/index.mjs'; -import { isAsyncIterable } from '#utils/misc.mjs'; +import { isAsyncIterable, omitKeys } from '#utils/misc.mjs'; import { CHUNKS_PER_THREAD } from './constants.mjs'; @@ -76,7 +76,15 @@ export default function createParallelWorker( ) { const { threads, chunkSize } = configuration; - const { name } = generator; + const { name, workerConfigOmit } = generator; + + const workerConfiguration = + workerConfigOmit?.length && configuration[name] + ? { + ...configuration, + [name]: omitKeys(configuration[name], workerConfigOmit), + } + : configuration; return { /** @@ -165,7 +173,14 @@ export default function createParallelWorker( running++; const task = pool.run( - createTask(batch, indices, extra, configuration, specifier, name) + createTask( + batch, + indices, + extra, + workerConfiguration, + specifier, + name + ) ); tasks.push( diff --git a/packages/react/src/html/index.mjs b/packages/react/src/html/index.mjs index 607592a39..c3e48c583 100644 --- a/packages/react/src/html/index.mjs +++ b/packages/react/src/html/index.mjs @@ -96,5 +96,8 @@ export default { // page count, so it is farmed out to the worker pool. hasParallelProcessor: true, + // The bundler runs on the main thread and contains non-cloneable functions. + workerConfigOmit: ['bundler'], + processChunk, };