Shared worker

@atolljs/core ships a SharedWorker-based runtime alongside WorkerPool. Its purpose is shared state, not messaging: one worker owns one SharedArrayBuffer, and every page, tab, and iframe that connects binds its contract to that same buffer — writes in one are readable and observable in all.

npm install @atolljs/core

Worker entry

src/incidents.sharedWorker.ts
import { sharedWorkerHost, TaskRegistry } from '@atolljs/core';
import { incidentsMemory } from './memory';
import { getIncidents, queryIncidents } from './queries';

TaskRegistry.register(InitIncidents, async () => seed(incidentsMemory));
TaskRegistry.register(QueryIncidents, (q) => queryIncidents(incidentsMemory, q));
sharedWorkerHost();

Client

main thread / any page, tab, or iframe
import { connectSharedWorker } from '@atolljs/core';
import { incidentsMemory, InitIncidents, QueryIncidents } from './contracts';

const worker = await connectSharedWorker({
  // Inline new SharedWorker(new URL(...)) so your bundler emits the chunk.
  createWorker: () => new SharedWorker(
    new URL('./incidents.sharedWorker.ts', import.meta.url),
    { type: 'module' }
  ),
  sharedMemory: incidentsMemory,
  tasks: { initIncidents: InitIncidents, queryIncidents: QueryIncidents },
});

await worker.initIncidents();                          // first-class task method
const page = await worker.queryIncidents({ offset: 0, limit: 50 });
worker.sharedMemory.metrics.read();                    // shared across ALL clients
worker.disconnect();                                   // close this port only

Config

OptionTypeNotes
workerUrlURLWorker entry URL — one of the three connect options is required
createWorker() => SharedWorkerPreferred — bundlers only emit the chunk when new SharedWorker(new URL(...)) appears inline
portMessagePortPre-opened port — tests or custom plumbing
sharedMemorySharedMemoryContract; bound to the worker's buffer on connect
memoryMemoryConfigBuffer sizing hint — first connected client wins; later clients share that buffer
tasksTaskMapNamed contracts → first-class methods
connectTimeoutMsnumberHandshake timeout, default 10s

Client surface

MemberBehavior
runTask(contract, ...args)Compute dispatch; resolves with the validated result for this client
clientIndexConnection order reported by the host
sharedMemoryThe bound contract — observe() and framework bindings work unchanged
disconnect()Closes this port; the worker and buffer live on for other clients

State propagates through memory, not messages

Cross-context sync needs no messaging at all: every write bumps a shared version counter (Atomics.add + notify), and each client's observe()/watch() subscribers wait on that counter (Atomics.waitAsync). A write in one tab resolves every other tab's observer directly — the port only carries the connect handshake and task dispatch.

// tab A writes
memory.metrics.write((m) => ({ ...m, critical: m.critical + 1 }));

// tab B observes — fires on tab A's write, no postMessage involved
observe(memory, 'metrics').subscribe(render);

Pool or shared worker?

Use…When
WorkerPoolCPU parallelism in one page; per-page private memory
connectSharedWorkerOne shared dataset across tabs/iframes — state syncs through memory

Caveats

  • Safari dropped SharedWorker. Feature-detect and fall back to a per-page WorkerPool:typeof SharedWorker === 'undefined'.
  • Same cross-origin isolation rules — it's still SharedArrayBuffer; see Hosting & headers. Iframed clients need the full embedding chain there.
  • One worker, serialized work — a SharedWorker is a single thread. State sync costs nothing (it's just shared memory), but task execution is one-at-a-time; use WorkerPool for CPU parallelism.
  • No terminate() — the browser owns the worker lifecycle; clients only disconnect.