@atolljs/islands 0.1.12 → 0.1.14

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,187 @@
1
+ /**
2
+ * Instance-scoped state shared by every renderer backend running inside a worker.
3
+ *
4
+ * - Per-instance op queues: each island/app owns a queue of {@link Op}s that the
5
+ * main-thread driver replays as real DOM mutations.
6
+ * - A shared id counter for element/text instances, used by both the React
7
+ * reconciler and the proxy DOM.
8
+ * - Handler registry for event listeners.
9
+ * - Doorbell contract used to bump the ops-version after a commit.
10
+ * - Pushed container-size geometry.
11
+ *
12
+ * This module is renderer-agnostic: it contains no react-reconciler imports.
13
+ * The React-specific host config lives in `hostConfig.ts`.
14
+ */
15
+ import type { IslandContract } from '../contract.js';
16
+ import type { Op, WireProps } from '../ops.js';
17
+ export interface ElementInstance {
18
+ kind: 'element';
19
+ id: number;
20
+ type: string;
21
+ /** Which mounted app this node belongs to — routes its ops. */
22
+ instance: string;
23
+ /** Namespace URI for non-HTML elements (svg/mathml) — rides the `create`
24
+ * op so the driver uses `createElementNS`. Tracked via host context. */
25
+ ns?: string;
26
+ /** Last serialized prop set — needed so hide/unhide can resend it. */
27
+ props: WireProps;
28
+ /** propName → stable handler id (keeps DOM listeners stable across updates). */
29
+ listenerSlots: Record<string, number>;
30
+ }
31
+ export interface TextInstance {
32
+ kind: 'text';
33
+ id: number;
34
+ text: string;
35
+ instance: string;
36
+ }
37
+ export type HostInstance = ElementInstance | TextInstance;
38
+ /** The root container is a sentinel — op `parent: 0` means "the root".
39
+ * definePolyWorker stamps `instance` on the container it hands
40
+ * createContainer, so createInstance can bind every element to the instance
41
+ * that rendered it — deterministic even when a commit runs outside a instance
42
+ * task (passive-effect renders, scheduler flushes), where activeInstance is
43
+ * ''. Instance-bound ops then always reach their island's queue. */
44
+ export interface RootContainer {
45
+ id: 0;
46
+ instance?: string;
47
+ }
48
+ export declare const ROOT_CONTAINER: RootContainer;
49
+ /**
50
+ * instance id → host record. Exported so the proxy DOM (worker/proxyDom.ts)
51
+ * shares the SAME instance space as the reconciler: a proxy-created element
52
+ * can nest inside a React-rendered parent and vice versa, because ops are
53
+ * id-addressed and ids come from one counter.
54
+ */
55
+ export declare const instances: Map<number, HostInstance>;
56
+ interface HandlerEntry {
57
+ fn: (payload: unknown) => void;
58
+ instance: string;
59
+ /** The element this handler was attached to — the event's currentTarget. */
60
+ instanceId?: number;
61
+ }
62
+ /** Move an instance record + its handler ids out of the live maps. */
63
+ export declare const detachInstance: (id: number, handlerIds: number[]) => void;
64
+ /** Restore an instance record + handler ids on re-append (idempotent). */
65
+ export declare const attachInstance: (id: number, handlerIds: number[]) => void;
66
+ /** instances.get that also resolves detached records — stale dispatches and
67
+ * relatedTarget materialization may still reference them. */
68
+ export declare const lookupInstance: (id: number) => HostInstance | undefined;
69
+ export declare const setInstanceContract: (instance: string, contract: IslandContract | undefined) => void;
70
+ /** Allocate an instance id — the shared counter React and the proxy DOM both draw from. */
71
+ export declare const allocId: () => number;
72
+ export declare const setActiveInstance: (instance: string) => string;
73
+ /** The instance the current task is running under — '' outside a instance task. */
74
+ export declare const getActiveInstance: () => string;
75
+ /**
76
+ * The most recent instance a task ran under — used by the instance dispatcher to
77
+ * route deferred library work (timers, promise continuations) to the right
78
+ * document when several mounts share one module (in-process tests).
79
+ */
80
+ export declare const getLastActiveInstance: () => string;
81
+ export declare const getLastTouchedInstance: () => string;
82
+ /**
83
+ * Marks `instance` as the ambient instance for out-of-task readers (timers,
84
+ * continuations, shim consumers outside instance work). Called by
85
+ * installDomShim so its document becomes the ambient document.
86
+ */
87
+ export declare const markInstanceActive: (instance: string) => void;
88
+ /**
89
+ * Run `fn` while `instance` is the active instance — the imperative-code twin of
90
+ * the reconciler's syncCommit wrapper. Imperative DOM writes (`emit`, the
91
+ * proxy DOM's `emit` calls, `clear`) only route correctly while a instance
92
+ * task holds the active instance: mount, updateProps, and dispatch all wrap
93
+ * their work in this, and imperative apps should do the same for any
94
+ * worker-initiated work (timers, promise continuations).
95
+ */
96
+ export declare const runInInstance: <T>(instance: string, fn: () => T) => T;
97
+ /** Queue an op onto a instance's queue — instance-bound ops pass their
98
+ * instance's instance, instance-less ops (`clear`, `emit`) the active one. */
99
+ export declare const pushOp: (instance: string, op: Op) => void;
100
+ /** The minimum surface `bumpOpsVersion` needs off the doorbell contract. */
101
+ interface DoorbellContract {
102
+ readonly bound: boolean;
103
+ connector(path: string): {
104
+ read(): unknown;
105
+ write(v: unknown): void;
106
+ };
107
+ }
108
+ /** Point the doorbell writes at a different doorbell-spec contract. */
109
+ export declare const setDoorbellContract: (contract: DoorbellContract) => void;
110
+ /**
111
+ * The doorbell write resetAfterCommit performs, exported so non-React op
112
+ * producers (the proxy DOM) can ring it too — a commit made outside any
113
+ * task still needs to wake the island's observe() loop. No-op while the
114
+ * contract is unbound (before the pool's INIT_MEMORY handshake).
115
+ */
116
+ export declare const bumpOpsVersion: () => void;
117
+ /** Drain one instance's queued ops — called by the worker's task methods. */
118
+ export declare const takeOps: (instance: string) => Op[];
119
+ /**
120
+ * Structural memory snapshot of the proxy layer — how much shadow state the
121
+ * engine retains for live nodes, handlers, and ops still queued. Removed
122
+ * nodes leave `instances` for a bounded tombstone (event dispatch may still
123
+ * resolve recently-detached target ids), so liveNodes is attached-only.
124
+ */
125
+ export declare const proxyInstanceStats: () => {
126
+ liveNodes: number;
127
+ handlers: number;
128
+ queuedOps: number;
129
+ queuedByInstance: Record<string, number>;
130
+ };
131
+ export declare const getHandler: (id: number) => HandlerEntry | undefined;
132
+ /** The instance's last pushed container size, or undefined if none arrived. */
133
+ export declare const getInstanceSize: (instance: string) => {
134
+ w: number;
135
+ h: number;
136
+ } | undefined;
137
+ /** Store the container size a `setSize` task pushed. */
138
+ export declare const setInstanceSize: (instance: string, w: number, h: number) => void;
139
+ /** The last pushed rect for one element id, or undefined if never measured. */
140
+ export declare const getInstanceRect: (instance: string, id: number) => {
141
+ x: number;
142
+ y: number;
143
+ w: number;
144
+ h: number;
145
+ } | undefined;
146
+ /** Store one element's measured box from a `setRects` push. */
147
+ export declare const setInstanceRect: (instance: string, rect: {
148
+ id: number;
149
+ x: number;
150
+ y: number;
151
+ w: number;
152
+ h: number;
153
+ }) => void;
154
+ /** Drop every measured rect for the instance — the unmount path calls this
155
+ * so stale geometry can't answer reads on a re-mounted instance's ids. */
156
+ export declare const clearInstanceRects: (instance: string) => void;
157
+ /**
158
+ * Register a function in the handler table outside prop serialization —
159
+ * the proxy DOM's addEventListener uses this. The instance is stamped on the
160
+ * entry so a dispatched event routes its re-render ops to the right queue.
161
+ * `listen`/`unlisten` ops carry the returned id so the main thread can wire
162
+ * and later detach the matching DOM listener.
163
+ */
164
+ export declare const registerHandler: (fn: (payload: unknown) => void, instance: string, instanceId?: number) => number;
165
+ export declare const unregisterHandler: (id: number) => void;
166
+ /**
167
+ * The island→shell channel. Apps call `emit(name, payload)` inside event
168
+ * handlers or commit-phase effects — it just queues an `emit` op, which the
169
+ * main-thread driver routes to the island's `onEvent` callback instead of
170
+ * the DOM. Call it while a task holds the instance (handlers, layout effects);
171
+ * a bare emit from a passive effect has no instance to route to in the
172
+ * many-mounts-per-module case and would be dropped.
173
+ */
174
+ export declare const emit: (name: string, payload?: unknown) => void;
175
+ /**
176
+ * Props must cross postMessage, so: `children` is skipped (structure comes
177
+ * from append/remove ops), `key`/`ref` are skipped, functions named `on*`
178
+ * become `{ __evt: id }`, everything else passes through structured clone.
179
+ * Each (instance, propName) pair reuses one handler-id slot, so the id the
180
+ * main thread sees never changes across re-renders — it can attach the DOM
181
+ * listener exactly once while the handler id keeps pointing at the latest
182
+ * closure.
183
+ */
184
+ export declare function serializeProps(instance: ElementInstance, props: Record<string, unknown>): WireProps;
185
+ export declare function newElement(type: string, props: Record<string, unknown>, ns: string | undefined, instance: string): ElementInstance;
186
+ export declare function newText(text: string, instance: string): TextInstance;
187
+ export {};
@@ -0,0 +1,10 @@
1
+ export { ProxyNode, ProxyText, ProxyComment, ProxyFragment, } from './dom/node.js';
2
+ export type { ProxyEventHandler, WireListenerOpts } from './dom/node.js';
3
+ export { ProxyElement, } from './dom/element.js';
4
+ export type { AdjacentPosition } from './dom/element.js';
5
+ export type { ProxyClassList } from './dom/css.js';
6
+ export { createProxyDocument, docForInstance, peekInstanceDoc, disposeInstanceDoc, } from './dom/document.js';
7
+ export type { ProxyDocument, InternalDocument, } from './dom/document.js';
8
+ export { installInstanceDispatcher, installDomShim, } from './dom/window.js';
9
+ export { ProxyResizeObserver, } from './dom/resizeObserver.js';
10
+ export type { WindowShim, WindowFacadeBundle, } from './dom/window.js';
@@ -0,0 +1,11 @@
1
+ import { type IslandHandle, type MountIslandOptions } from '../island.js';
2
+ /**
3
+ * The surface is ONE function: `mountIsland`. A proxy `el` makes the mount
4
+ * nested — these are the same options either way. `slots` receive the
5
+ * anchor's ProxyElement (worker-side content portals into it); slot names
6
+ * this mount doesn't list bubble up to the outer island's `slots`.
7
+ */
8
+ export type MountSubIslandOptions = MountIslandOptions;
9
+ /** One handle shape for both mount targets — `instance` reads `parent~app@N`. */
10
+ export type SubIslandHandle = IslandHandle;
11
+ export declare function mountSubIsland(opts: MountSubIslandOptions): Promise<SubIslandHandle>;
package/package.json CHANGED
@@ -1,16 +1,26 @@
1
1
  {
2
2
  "name": "@atolljs/islands",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Atoll islands — MonoWorker/PolyWorker hosts rendering framework islands (React/Vue/Svelte/Solid/Angular) over a proxy-DOM op protocol",
5
5
  "homepage": "https://jwhenry3.github.io/atolljs/consumer/quickstart/",
6
6
  "type": "module",
7
7
  "exports": {
8
- ".": "./src/index.ts",
9
- "./worker": "./src/worker/index.ts"
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./worker": {
13
+ "types": "./dist/worker/index.d.ts",
14
+ "default": "./dist/worker/index.js"
15
+ }
10
16
  },
11
17
  "files": [
12
- "src"
18
+ "src",
19
+ "dist"
13
20
  ],
21
+ "scripts": {
22
+ "build": "node ../../scripts/build-lib.mjs"
23
+ },
14
24
  "publishConfig": {
15
25
  "access": "public",
16
26
  "registry": "https://registry.npmjs.org"
@@ -19,7 +29,7 @@
19
29
  "htmlparser2": "^12.0.0"
20
30
  },
21
31
  "peerDependencies": {
22
- "@atolljs/core": "0.1.12"
32
+ "@atolljs/core": "^0.1.14"
23
33
  },
24
34
  "repository": {
25
35
  "type": "git",
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  * app: 'controls',
12
12
  * });
13
13
  */
14
- export { connectIslandWorker, mountIsland } from './island';
14
+ export { connectIslandWorker, isKeepAliveClient, mountIsland } from './island';
15
15
  export type {
16
16
  ConnectIslandWorkerConfig,
17
17
  IslandClient,
package/src/island.ts CHANGED
@@ -181,6 +181,14 @@ interface MountIslandBaseOptions {
181
181
  * hard failures (module error, crash) already reject immediately.
182
182
  */
183
183
  mountTimeout?: number;
184
+ /**
185
+ * This mount's `destroy()` never terminates the client: the instance
186
+ * unmounts (dispose hooks, `clear` op replay) but the thread stays
187
+ * resident and idle for a warm remount. The mount-level version of
188
+ * `connectIslandWorker({ keepAlive: true })` — either flag marks the
189
+ * client warm for this island's teardown.
190
+ */
191
+ keepAlive?: boolean;
184
192
  }
185
193
 
186
194
  /**
@@ -1369,9 +1377,11 @@ export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandl
1369
1377
  cbTable.clear();
1370
1378
  slotNodes.clear();
1371
1379
  dispatchQueue.length = 0;
1372
- // keepAlive clients stay resident — the instance is unmounted but
1373
- // the thread idles so the next mount is a warm start.
1374
- if (remaining <= 0 && !isKeepAliveClient(client) && (clientMounts.get(client) ?? 0) === 0)
1380
+ // keepAlive (client-level flag or this mount's override) keeps the
1381
+ // thread resident — the instance is unmounted but the worker idles
1382
+ // so the next mount is a warm start.
1383
+ const warm = opts.keepAlive === true || isKeepAliveClient(client);
1384
+ if (remaining <= 0 && !warm && (clientMounts.get(client) ?? 0) === 0)
1375
1385
  client.terminate();
1376
1386
  };
1377
1387
  void islandCall('unmount', client.unmount(instance))
@@ -650,10 +650,11 @@ export async function mountSubIsland(opts: MountSubIslandOptions): Promise<SubIs
650
650
  if (pollTimer !== null) clearInterval(pollTimer);
651
651
  const remaining = (clientMounts.get(client) ?? 1) - 1;
652
652
  clientMounts.set(client, remaining);
653
- // keepAlive clients release the instance but keep the thread warm;
654
- // worker-owned and last-mount clients terminate outright.
655
- if (!isKeepAliveClient(client) && (givenClient === undefined || remaining <= 0))
656
- client.terminate();
653
+ // keepAlive (client-level or this mount's override) releases the
654
+ // instance but keeps the thread warm; worker-owned and last-mount
655
+ // clients terminate outright.
656
+ const warm = opts.keepAlive === true || isKeepAliveClient(client);
657
+ if (!warm && (givenClient === undefined || remaining <= 0)) client.terminate();
657
658
  else void islandCall('unmount', client.unmount(instance)).catch(() => {});
658
659
  },
659
660
  };