diff --git a/src/web-ui/src/flow_chat/timeline/highlight.worker.test.ts b/src/web-ui/src/flow_chat/timeline/highlight.worker.test.ts new file mode 100644 index 0000000000..5f14c8f9f1 --- /dev/null +++ b/src/web-ui/src/flow_chat/timeline/highlight.worker.test.ts @@ -0,0 +1,63 @@ +// @vitest-environment node +import { fileURLToPath } from 'node:url'; +import { runInNewContext } from 'node:vm'; +import { build } from 'vite'; +import { expect, it } from 'vitest'; + +const root = fileURLToPath(new URL('../../../', import.meta.url)); +const entry = fileURLToPath(new URL('./highlight.worker.ts', import.meta.url)); + +type WorkerSandbox = { + self: WorkerSandbox; + Prism?: { disableWorkerMessageHandler?: boolean }; + onmessage?: (event: { data: unknown }) => void; + postMessage: (message: unknown) => void; + addEventListener: (type: string, listener: (event: { data: unknown }) => void) => void; +}; + +it('uses the app worker protocol without Prism installing its JSON worker handler', async () => { + const result = await build({ + root, + configFile: false, + publicDir: false, + logLevel: 'silent', + build: { + write: false, + minify: true, + rollupOptions: { input: entry, preserveEntrySignatures: 'strict' }, + }, + }); + if ('on' in result) throw new Error('Unexpected watch build'); + + const output = (Array.isArray(result) ? result : [result]).flatMap(bundle => bundle.output); + const worker = output.find(asset => asset.type === 'chunk' && /highlight\.worker-.*\.js$/.test(asset.fileName)); + if (!worker || worker.type !== 'chunk') throw new Error('Highlight worker asset was not emitted'); + + const replies: unknown[] = []; + const listeners: Array<(event: { data: unknown }) => void> = []; + const sandbox = { + postMessage: (message: unknown) => replies.push(message), + addEventListener: (type: string, listener: (event: { data: unknown }) => void) => { + if (type === 'message') listeners.push(listener); + }, + } as unknown as WorkerSandbox; + runInNewContext( + 'class WorkerGlobalScope {}; globalThis.WorkerGlobalScope = WorkerGlobalScope; globalThis.self = globalThis; Object.setPrototypeOf(globalThis, WorkerGlobalScope.prototype);', + sandbox, + ); + + runInNewContext(worker.code, sandbox, { timeout: 2000 }); + + expect(sandbox.Prism?.disableWorkerMessageHandler).toBe(true); + expect(listeners).toHaveLength(0); + expect(sandbox.onmessage).toBeTypeOf('function'); + + const message = { id: 7, content: 'const answer: number = 42;', language: 'typescript' }; + expect(() => { + for (const listener of listeners) listener({ data: message }); + sandbox.onmessage!({ data: message }); + }).not.toThrow(); + + expect(replies).toHaveLength(1); + expect(replies[0]).toMatchObject({ id: 7 }); +}, 20_000); diff --git a/src/web-ui/src/flow_chat/timeline/highlight.worker.ts b/src/web-ui/src/flow_chat/timeline/highlight.worker.ts index fb141718e2..2a6cd1bd59 100644 --- a/src/web-ui/src/flow_chat/timeline/highlight.worker.ts +++ b/src/web-ui/src/flow_chat/timeline/highlight.worker.ts @@ -1,3 +1,4 @@ +import './prismWorkerSetup'; import Prism from 'prismjs'; import 'prismjs/components/prism-typescript'; import 'prismjs/components/prism-jsx'; diff --git a/src/web-ui/src/flow_chat/timeline/prismWorkerSetup.ts b/src/web-ui/src/flow_chat/timeline/prismWorkerSetup.ts new file mode 100644 index 0000000000..135e384f95 --- /dev/null +++ b/src/web-ui/src/flow_chat/timeline/prismWorkerSetup.ts @@ -0,0 +1,12 @@ +/** + * Prism installs its own JSON-based worker protocol when it detects a worker. + * This worker uses the app's structured message protocol instead. + */ +const workerGlobal = globalThis as typeof globalThis & { + Prism?: { disableWorkerMessageHandler?: boolean }; +}; + +workerGlobal.Prism = { + ...(workerGlobal.Prism ?? {}), + disableWorkerMessageHandler: true, +};