Shell & mounting

The shell side is @preact-mvc/atoll-island (the package root): mount an island as an element, pass props and callbacks, receive events. Three mounting surfaces cover the common cases.

islandComponent(contract) — the facade

The recommended mount. The contract module names the app, declares the wire schemas, and carries the worker factory — so the call site passes no worker plumbing, and props/events typecheck against the contract:

shell
import { islandComponent } from '@preact-mvc/atoll-island';
import { tickerContract } from './ticker.contract';

const Ticker = islandComponent(tickerContract);

// onEvent narrows to the contract's declared events; island props are
// the contract's props schema. No worker prop — the contract carries it.
<Ticker
  source="orders"
  onEvent={(name, payload) => {
    if (name === 'worker-ping') notify(payload);
  }}
/>

The string-keyed form still exists — islandComponent('ticker') — for registries whose workers the shell already wires up.

<Island> — the primitive

shell
import { Island } from '@preact-mvc/atoll-island';
import type { WorkerLike } from '@preact-mvc/atoll-island';

const tickerWorker = (): WorkerLike =>
  new Worker(new URL('./ticker.worker.tsx', import.meta.url), {
    type: 'module',
  }) as WorkerLike;

<Island
  app="ticker"                    // registry key, stamped ref, or contract
  worker={tickerWorker}           // omit when app is a contract with worker
  props={{ source: 'orders' }}    // structured-cloneable
  onEvent={(name, payload) => log(name, payload)}
  mode="poll"                     // 'push' needs COOP/COEP isolation headers
  onReady={(handle) => handle.updateProps({ source: 'live' })}
  onError={(err) => show(err)}
  className="metric-card"
/>

worker accepts a WorkerLike directly or a factory — use the factory form in real code so Vite detects the entry. When app is a contract that carries a worker, the contract module is the whole connection. Unmounting the element terminates an owned worker.

Shared workers: one client, many islands

A poly worker can host several islands at once — connect once, hand the client to each mount:

shell
import { connectIslandWorker, Island } from '@preact-mvc/atoll-island';

const client = connectIslandWorker({
  worker: () =>
    new Worker(new URL('./dash.worker.tsx', import.meta.url), {
      type: 'module',
    }) as WorkerLike,
});

// Each mount is an isolated instance inside the shared worker:
<Island client={client} app="ticker" props={{ source: 'orders' }} />
<Island client={client} app="ticker" props={{ source: 'payments' }} />
<Island client={client} app="metrics" />

Instances are keyed app@N in the worker — separate view trees, separate state, separate props, one worker process.

lazyIsland — code-split facades

Lazy-loads a module that exports a contract (or a stamped app) — ideal for keeping a contract's worker factory and schema out of the initial bundle's critical path:

shell
const HeavyChart = lazyIsland(() => import('./heavy-chart.contract'));

// The contract module loads on first mount; fallback renders meanwhile.
<HeavyChart dataset="q4" fallback={<Skeleton />} />

If you reference a stamped app instead of a contract, import it with import type in shell code — a worker definition must never leak into the main-thread bundle (defining one installs instance globals).

Imperative: mountIsland

For non-JSX integration (a portal, a vanilla container, tests), mountIsland attaches to any element and returns a handle:

shell
const handle = await mountIsland({
  el,
  worker: tickerWorker,
  app: 'ticker',
  props: { source: 'orders' },
  onEvent: (name, p) => console.log(name, p),
});

handle.updateProps({ source: 'payments' }); // re-render with new props
handle.destroy();                            // teardown + worker cleanup

Events back to the shell

Everything the worker emit()s arrives at onEvent(name, payload); shell functions cross in the other direction via callbackProp — details in Island apps. Instance scoping and the op wire are in How it works.