Islands — quickstart

Island apps run inside the worker against a proxy document — every mutation serializes to ops the shell replays. Three app shapes cover the spectrum from "plain component" to "DOM-heavy library, unmodified". Concepts and workload guidance are on Overview.

Install

npm install @atolljs/core @atolljs/islands
# plus your framework's island package if the worker hosts one, e.g.:
npm install @atolljs/react-island   # vue / svelte / solidjs / angular too

Mounting

render.worker.ts — the whole worker entry
import { definePolyWorker } from '@atolljs/islands/worker';

export const renderWorker = definePolyWorker({
  apps: {
    // Either app shape — a framework component via its island package's
    // helper, or pure proxy-DOM code with no framework at all:
    dashboard: reactIslandApp(DashboardApp),   // @atolljs/react-island/worker
    vanilla: { imperative: (doc, props) => { ... } },
  },
});
FrameworkWorker helperPackage
ReactreactIslandApp / defineReactPolyWorker@atolljs/react-island
VuevueIslandApp / defineVuePolyWorker@atolljs/vue-island
SveltesvelteIslandApp@atolljs/svelte-island
SolidJSsolidIslandApp@atolljs/solid-island
AngularangularIslandApp@atolljs/angular-island
main thread
import { mountIsland } from '@atolljs/islands';

const island = await mountIsland({
  // The inline new URL(...) literal is what lets the bundler see the entry.
  worker: () => new Worker(new URL('./render.worker.ts', import.meta.url), { type: 'module' }),
  el: document.getElementById('island')!,
  app: 'dashboard',
  props: { ... },
  onEvent: (name, payload) => { ... },          // island → shell emit() channel
  slots: { preview: (el) => mountCanvas(el) },  // transclusion holes
});
island.updateProps({ ... });
island.destroy();
OptionNotes
elContainer element the op stream replays into. Required.
worker / clientExactly one: a bundler-detectable worker factory (or URL) this mount owns, or a shared connectIslandWorker client.
appRegistry key into apps. Optional for MonoWorkers — a single-app worker resolves its sole app regardless.
propsCross via structuredClone — uncloneable values reject naming the offending key. callbackProp(fn) markers pass shell functions through.
onEventReceives every worker-side emit(name, payload).
slots{ name: (el | null) => void } — a data-atoll-slot element's contents are yours to fill with real main-thread DOM.
mode'push' (default — SharedArrayBuffer doorbell, needs COOP/COEP) or 'poll' (50ms drain — no SAB, no headers needed).
mountTimeoutDefault 15s, 0 disables — a worker entry that never answers rejects with a named error instead of hanging.
onActivity / onOpsPer-op-batch hooks — stats, and worker-vs-main timing split.

Pass client: connectIslandWorker({ worker }) instead of worker when several islands should share one worker (multi-island-per-worker — the client is released when its last island destroys). When your shell is a framework app, prefer the *-island package's components — they wrap exactly these calls (see the Islands page under each framework).

Three app kinds

ShapeWhat it is
Framework componentWrapped by the *-island package's *IslandApp helper — the framework's real renderer (reconciler/runtime) executes in the worker.
{ imperative }{ imperative: (doc, props) => void, dispose?(doc) } — no framework: mutate the proxy doc directly, ops emit as you go. updateProps is dispose + clear + rebuild.
RenderedIslandApp{ mount(ctx) => { update?, dispose? } } — the framework-adapter shape. The *-island worker packages produce these; write your own to bring a new renderer.

Stamp apps with islandApp('name', app) — a data property that survives minification (unlike fn.name), so shell-side component references resolve to the wire key. Framework components pass through their package's define*PolyWorker unwrapped.

Channels

// worker side — from your *-island package's /worker entry
// (or '@atolljs/islands/worker' directly):
import { emit, runInInstance, bumpOpsVersion } from '…/worker';

// island → shell: lands in mountIsland's onEvent.
emit('rowSelected', { id: row.id });

// shell → island: pass a function marker in props…
//   props: { onSave: callbackProp((id) => save(id)) }   // from atoll-islands
// …the worker receives a callable (fire-and-forget — no return channel):
props.onSave(id);

// Transclusion — the shell fills this leaf with real main-thread DOM.
// The mechanism is one attribute; any app/framework can render it:
doc.createElement('div').setAttribute('data-atoll-slot', 'preview');
// (react-island also exports a <Slot name="preview" /> component)

Mediation is unidirectional by convention — emit → onEvent → shell state → back in as props. Slots are the escape hatch for content the worker can't own (canvases, Monaco, Google Maps JS): the worker owns the box, the shell owns the contents.

The proxy document

Inside the worker there is no DOM — a ProxyDocument facade records every mutation as an op the shell replays. The full op vocabulary, DOM coverage, the geometry/events channels, limits, and per-renderer benchmarks live under Proxy document.

Testing in-process

import { InProcessWorker } from '@atolljs/core/testing/inProcessWorker';
vi.stubGlobal('Worker', InProcessWorker);
InProcessWorker.handlerModules = [() => import('./my.worker')];

const island = await mountIsland({ worker: () => new Worker(url, { type: 'module' }), ... });
// Real task registry, real op stream, real shared-memory binding —
// only the thread boundary is faked. flushObservers() settles microtasks.