Guide · @staticflux/worker · @staticflux/document · @staticflux/wasm · @staticflux/surface

Workers, documents, Wasm & surfaces

Heavy work runs off the main thread by default — supervised, replicated, traced end to end, and with a fallback for every browser gap.

Supervised workers and typed protocols

apps/studio/src/adapters/document-worker.ts (abridged)
export const DocumentWorkerProtocol = defineProtocol({
  id: 'studio/document-worker',
  version: 1,
  requests: { open: request<OpenArgs, { readonly entities: number }>({ idempotent: true }) },
  events: { archived: event<{ readonly docId: string; readonly archive: DocumentArchive }>() },
});

const worker = createSupervisedWorker({
  id: 'studio/document-worker',
  protocol: DocumentWorkerProtocol,
  spawn: dedicatedWorkerSpawner(() => new Worker(new URL('./document.worker.ts', import.meta.url), { type: 'module' })),
  contexts,                                 // session, flags, decisions, platform → every worker
  replicas: [replica, presence],
  rehydrate: () => lastArchive,             // state restored after a respawn
  heartbeat: { intervalMs: 5_000, timeoutMs: 4_000, maxMissed: 3 },
  restart: { intensity: 3, periodMs: 60_000, baseDelayMs: 50 },
  fallback: { description: 'Read-only view', activate: () => readOnly.set(true), deactivate: () => readOnly.set(false) },
});
const opened = await worker.request('open', { docId: '42', blocks }, { signal });
  • Worker lifecycle: spawning → handshaking → ready ⇄ busy, heartbeat-based unresponsive, crashed → respawning with backoff, failed (fallback activated, app degraded) when the intensity is exceeded. Throttled background tabs recover instead of respawning.
  • One owner per store; replicas never mutate (I-9): replica.intent(action, args) sends writes to the owner, patches come back batched per frame; a gap or epoch mismatch resnapshots.
  • Every envelope carries traceparent — worker telemetry joins the main timeline. Raw tokens are not replicated; a token broker executes authorized calls on the owning side.
  • Multi-tab: createTabCoordinator (SharedWorker or leader tab via Web Locks + BroadcastChannel, with numbered terms) from @staticflux/worker/coordinator.

The document engine

@staticflux/document runs inside the document worker. v1 (used by Studio): an ECS world (entities, structure-of-arrays component stores, queries, systems) materialized from a DocumentCRDT (Yjs behind an interface), a DocumentEngine that is itself a replicable store, presence, a collaboration transport contract with an in-memory mock, and snapshot / restore for archives and respawn. v2 (@staticflux/document/v2): one CRDT document per page behind the engine-neutral DocumentCRDTv2 port, a DocumentRepo with one commit per gesture, versioned components, a Loro engine, a binary render projection and a spatial index. The main thread holds only a projection — no CRDT library in the main bundle.

Document v2, the stage and the editor ↔ render pipeline

Wasm as worker-scoped units

TypeScript
export const ImageCodec = defineWasmModule<ImageCodecExports>({
  id: 'media/image-codec',
  source: { chunk: 'wasm:image-codec' },              // a logical chunk: gated, warmed, budgeted
  features: { required: ['simd'] },
  variants: [{ id: 'threads', source: { chunk: 'wasm:image-codec-mt' }, requires: ['threads'] }],
  budget: { bytes: 400_000, compileMs: 80 },
});
export const imageCodecUnit = wasmModuleUnit({ id: 'media/image-codec-unit', provides: ImageCodecToken, module: ImageCodec });

Feature facts (SIMD, threads, memory64, exceptions, GC) come from WebAssembly.validate probes plus cross-origin isolation; threaded builds are picked only when shared memory is actually usable, otherwise the next variant. Instantiation is the unit’s init (compileStreaming, with an ArrayBuffer fallback). Main-thread Wasm needs an explicit allowMainThread: { reason, budgetMs }.

Rich surfaces

@staticflux/surface gives DOM, Canvas2D, WebGL, WebGPU and HTML-in-Canvas one Surface contract. Engines (RenderEngine: init, frame, resize, input, dispose) never touch the DOM, so the same engine runs in a render worker via a transferred OffscreenCanvas (createWorkerSurface / hostRenderSurfaces) or on the main thread. Input is forwarded as compact timestamped events and becomes intents where the state lives; pure-canvas content gets a parallel accessible DOM layer; per-surface frame metrics carry budgets. Tiled GPU raster (/raster), canvas text input (/text-input) and image scaling (/image) are separate entries, covered in the editors guide.

AGENTS.md on GitHub: @staticflux/worker · @staticflux/document · @staticflux/wasm · @staticflux/surface · @staticflux/platform