| name | web-workers |
| 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),
);
const { remote, dispose } = ComlinkWrap<MyWorkerAPI>(worker);
const result = await remote.process(data);
dispose();
Worker Implementation (worker thread)
import * as Comlink from "@lichtblick/comlink";
class MyWorkerImpl {
async process(data: Uint8Array): Promise<Result> {
return 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();
});
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";
Comlink.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],
),
);
Used by: Plot panel (OffscreenCanvasRenderer), Chart component.
ArrayBuffer Transfer
await remote.processData(Comlink.transfer(buffer, [buffer.buffer]));
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-meta handles 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(),
});
SharedWorker chosen for memory efficiency (shared code across script instances)
- Unique
name per instance prevents cross-tab Worker sharing (intentional isolation)
Testing Workers
makeComlinkWorkerMock
import { makeComlinkWorkerMock } from "@lichtblick/den/testing";
Object.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
EventEmitter to simulate postMessage / 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