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 tooMounting
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) => { ... } },
},
});| Framework | Worker helper | Package |
|---|---|---|
| React | reactIslandApp / defineReactPolyWorker | @atolljs/react-island |
| Vue | vueIslandApp / defineVuePolyWorker | @atolljs/vue-island |
| Svelte | svelteIslandApp | @atolljs/svelte-island |
| SolidJS | solidIslandApp | @atolljs/solid-island |
| Angular | angularIslandApp | @atolljs/angular-island |
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();| Option | Notes |
|---|---|
el | Container element the op stream replays into. Required. |
worker / client | Exactly one: a bundler-detectable worker factory (or URL) this mount owns, or a shared connectIslandWorker client. |
app | Registry key into apps. Optional for MonoWorkers — a single-app worker resolves its sole app regardless. |
props | Cross via structuredClone — uncloneable values reject naming the offending key. callbackProp(fn) markers pass shell functions through. |
onEvent | Receives 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). |
mountTimeout | Default 15s, 0 disables — a worker entry that never answers rejects with a named error instead of hanging. |
onActivity / onOps | Per-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
| Shape | What it is |
|---|---|
| Framework component | Wrapped 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.