Skill v1.0.0
currentAutomated scan100/100version: "1.0.0" description: "Web Worker patterns used throughout the Lichtblick codebase: Comlink integration, ComlinkWrap lifecycle, transfer handlers, OffscreenCanvas, SharedWorker isolation, and testing utilities."
Web Workers Skill
Standard Pattern: ComlinkWrap
All Worker communication in Lichtblick uses Comlink with the ComlinkWrap utility for safe lifecycle management.
Worker Creation (main thread)
import { ComlinkWrap } from "@lichtblick/den/worker";const worker = new Worker(new URL("./MyWorker.worker", import.meta.url), // webpack-compatible URL);const { remote, dispose } = ComlinkWrap<MyWorkerAPI>(worker);// Use the remote APIconst result = await remote.process(data);// Cleanup when donedispose(); // releases Comlink proxy + terminates worker
Worker Implementation (worker thread)
import * as Comlink from "@lichtblick/comlink";class MyWorkerImpl {async process(data: Uint8Array): Promise<Result> {// Heavy computation herereturn result;}}Comlink.expose(new MyWorkerImpl());
Key file: packages/den/worker/ComlinkWrap.ts
FinalizationRegistry Cleanup
ComlinkWrap returns a dispose function, but the project also uses FinalizationRegistry as a safety net:
const registry = new FinalizationRegistry<() => void>((dispose) => {dispose(); // Worker terminated when wrapper is garbage collected});// In constructor:registry.register(this, dispose);
This prevents Worker leaks if the wrapping object is GC'd without explicit disposal.
Transfer Handlers
AbortSignal Transfer
import { abortSignalTransferHandler } from "@lichtblick/comlink-transfer-handlers";// Register BEFORE any Comlink communicationComlink.transferHandlers.set("abortsignal", abortSignalTransferHandler);
Allows passing AbortSignal across Worker boundaries — used by WorkerIterableSource to cancel iteration.
OffscreenCanvas Transfer
const offscreenCanvas = canvas.transferControlToOffscreen();const { remote, dispose } = ComlinkWrap<RendererService>(worker);await remote.init(Comlink.transfer({ canvas: offscreenCanvas, devicePixelRatio: window.devicePixelRatio },[offscreenCanvas], // Transfer list),);
Used by: Plot panel (OffscreenCanvasRenderer), Chart component.
ArrayBuffer Transfer
// Transfer large binary data to Worker (zero-copy)await remote.processData(Comlink.transfer(buffer, [buffer.buffer]));// After transfer: buffer.byteLength === 0 (detached)
Worker URL Pattern (Webpack)
All Worker URLs use the import.meta.url pattern for webpack compatibility:
new Worker(new URL("./MyWorker.worker", import.meta.url));
- File must be named
*.worker.ts(webpack recognizes this pattern) babel-plugin-transform-import-metahandles the URL resolution- Each Worker file is bundled as a separate chunk
SharedWorker Pattern
Used by UserScriptPlayer for script execution:
new SharedWorker(new URL("./transformerWorker/index", import.meta.url), {name: uuidv4(), // Unique name prevents sharing between tabs});
SharedWorkerchosen for memory efficiency (shared code across script instances)- Unique
nameper instance prevents cross-tab Worker sharing (intentional isolation)
Testing Workers
makeComlinkWorkerMock
import { makeComlinkWorkerMock } from "@lichtblick/den/testing";// Replace global Worker constructor with a mock that uses in-process ComlinkObject.defineProperty(global, "Worker", {writable: true,value: makeComlinkWorkerMock(() => new ActualImplementation()),});
Located in packages/den/testing/makeComlinkWorkerMock.ts:
- Creates an in-process Comlink channel (no actual Worker thread)
- Allows unit testing Worker-based code without spawning real threads
- Uses
EventEmitterto simulatepostMessage/onmessage
Workers in the Codebase
| Location | Purpose | Pattern | |
|---|---|---|---|
IterablePlayer/WorkerIterableSource.ts | Data source parsing | ComlinkWrap + AbortSignal | |
Plot/OffscreenCanvasRenderer.ts | Chart.js rendering | ComlinkWrap + OffscreenCanvas | |
Plot/builders/TimestampDatasetsBuilder.ts | Dataset building | ComlinkWrap + FinalizationRegistry | |
ThreeDeeRender/renderables/Images/WorkerImageDecoder.ts | Image decoding | ComlinkWrap | |
UserScriptPlayer/index.ts | Script execution | SharedWorker + unique name | |
FoxgloveWebSocketPlayer/WorkerSocketAdapter.ts | WebSocket I/O | Raw Worker + postMessage | |
components/Chart/index.tsx | Legacy chart rendering | WebWorkerManager + Rpc |
Performance Considerations
- Transfer vs Copy: Always use
Comlink.transfer()for large ArrayBuffers - Worker startup: Workers are created lazily — first use incurs startup cost
- Proxy cleanup: Always call
dispose()or rely on FinalizationRegistry - Message overhead: Small frequent messages have higher overhead than batched large messages
- SharedWorker caveats: Debugging is harder (separate DevTools), errors may be silent