Web Worker patterns used throughout the Lichtblick codebase: Comlink integration, ComlinkWrap lifecycle, transfer handlers, OffscreenCanvas, SharedWorker isolation, and testing utilities.
78%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./.github/skills/web-workers/SKILL.mdAll Worker communication in Lichtblick uses Comlink with the ComlinkWrap utility for safe lifecycle management.
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 API
const result = await remote.process(data);
// Cleanup when done
dispose(); // releases Comlink proxy + terminates workerimport * as Comlink from "@lichtblick/comlink";
class MyWorkerImpl {
async process(data: Uint8Array): Promise<Result> {
// Heavy computation here
return result;
}
}
Comlink.expose(new MyWorkerImpl());packages/den/worker/ComlinkWrap.tsComlinkWrap 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.
import { abortSignalTransferHandler } from "@lichtblick/comlink-transfer-handlers";
// Register BEFORE any Comlink communication
Comlink.transferHandlers.set("abortsignal", abortSignalTransferHandler);Allows passing AbortSignal across Worker boundaries — used by WorkerIterableSource to cancel iteration.
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.
// Transfer large binary data to Worker (zero-copy)
await remote.processData(Comlink.transfer(buffer, [buffer.buffer]));
// After transfer: buffer.byteLength === 0 (detached)All Worker URLs use the import.meta.url pattern for webpack compatibility:
new Worker(new URL("./MyWorker.worker", import.meta.url));*.worker.ts (webpack recognizes this pattern)babel-plugin-transform-import-meta handles the URL resolutionUsed by UserScriptPlayer for script execution:
new SharedWorker(new URL("./transformerWorker/index", import.meta.url), {
name: uuidv4(), // Unique name prevents sharing between tabs
});SharedWorker chosen for memory efficiency (shared code across script instances)name per instance prevents cross-tab Worker sharing (intentional isolation)import { makeComlinkWorkerMock } from "@lichtblick/den/testing";
// Replace global Worker constructor with a mock that uses in-process Comlink
Object.defineProperty(global, "Worker", {
writable: true,
value: makeComlinkWorkerMock(() => new ActualImplementation()),
});Located in packages/den/testing/makeComlinkWorkerMock.ts:
EventEmitter to simulate postMessage / onmessage| 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 |
Comlink.transfer() for large ArrayBuffersdispose() or rely on FinalizationRegistrylichtblick-suite/lichtblick
9589f38
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.