@atolljs/islands 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Island contracts — the framework-free seam between a micro-frontend's
3
+ * worker and any shell.
4
+ *
5
+ * The contract module is the one file BOTH sides import: it names the
6
+ * registry app, declares the props/events wire shape as `Schema`s (the
7
+ * vendored `z` from `@atolljs/core`, consumer-supplied zod, or any
8
+ * `{ parse }` validator), and can carry the worker factory so
9
+ * `lazyIsland(() => import('./x.contract.js'))` is a self-contained split
10
+ * point. No framework types cross it — a React shell mounting an Angular
11
+ * app needs only this file.
12
+ *
13
+ * ```ts
14
+ * // checkout.contract.ts — published by the MFE, imported by both threads
15
+ * import { z } from '@atolljs/core';
16
+ * import { defineIslandContract } from '@atolljs/islands';
17
+ *
18
+ * export const checkout = defineIslandContract({
19
+ * app: 'checkout',
20
+ * props: z.object({ label: z.string().optional() }),
21
+ * events: { paid: z.object({ total: z.number() }) },
22
+ * worker: () => new Worker(new URL('./checkout.worker.ts', import.meta.url), { type: 'module' }),
23
+ * });
24
+ * ```
25
+ *
26
+ * Enforcement is worker-side: mount/updateProps parse props AFTER
27
+ * `unmarshalCallbackProps` restores `callbackProp` callables (declare those
28
+ * members `z.callback<Fn>()`), and `emit` parses payloads for declared
29
+ * event names. Undeclared event names pass through — a contract describes
30
+ * the wire, it doesn't fence it (a newer worker emitting an event an older
31
+ * shell doesn't know is normal evolution; a mismatched payload on a KNOWN
32
+ * event is the violation).
33
+ */
34
+ import type { Schema } from '@atolljs/core';
35
+ /**
36
+ * A mountable app contract: the registry key, the prop/event wire schemas,
37
+ * and optionally the worker factory. `P`/`E` are the shell-facing types —
38
+ * they ride the contract as pure type information, so consumers of a
39
+ * framework-rendered island (an Angular component, a Vue SFC) get typed
40
+ * props and narrowed `onEvent` without ever importing that framework.
41
+ */
42
+ export interface IslandContract<P = Record<string, unknown>, E extends Record<string, unknown> = Record<string, unknown>> {
43
+ readonly islandContract: true;
44
+ /** Registry key — the name `mountIsland({ app })` resolves to. */
45
+ readonly app: string;
46
+ /** Prop wire schema — enforced worker-side at mount and updateProps. */
47
+ readonly props?: Schema<P>;
48
+ /** Declared emit vocabulary — declared names' payloads parse at emit(). */
49
+ readonly events?: {
50
+ readonly [K in keyof E & string]: Schema<E[K]>;
51
+ };
52
+ /** The worker factory — optional; when present the contract object itself
53
+ * is a lazy contract module (`{ app, worker }`). */
54
+ readonly worker?: (() => Worker) | URL;
55
+ }
56
+ export declare function defineIslandContract<P = Record<string, unknown>, E extends Record<string, unknown> = Record<string, unknown>>(contract: {
57
+ app: string;
58
+ props?: Schema<P>;
59
+ events?: {
60
+ [K in keyof E & string]: Schema<E[K]>;
61
+ };
62
+ worker?: (() => Worker) | URL;
63
+ }): IslandContract<P, E>;
64
+ /** Structural contract check — the brand, not the shape, so a hand-rolled
65
+ * `{ app, props }` without `defineIslandContract` stays an app reference. */
66
+ export declare const isIslandContract: (value: unknown) => value is IslandContract;
67
+ /** The props a contract admits — `IslandContract<P>` → `P`. */
68
+ export type IslandContractProps<C> = C extends IslandContract<infer P, infer _E> ? P : Record<string, unknown>;
69
+ /** The event vocabulary a contract declares — name → payload map. */
70
+ export type IslandContractEvents<C> = C extends IslandContract<infer _P, infer E> ? E : Record<string, unknown>;
71
+ /**
72
+ * The `onEvent` signature a contract implies — `(name, payload)` with the
73
+ * payload narrowed per event name. Framework-free sibling of
74
+ * `@atolljs/angular-island`'s `IslandEventHandler<C>`.
75
+ */
76
+ export type IslandContractEventHandler<C> = <K extends keyof IslandContractEvents<C> & string>(name: K, payload: IslandContractEvents<C>[K]) => void;
77
+ /**
78
+ * `withContract(contract, app)` — attach a contract to a registry app
79
+ * (imperative def or any framework adapter's `RenderedIslandApp`). The
80
+ * stamp rides the app object like `islandAppName`; the worker reads it at
81
+ * mount/updateProps for prop validation and hands it to `emit` for
82
+ * declared-payload checks.
83
+ *
84
+ * apps: {
85
+ * checkout: withContract(checkout, angularIslandApp(CheckoutComponent)),
86
+ * }
87
+ */
88
+ export declare function withContract<C extends IslandContract, A>(contract: C, app: A): A;
89
+ /** Read the contract a `withContract`-stamped app carries (undefined when
90
+ * unstamped — most registry entries). */
91
+ export declare const contractOf: (app: unknown) => IslandContract | undefined;
92
+ /** The worker factory a contract-carrying `app` reference supplies —
93
+ * mount seams fall back to it when no `worker`/`client` was passed, so
94
+ * `<Island app={contract}/>` and `islandComponent(contract)` are fully
95
+ * self-contained. */
96
+ export declare const contractWorkerOf: (app: unknown) => (() => Worker) | URL | undefined;
@@ -0,0 +1,21 @@
1
+ import type { IslandHandle } from './island.js';
2
+ export interface DevtoolsIslandEntry {
3
+ handle: IslandHandle;
4
+ /** The real container element the island replays into. */
5
+ el: HTMLElement;
6
+ /** The props as the caller last gave them (pre-marshal: markers intact). */
7
+ props(): Record<string, unknown>;
8
+ }
9
+ /** Track a mounted island for the dashboard; returns the untrack function. */
10
+ export declare const trackIslandForDevtools: (entry: DevtoolsIslandEntry) => (() => void);
11
+ export interface IslandTreeNode {
12
+ /** Child-index path from the container: '0', '0.2', '0.2.s' (shadow root). */
13
+ id: string;
14
+ /** Lowercase tag, or '#text' / '#shadow-root'. */
15
+ tag: string;
16
+ attrs: [string, string][];
17
+ text?: string;
18
+ children: IslandTreeNode[];
19
+ /** Children omitted once the node budget ran out. */
20
+ more?: number;
21
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * JSON-safe deep copy of caller-given props: functions and `callbackProp`
3
+ * markers become '[fn]', bigints `'<n>n'`, binary data a size label,
4
+ * cycles '[cycle]'. The shape the dashboard can display and round-trip.
5
+ */
6
+ export declare const jsonSafe: (value: unknown) => unknown;
7
+ /** `island:props` payload: a bounded preview with callbacks as '[fn]'. */
8
+ export declare const propsPreview: (props: Record<string, unknown>) => string;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The main-thread surface of @atolljs/islands — React trees (or
3
+ * imperative proxy-DOM apps) rendered inside Web Workers, replayed onto
4
+ * real DOM here as a serialized op stream. There is no React on this side.
5
+ *
6
+ * const island = await mountIsland({
7
+ * client: connectIslandWorker({
8
+ * worker: () => new Worker(new URL('./render.worker.ts', import.meta.url), { type: 'module' }),
9
+ * }),
10
+ * el: document.getElementById('island')!,
11
+ * app: 'controls',
12
+ * });
13
+ */
14
+ export { connectIslandWorker, isKeepAliveClient, mountIsland } from './island.js';
15
+ export type { ConnectIslandWorkerConfig, IslandClient, IslandHandle, IslandWorkerDefinition, IslandWorkerOptions, Mode, MountIslandOptions, } from './island.js';
16
+ export { makeDoorbell } from './memory.js';
17
+ export type { DoorbellSpec } from './memory.js';
18
+ export { islandApp, islandAppNameOf } from './app.js';
19
+ export type { IslandAppLike, IslandAppProps } from './app.js';
20
+ export { defineIslandContract, isIslandContract, withContract, contractOf, contractWorkerOf, } from './contract.js';
21
+ export type { IslandContract, IslandContractEventHandler, IslandContractEvents, IslandContractProps, } from './contract.js';
22
+ export { callbackProp } from './callbackProps.js';
23
+ export { isEventRef } from './ops.js';
24
+ export type { EventPayload, EventRef, IslandWorkerMethods, MeasuredRect, Op, WireProps, } from './ops.js';
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ import {
2
+ callbackProp,
3
+ connectIslandWorker,
4
+ contractOf,
5
+ contractWorkerOf,
6
+ defineIslandContract,
7
+ isEventRef,
8
+ isIslandContract,
9
+ isKeepAliveClient,
10
+ islandApp,
11
+ islandAppNameOf,
12
+ makeDoorbell,
13
+ mountIsland,
14
+ withContract
15
+ } from "./chunk-TX4FAHRB.js";
16
+ export {
17
+ callbackProp,
18
+ connectIslandWorker,
19
+ contractOf,
20
+ contractWorkerOf,
21
+ defineIslandContract,
22
+ isEventRef,
23
+ isIslandContract,
24
+ isKeepAliveClient,
25
+ islandApp,
26
+ islandAppNameOf,
27
+ makeDoorbell,
28
+ mountIsland,
29
+ withContract
30
+ };
@@ -0,0 +1,175 @@
1
+ import type { ConnectWorkerConfig, SharedSpec, WorkerClient, WorkerDefinition } from '@atolljs/core';
2
+ import { type DoorbellSpec } from './memory.js';
3
+ import { type IslandWorkerMethods, type Op } from './ops.js';
4
+ import type { ProxyElement } from './worker/dom/element.js';
5
+ export type Mode = 'push' | 'poll';
6
+ /** The worker definition every `definePolyWorker` call produces. */
7
+ export type IslandWorkerDefinition = WorkerDefinition<DoorbellSpec, IslandWorkerMethods>;
8
+ /** One island client: one dedicated worker, one doorbell buffer. */
9
+ export type IslandClient = WorkerClient<IslandWorkerDefinition, DoorbellSpec>;
10
+ /**
11
+ * An island client is ONE worker holding its islands' trees (one per mount
12
+ * on it), so pooling is disabled by construction — the type omits
13
+ * `workers`/`poolSize`/`worker`/`sharedMemory` (all island-internal) and the
14
+ * literal below pins `workers: 1` after the spread, so a pool can't sneak
15
+ * through a cast either. `worker` is supplied per call — the package can't
16
+ * know where the consumer's worker entry lives — everything else
17
+ * (taskTimeout, respawn, lazy…) still passes through. The pool-only
18
+ * `concurrency`/`maxQueue` are accepted but have no effect.
19
+ */
20
+ export type IslandWorkerOptions = Omit<ConnectWorkerConfig<SharedSpec>, 'sharedMemory' | 'worker' | 'workers' | 'poolSize'>;
21
+ export interface ConnectIslandWorkerConfig extends IslandWorkerOptions {
22
+ /**
23
+ * Bundler-detectable factory `() => new Worker(new URL('./x.worker.ts',
24
+ * import.meta.url), { type: 'module' })`, or a URL. The worker script must
25
+ * call `definePolyWorker({ apps })` from '@atolljs/islands/worker'.
26
+ */
27
+ worker: (() => Worker) | URL;
28
+ /**
29
+ * Default true — the client carries a SharedArrayBuffer doorbell for
30
+ * push-mode flush (requires COOP/COEP cross-origin isolation). Pass
31
+ * `false` for a message-only client: `setMode('push')` falls back to
32
+ * polling and the page needs no special headers.
33
+ */
34
+ doorbell?: boolean;
35
+ /**
36
+ * Default false — when true, the last island to destroy on this client
37
+ * leaves the worker ALIVE but idle: the instance still unmounts (dispose
38
+ * hooks run, timers clear, ops stop, driver state releases) but the
39
+ * thread, its module graph, and any worker-side caches stay resident, so
40
+ * the next `mount` on this client is a warm start. Nothing runs while
41
+ * idle — the instance is gone; only the process survives.
42
+ */
43
+ keepAlive?: boolean;
44
+ }
45
+ /** True when this client stays resident after its last island destroys. */
46
+ export declare const isKeepAliveClient: (client: object) => boolean;
47
+ /** One island client: one dedicated worker, one doorbell buffer. */
48
+ export declare const connectIslandWorker: <W extends IslandWorkerDefinition = IslandWorkerDefinition>({ worker, doorbell, keepAlive, ...options }: ConnectIslandWorkerConfig) => WorkerClient<W, DoorbellSpec>;
49
+ interface MountIslandBaseOptions {
50
+ /**
51
+ * Container element the island's ops are applied into — a real element on
52
+ * the shell side, or a `ProxyElement` inside a worker-rendered island
53
+ * (nested mount: the sub-island's ops replay into the parent instance's
54
+ * shadow tree and tunnel upward through its op stream). Required.
55
+ */
56
+ el: HTMLElement | ProxyElement;
57
+ /**
58
+ * Registry app name — a key of the `apps` map passed to
59
+ * `definePolyWorker`. Optional against a instance worker
60
+ * (`defineMonoWorker`), which resolves its single app regardless.
61
+ */
62
+ app?: string;
63
+ props?: Record<string, unknown>;
64
+ /** Island → shell channel: receives every `emit` op the app produces. */
65
+ onEvent?: (name: string, payload: unknown) => void;
66
+ /**
67
+ * Transclusion slots — worker markup renders `<div data-atoll-slot="name">`
68
+ * as a LEAF; when its create op lands, the real element is handed to
69
+ * `slots[name](el)` so the shell can mount main-thread content inside it
70
+ * (a widget, a canvas, even a main-thread React root — real DOM, real
71
+ * events, zero wire). Called again with `null` when the worker removes or
72
+ * renames the element, so the shell can tear down. The element itself
73
+ * stays worker-owned — its box/layout props apply normally.
74
+ *
75
+ * Nested mounts (a ProxyElement `el`) receive the anchor's ProxyElement
76
+ * instead, and only claim names present in this object (`name in slots`);
77
+ * unclaimed slot names bubble up to the outer island's `slots`.
78
+ */
79
+ slots?: Record<string, (el: HTMLElement | null) => void>;
80
+ /** Fired after each applied op batch — the shell uses it for stats. */
81
+ onActivity?: () => void;
82
+ /**
83
+ * Perf hook — fired after each op batch is replayed with the batch and the
84
+ * main-thread replay time in ms (performance.now). Pair with client call
85
+ * timing to split worker-vs-main cost for a sub-application.
86
+ */
87
+ onOps?: (ops: readonly Op[], elapsedMs: number) => void;
88
+ /**
89
+ * Initial flush mode — 'push' (default) subscribes the shared-memory
90
+ * doorbell once the mount handshake lands; 'poll' drains committed ops on
91
+ * a 50ms interval. With the `worker` shorthand, 'poll' also builds the
92
+ * client without a doorbell — no SharedArrayBuffer, so no COOP/COEP
93
+ * cross-origin isolation requirement. Switch later with `handle.setMode`.
94
+ */
95
+ mode?: Mode;
96
+ /**
97
+ * Optional renderer/framework tag — pure observability metadata forwarded
98
+ * to devtools on `island:mount` ('react', 'vue', 'svelte', …). When
99
+ * omitted (and devtools is on), the driver asks the worker: package
100
+ * adapters report their renderer, imperative apps report none and the
101
+ * map draws them as plain workers. An explicit tag always wins.
102
+ */
103
+ framework?: string;
104
+ /**
105
+ * Milliseconds the mount handshake (mount + whoami) may take before the
106
+ * returned promise rejects — default 15_000, `0` disables. A worker entry
107
+ * that loads but never answers (no definePolyWorker/defineMonoWorker
108
+ * call, an import that hangs) otherwise leaves mount pending forever;
109
+ * hard failures (module error, crash) already reject immediately.
110
+ */
111
+ mountTimeout?: number;
112
+ /**
113
+ * This mount's `destroy()` never terminates the client: the instance
114
+ * unmounts (dispose hooks, `clear` op replay) but the thread stays
115
+ * resident and idle for a warm remount. The mount-level version of
116
+ * `connectIslandWorker({ keepAlive: true })` — either flag marks the
117
+ * client warm for this island's teardown.
118
+ */
119
+ keepAlive?: boolean;
120
+ }
121
+ /**
122
+ * The mount needs one worker-side endpoint: either a `client` a
123
+ * `connectIslandWorker` call produced (shareable — several islands can
124
+ * mount into one worker), or a `worker` entry factory/URL this call builds
125
+ * its own client from (the common one-island-per-worker form — the client
126
+ * is island-internal and dies with `destroy()`).
127
+ */
128
+ export type MountIslandOptions = MountIslandBaseOptions & ({
129
+ client: IslandClient;
130
+ worker?: undefined;
131
+ } | (IslandWorkerOptions & {
132
+ worker: (() => Worker) | URL;
133
+ client?: undefined;
134
+ }));
135
+ export interface IslandHandle {
136
+ readonly app: string;
137
+ /** The worker-side instance's random id — proof each island is a distinct worker. */
138
+ readonly pid: string;
139
+ /**
140
+ * The wire instance key — `app@N` for a top-level island, `parent~app@N`
141
+ * when mounted into a proxy element inside a worker island.
142
+ */
143
+ readonly instance: string;
144
+ readonly mode: Mode;
145
+ opsApplied: number;
146
+ flushCalls: number;
147
+ /** Re-render the island's root with new serializable props. */
148
+ updateProps(props: Record<string, unknown>): Promise<void>;
149
+ /**
150
+ * Switch how async commits are noticed. The mode given at mount ('push'
151
+ * by default) is started automatically once the handshake lands — call
152
+ * this only to switch modes afterwards. Rebinds of the doorbell contract
153
+ * (in-process harnesses bind every contract on each pool spawn) re-watch
154
+ * transparently.
155
+ */
156
+ setMode(mode: Mode): void;
157
+ /** Manual flush — drains ops committed outside task calls. */
158
+ flush(): Promise<void>;
159
+ /**
160
+ * Stop transport timers/subscriptions and release the instance. When this
161
+ * island owns the client the worker terminates; when the client is shared
162
+ * (several islands mounted into it) the instance unmounts and the worker
163
+ * lives on for its siblings — it dies with the last island to leave.
164
+ */
165
+ destroy(): void;
166
+ }
167
+ export declare const assertCloneableProps: (props: Record<string, unknown>, context: string) => void;
168
+ /** Coerce a wire style value to a CSS string — px-suffixes bare numbers
169
+ * on non-unitless keys (custom properties pass verbatim). Exported for the
170
+ * worker-side nested driver (worker/subIsland.ts), which applies the same
171
+ * unit semantics when replaying style values into proxy elements. */
172
+ export declare const cssStyleValue: (key: string, value: unknown) => string;
173
+ export declare const registerSubIslandMounter: (fn: (opts: MountIslandOptions) => Promise<IslandHandle>) => void;
174
+ export declare function mountIsland(opts: MountIslandOptions): Promise<IslandHandle>;
175
+ export {};
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The doorbell layout. One counter the worker bumps after every commit —
3
+ * the main thread observe()s it (Atomics.waitAsync underneath) and flushes
4
+ * the op queue on each tick. Ops themselves still ride postMessage; shared
5
+ * memory only replaces the flush() poll with a push.
6
+ */
7
+ declare const doorbellSpec: {
8
+ opsVersion: import("@atolljs/core").FieldDescriptor<number>;
9
+ };
10
+ /** The spec `renderMemory`/`makeDoorbell` share — pinned to the doorbell layout. */
11
+ export type DoorbellSpec = typeof doorbellSpec;
12
+ /**
13
+ * The worker-side contract. A worker script has ONE module-level instance —
14
+ * the pool's INIT_MEMORY handshake binds it to that worker's own buffer, so
15
+ * every island's worker gets an independent doorbell for free.
16
+ */
17
+ export declare const renderMemory: import("@atolljs/core").SharedMemory<{
18
+ opsVersion: import("@atolljs/core").FieldDescriptor<number>;
19
+ }> & {
20
+ opsVersion: import("@atolljs/core").Connector<number>;
21
+ };
22
+ /**
23
+ * The main-thread side needs one contract INSTANCE per island: a contract
24
+ * object can only be bound to one buffer at a time (bind() rewires its
25
+ * connectors), and each island client creates its own pool with its own
26
+ * SharedArrayBuffer. Same spec, separate instance → separate buffer → a
27
+ * doorbell that only rings for that island's worker.
28
+ */
29
+ export declare const makeDoorbell: () => import("@atolljs/core").SharedMemory<{
30
+ opsVersion: import("@atolljs/core").FieldDescriptor<number>;
31
+ }> & {
32
+ opsVersion: import("@atolljs/core").Connector<number>;
33
+ };
34
+ export {};
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Internal bookkeeping for measuring the island engine's own cost, separate
3
+ * from application/render work. Disabled by default — the flag check is one
4
+ * predictable branch per chokepoint, so production mounts pay nothing.
5
+ *
6
+ * The one subtlety is re-entrancy: a proxy-DOM method like `el.appendChild`
7
+ * calls `pushOp`, which is itself instrumented. Timing every frame would
8
+ * double-count, so `enter`/`exit` share a depth counter — only the
9
+ * outermost instrumented frame accumulates. The residual
10
+ * (`workerMs − recordMs`) is then application/adapter time.
11
+ *
12
+ * The bench spec additionally calls {@link instrumentPrototype} on the
13
+ * worker-side dom/* classes so imperative-app shadow-tree maintenance is
14
+ * captured too; framework adapters reach the same sinks directly.
15
+ */
16
+ export declare const proxyMetrics: {
17
+ enabled: boolean;
18
+ /** Frames currently inside instrumented proxy code (re-entrancy guard). */
19
+ depth: number;
20
+ /** ms inside outermost instrumented frames since reset(). */
21
+ recordMs: number;
22
+ /** Ops pushed since reset() — a count, free to keep. */
23
+ opsPushed: number;
24
+ reset(): void;
25
+ };
26
+ /**
27
+ * Enter an instrumented frame — returns a start timestamp for the outermost
28
+ * frame, or -1 when nested (in which case exit records nothing).
29
+ */
30
+ export declare const enterProxyFrame: () => number;
31
+ /** Pair with {@link enterProxyFrame} — accumulates only for t0 >= 0. */
32
+ export declare const exitProxyFrame: (t0: number) => void;
33
+ /**
34
+ * Wrap every method/getter/setter on a class prototype with timing. Live
35
+ * for the rest of the process — intended for benchmark instrumentation,
36
+ * not production.
37
+ */
38
+ export declare function instrumentPrototype(proto: object): void;