Island apps
An island app is a view tree packaged for the worker: a worker entry registers it with a definer, the shell mounts it by name, and props/events cross the wire in both directions.
Define the worker entry
Two definers, same apps map — poly hosts a registry of named apps (many apps per worker), mono dedicates the worker to one:
import { defineViewPolyWorker } from '@solid-mvc/atoll-island/worker';
import { TickerIsland } from './ticker';
import { MetricsIsland } from './metrics';
// One worker entry hosts every island app this file exports a view for.
export const worker = defineViewPolyWorker({
apps: { ticker: TickerIsland, metrics: MetricsIsland },
});import { defineViewMonoWorker } from '@solid-mvc/atoll-island/worker';
import { EditorIsland } from './editor';
export const worker = defineViewMonoWorker(EditorIsland);The shell imports the entry only as a type —
import type { worker } from './x.worker'— and spawns it with the inlinenew Worker(new URL('./x.worker.tsx', import.meta.url), { type: 'module' })literal so the bundler can detect it.
Everything inside works the same
The worker compiles JSX for Solid's universal renderer against @solid-mvc/atoll-island/worker, so the framework is unchanged in the worker: @Component/View construction, inject() into the island's own root injector, signals, and lifecycle all behave exactly as on the main thread.
@Component()
export class MetricsIsland extends View<{ source?: string }> {
private points = signal<number[]>([]);
private timer?: ReturnType<typeof setInterval>;
onInit() {
// Timers/signals outside an event task still update: each tracked
// write re-syncs the DOM and ships an op batch to the shell
// (an "unsolicited" push).
this.timer = setInterval(() =>
this.points.value = [...this.points.value.slice(-59), sample()], 1000);
}
onDestroy() { clearInterval(this.timer); }
render() {
return <Sparkline points={this.points.value} label={this.props.source} />;
}
}viewIsland — stamped app references
viewIsland(name, App) stamps an app so shell code can pass the reference itself instead of a registry string — the reference carries its own name (and optional island-scoped providers via viewIslandApp(App, { providers })):
export const tickerApp = viewIsland('ticker', TickerIsland);
// shell: <Island app={tickerApp} worker={tickerWorker} />
// name comes from the stamp — no string to drift out of sync.viewIslandApp — the contract attach
For full facade decoupling, the contract module both sides import gets attached to the app at registration: mount/updateProps props are validated against its schema and every declared emit payload is checked worker-side before it goes on the wire:
import { defineIslandContract, z } from '@solid-mvc/atoll-island';
export const tickerContract = defineIslandContract({
app: 'ticker',
props: z.object({ source: z.string().optional() }),
events: { 'worker-ping': z.object({ ticks: z.number() }) },
worker: () =>
new Worker(new URL('./ticker.worker.tsx', import.meta.url), {
type: 'module',
}),
});export const worker = defineViewPolyWorker({
apps: {
ticker: viewIslandApp(TickerIsland, { contract: tickerContract }),
},
});z is Atoll's bundled schema mini-lib, re-exported so no extra dependency is needed; any safeParse-shaped schema (real zod included) satisfies Schema. The shell then mounts islandComponent(tickerContract) with fully typed props and events — see Shell & mounting.
viewIsland(name, App) stamps an app so shell code can pass the reference itself instead of a registry string — the reference carries its own name (and optional island-scoped providers via viewIslandApp(App, { providers })):
export const tickerApp = viewIsland('ticker', TickerIsland);
// shell: <Island app={tickerApp} worker={tickerWorker} />
// name comes from the stamp — no string to drift out of sync.Talking to the shell: emit()
// worker side — any event name + structured-cloneable payload:
emit('milestone', { count: this.count.value });
// shell side receives every emit through onEvent:
<Island app="ticker" worker={w} onEvent={(name, payload) => {
if (name === 'milestone') notify(payload);
}} />Receiving callbacks: callbackProp
<Island
app="editor" worker={w}
props={{ onSave: callbackProp((doc) => save(doc)) }}
/>// worker side — props.onSave is a real function; calling it posts back
// to the shell and returns a Promise of the shell fn's return value:
async save() {
const result = await this.props.onSave(this.document.value);
}Only functions wrapped in callbackProp cross — raw function props fail structured-clone on the wire.