@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.
- package/dist/app.d.ts +57 -0
- package/dist/callbackProps.d.ts +35 -0
- package/dist/chunk-TX4FAHRB.js +1187 -0
- package/dist/contract.d.ts +96 -0
- package/dist/devtoolsCommands.d.ts +21 -0
- package/dist/devtoolsPreview.d.ts +8 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +30 -0
- package/dist/island.d.ts +175 -0
- package/dist/memory.d.ts +34 -0
- package/dist/metrics.d.ts +38 -0
- package/dist/ops.d.ts +339 -0
- package/dist/worker/defineWorkers.d.ts +207 -0
- package/dist/worker/dom/css.d.ts +24 -0
- package/dist/worker/dom/document.d.ts +274 -0
- package/dist/worker/dom/element.d.ts +199 -0
- package/dist/worker/dom/html.d.ts +7 -0
- package/dist/worker/dom/node.d.ts +172 -0
- package/dist/worker/dom/resizeObserver.d.ts +29 -0
- package/dist/worker/dom/selectors.d.ts +17 -0
- package/dist/worker/dom/window.d.ts +80 -0
- package/dist/worker/index.d.ts +33 -0
- package/dist/worker/index.js +3159 -0
- package/dist/worker/instance.d.ts +187 -0
- package/dist/worker/proxyDom.d.ts +10 -0
- package/dist/worker/subIsland.d.ts +11 -0
- package/package.json +15 -5
- package/src/index.ts +1 -1
- package/src/island.ts +13 -3
- package/src/worker/subIsland.ts +5 -4
package/dist/ops.d.ts
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The whole protocol between the two threads.
|
|
3
|
+
*
|
|
4
|
+
* The worker runs a real React reconciler whose "host environment" is a set
|
|
5
|
+
* of records in a Map — every render-phase and mutation-phase host-config
|
|
6
|
+
* call appends one of these ops to a queue. Task methods (`mount`,
|
|
7
|
+
* `updateProps`, `dispatch`, `flush`) return the flushed batch, and the main
|
|
8
|
+
* thread's only job is to replay them against the DOM. Ops ride postMessage —
|
|
9
|
+
* no shared memory is involved.
|
|
10
|
+
*
|
|
11
|
+
* ISLANDS: every island owns an independent op stream — instance ids are only
|
|
12
|
+
* unique within one worker, so each island's driver keeps its own node map
|
|
13
|
+
* (see src/island.ts). `emit` is the one op that is NOT a DOM mutation: it is
|
|
14
|
+
* the island→shell event channel — the driver hands `{name, payload}` to the
|
|
15
|
+
* shell-registered `onEvent` callback instead of touching the DOM.
|
|
16
|
+
*/
|
|
17
|
+
export type Op =
|
|
18
|
+
/**
|
|
19
|
+
* createElement-equivalent — `props` is already wire-serialized. `ns` is
|
|
20
|
+
* the element's namespace URI when it isn't HTML ('http://www.w3.org/2000/svg',
|
|
21
|
+
* 'http://www.w3.org/1998/Math/MathML') — the driver calls
|
|
22
|
+
* `createElementNS(ns, type)`. The reconciler's host context tracks it
|
|
23
|
+
* (svg/math enter, foreignObject/desc/title return to HTML), so SVG trees
|
|
24
|
+
* like recharts' arrive correctly namespaced.
|
|
25
|
+
*/
|
|
26
|
+
{
|
|
27
|
+
t: 'create';
|
|
28
|
+
id: number;
|
|
29
|
+
type: string;
|
|
30
|
+
props: WireProps;
|
|
31
|
+
ns?: string;
|
|
32
|
+
}
|
|
33
|
+
/** createTextNode-equivalent. */
|
|
34
|
+
| {
|
|
35
|
+
t: 'text';
|
|
36
|
+
id: number;
|
|
37
|
+
text: string;
|
|
38
|
+
}
|
|
39
|
+
/** `parent.insertBefore(child, before ?? null)` — parent 0 is the root container. */
|
|
40
|
+
| {
|
|
41
|
+
t: 'append';
|
|
42
|
+
parent: number;
|
|
43
|
+
child: number;
|
|
44
|
+
before?: number;
|
|
45
|
+
}
|
|
46
|
+
/** Detach `child` from wherever it is mounted. */
|
|
47
|
+
| {
|
|
48
|
+
t: 'remove';
|
|
49
|
+
child: number;
|
|
50
|
+
}
|
|
51
|
+
/** Re-serialized full prop set for `id` (main diffs against its last seen set). */
|
|
52
|
+
| {
|
|
53
|
+
t: 'update';
|
|
54
|
+
id: number;
|
|
55
|
+
props: WireProps;
|
|
56
|
+
}
|
|
57
|
+
/** New text content for a text instance. Also how the proxy DOM writes
|
|
58
|
+
* `element.textContent` — the driver assigns `node.textContent`, which
|
|
59
|
+
* replaces the element's children with a single text node. */
|
|
60
|
+
| {
|
|
61
|
+
t: 'utext';
|
|
62
|
+
id: number;
|
|
63
|
+
text: string;
|
|
64
|
+
}
|
|
65
|
+
/** Clear the root container (React's clearContainer, or an imperative
|
|
66
|
+
* instance's rebuild). */
|
|
67
|
+
| {
|
|
68
|
+
t: 'clear';
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Set/remove a single attribute — emitted by the worker-side proxy DOM
|
|
72
|
+
* (`setAttribute`, `id`/`className` setters, `classList`, `dataset`).
|
|
73
|
+
* `value: null` removes the attribute.
|
|
74
|
+
*/
|
|
75
|
+
| {
|
|
76
|
+
t: 'attr';
|
|
77
|
+
id: number;
|
|
78
|
+
name: string;
|
|
79
|
+
value: string | null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Merge inline-style changes — the proxy DOM's `style` proxy re-sends only
|
|
83
|
+
* the changed keys; `''` clears a key (deleteProperty). Two conventions:
|
|
84
|
+
* keys starting `--` are CSS custom properties (driver routes them to
|
|
85
|
+
* `style.setProperty`), and a value ending in `!important` applies with
|
|
86
|
+
* priority (the proxy's `style.setProperty(k, v, 'important')` encodes it
|
|
87
|
+
* this way — there is no separate priority field).
|
|
88
|
+
*/
|
|
89
|
+
| {
|
|
90
|
+
t: 'style';
|
|
91
|
+
id: number;
|
|
92
|
+
props: Record<string, string>;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Attach a DOM listener for a proxy-DOM `addEventListener` — `handler` is
|
|
96
|
+
* a worker handler-table id (same currency as `__evt` refs); the driver
|
|
97
|
+
* wires it to `client.dispatch(handler, payload)`. `id: 0` targets the
|
|
98
|
+
* island's root container — that's where `document`/`window` listeners
|
|
99
|
+
* land, which makes delegated handlers work. `opts` carries the
|
|
100
|
+
* once/passive/capture flags through to the real addEventListener.
|
|
101
|
+
*/
|
|
102
|
+
| {
|
|
103
|
+
t: 'listen';
|
|
104
|
+
id: number;
|
|
105
|
+
type: string;
|
|
106
|
+
handler: number;
|
|
107
|
+
opts?: {
|
|
108
|
+
once?: boolean;
|
|
109
|
+
passive?: boolean;
|
|
110
|
+
capture?: boolean;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** Detach the listener a `listen` op attached (type + handler identify it).
|
|
114
|
+
* `opts.capture` must match the listen's flag for the detach to apply. */
|
|
115
|
+
| {
|
|
116
|
+
t: 'unlisten';
|
|
117
|
+
id: number;
|
|
118
|
+
type: string;
|
|
119
|
+
handler: number;
|
|
120
|
+
opts?: {
|
|
121
|
+
once?: boolean;
|
|
122
|
+
passive?: boolean;
|
|
123
|
+
capture?: boolean;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Island → shell event — worker code calls `emit(name, payload)` inside a
|
|
128
|
+
* handler (or a commit-phase effect); the driver invokes the island's
|
|
129
|
+
* `onEvent(name, payload)` instead of mutating the DOM. Payloads must be
|
|
130
|
+
* structured-cloneable — same rule as props.
|
|
131
|
+
*/
|
|
132
|
+
| {
|
|
133
|
+
t: 'emit';
|
|
134
|
+
name: string;
|
|
135
|
+
payload?: unknown;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Request real geometry for `id` — the worker-side proxy DOM emits this
|
|
139
|
+
* when `doc.observeSize(el, cb)` registers (and once, lazily, when an
|
|
140
|
+
* unmarked element's geometry is read). The driver measures the real
|
|
141
|
+
* node, observes it with a ResizeObserver, and pushes rects back through
|
|
142
|
+
* the `setRects` task. Fire-and-forget: reads keep returning the honest 0
|
|
143
|
+
* until the first push lands. Older drivers ignore the op; the reads stay
|
|
144
|
+
* 0 — same degradation as an element that never gets measured.
|
|
145
|
+
*/
|
|
146
|
+
| {
|
|
147
|
+
t: 'measure';
|
|
148
|
+
id: number;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Cancel a `measure` request — emitted when the last `observeSize`
|
|
152
|
+
* observer for `id` detaches. Elements auto-requested by a geometry read
|
|
153
|
+
* stay observed for the element's lifetime (the driver also unobserves
|
|
154
|
+
* on `remove`/`clear`).
|
|
155
|
+
*/
|
|
156
|
+
| {
|
|
157
|
+
t: 'unmeasure';
|
|
158
|
+
id: number;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* One measured box, pushed main→worker by the `setRects` task. `x`/`y` are
|
|
162
|
+
* the node's viewport-relative position (getBoundingClientRect), `w`/`h`
|
|
163
|
+
* its size — worker-side reads derive top/left/right/bottom from these.
|
|
164
|
+
* Updates fire on element RESIZE (the driver's ResizeObserver); position-only
|
|
165
|
+
* moves don't retrigger, matching what chart/map libraries actually consume.
|
|
166
|
+
*/
|
|
167
|
+
export interface MeasuredRect {
|
|
168
|
+
id: number;
|
|
169
|
+
x: number;
|
|
170
|
+
y: number;
|
|
171
|
+
w: number;
|
|
172
|
+
h: number;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Wire-serialized props. Functions named `on*` (onClick, onInput, …) cross as
|
|
176
|
+
* `{ __evt: handlerId }`; the main thread turns that into a DOM listener that
|
|
177
|
+
* dispatches back into the worker. `children` never crosses — tree structure
|
|
178
|
+
* is expressed entirely by append/remove ops.
|
|
179
|
+
*
|
|
180
|
+
* One prop is NOT a normal attribute: `data-atoll-slot` marks a transclusion
|
|
181
|
+
* slot — a leaf element whose box is worker-owned but whose contents the
|
|
182
|
+
* shell mounts main-thread DOM into (the island's `slots` registry). The
|
|
183
|
+
* driver routes it to the slot machinery instead of treating it like any
|
|
184
|
+
* other data attribute.
|
|
185
|
+
*/
|
|
186
|
+
export type WireProps = Record<string, unknown>;
|
|
187
|
+
/** Marker for a prop that was a function — a handle into the worker's handler table. */
|
|
188
|
+
export interface EventRef {
|
|
189
|
+
__evt: number;
|
|
190
|
+
}
|
|
191
|
+
export declare const isEventRef: (v: unknown) => v is EventRef;
|
|
192
|
+
/**
|
|
193
|
+
* What the main thread sends back when a DOM event fires. Deliberately NOT a
|
|
194
|
+
* SyntheticEvent — just a best-effort handful of fields read off the DOM
|
|
195
|
+
* event and its target. Mouse fields are `undefined` for non-mouse events;
|
|
196
|
+
* `targetId` is only set when the event target is an op-created node (shell-
|
|
197
|
+
* owned slot content has no instance id).
|
|
198
|
+
*/
|
|
199
|
+
export interface EventPayload {
|
|
200
|
+
type: string;
|
|
201
|
+
value?: string;
|
|
202
|
+
checked?: boolean;
|
|
203
|
+
key?: string;
|
|
204
|
+
clientX?: number;
|
|
205
|
+
clientY?: number;
|
|
206
|
+
screenX?: number;
|
|
207
|
+
screenY?: number;
|
|
208
|
+
/**
|
|
209
|
+
* Pointer coords in the LISTENER element's local space:
|
|
210
|
+
* `clientX/Y − event.currentTarget.getBoundingClientRect()` computed
|
|
211
|
+
* main-thread where real rects exist. The proxy DOM has no measurement
|
|
212
|
+
* channel, so worker-side hit-testing libraries cannot derive local
|
|
213
|
+
* coords themselves: zrender's four-marker getBoundingClientRect
|
|
214
|
+
* transform degenerates, and e.offsetX is absent anyway. zrender's
|
|
215
|
+
* normalizeEvent early-returns on `e.zrX != null`, which is why the
|
|
216
|
+
* fields carry its names — charts (echarts SVG renderer) get working
|
|
217
|
+
* axis pointers/tooltips, any library can read them the same way.
|
|
218
|
+
*/
|
|
219
|
+
zrX?: number;
|
|
220
|
+
zrY?: number;
|
|
221
|
+
button?: number;
|
|
222
|
+
/**
|
|
223
|
+
* UIEvent.which — non-standard but set by every browser (1 = left button,
|
|
224
|
+
* 2 = middle, 3 = right). Libraries like Leaflet's Draggable gate on it:
|
|
225
|
+
* `e.which !== 1 && e.button !== 1` must not both hold for a left press.
|
|
226
|
+
*/
|
|
227
|
+
which?: number;
|
|
228
|
+
/** Modifier states for the event (KeyboardEvent/MouseEvent share them). */
|
|
229
|
+
shiftKey?: boolean;
|
|
230
|
+
ctrlKey?: boolean;
|
|
231
|
+
altKey?: boolean;
|
|
232
|
+
metaKey?: boolean;
|
|
233
|
+
/** WheelEvent deltas — only set for `wheel` events (deltaMode is always
|
|
234
|
+
* 0/pixels in practice; reported raw). */
|
|
235
|
+
deltaX?: number;
|
|
236
|
+
deltaY?: number;
|
|
237
|
+
/**
|
|
238
|
+
* WheelEvent.deltaMode (0 = pixels, 1 = lines, 2 = pages). Libraries like
|
|
239
|
+
* Leaflet gate on `e.deltaMode === 0` inside getWheelDelta, so the driver
|
|
240
|
+
* always sends a concrete number — defaulting to 0 when the environment
|
|
241
|
+
* doesn't expose it but deltas exist.
|
|
242
|
+
*/
|
|
243
|
+
deltaMode?: number;
|
|
244
|
+
/** PointerEvent.pointerType ('mouse' | 'touch' | 'pen' | ''). */
|
|
245
|
+
pointerType?: string;
|
|
246
|
+
/** `target.scrollTop` when the target is an element. */
|
|
247
|
+
scrollTop?: number;
|
|
248
|
+
/** The worker instance id of `event.target`, when it maps to one. */
|
|
249
|
+
targetId?: number;
|
|
250
|
+
/**
|
|
251
|
+
* The worker instance id of `event.relatedTarget` (MouseEvent/FocusEvent),
|
|
252
|
+
* when it maps to an op-created node. Lets over/out/leave/enter handlers
|
|
253
|
+
* distinguish "moved to a child" from "actually left the element" —
|
|
254
|
+
* zrender's mouseout gate (`toElement || relatedTarget` → isLocalEl walk)
|
|
255
|
+
* is the canonical consumer; without it every child transition looks like
|
|
256
|
+
* a real leave and charts flicker as tooltips hide/show.
|
|
257
|
+
*/
|
|
258
|
+
relatedTargetId?: number;
|
|
259
|
+
/**
|
|
260
|
+
* The proxy-DOM node for `targetId` — SYNTHESIZED worker-side by the proxy
|
|
261
|
+
* DOM when it wraps a listener (it never crosses the wire). Lets delegated
|
|
262
|
+
* handlers written for the real DOM — `e.target.closest('.row')`,
|
|
263
|
+
* `e.target.dataset` — run unmodified. Only set when the target maps to a
|
|
264
|
+
* known worker instance; DOM-fidelity only for nodes built through the
|
|
265
|
+
* same proxy document.
|
|
266
|
+
*/
|
|
267
|
+
target?: unknown;
|
|
268
|
+
/**
|
|
269
|
+
* The proxy-DOM node for `relatedTargetId` — SYNTHESIZED worker-side like
|
|
270
|
+
* `target` (never crosses the wire). Handler code can walk
|
|
271
|
+
* `relatedTarget.parentNode` — e.g. zrender's isLocalEl — to test
|
|
272
|
+
* whether the pointer moved within the listener's subtree.
|
|
273
|
+
*/
|
|
274
|
+
relatedTarget?: unknown;
|
|
275
|
+
/**
|
|
276
|
+
* Synthesized NO-OPS — added worker-side by the proxy DOM when it wraps a
|
|
277
|
+
* listener's payload (they never cross the wire). They exist so library
|
|
278
|
+
* code written for real DOM events — `e.preventDefault()`,
|
|
279
|
+
* `e.stopPropagation()` — doesn't crash. They CANNOT cancel anything: the
|
|
280
|
+
* real event already dispatched on the main thread before this payload
|
|
281
|
+
* ever reached the worker. Practical consequence: a `wheel` listener on a
|
|
282
|
+
* scrolling island can't preventDefault — the page scrolls anyway.
|
|
283
|
+
*/
|
|
284
|
+
preventDefault?(): void;
|
|
285
|
+
stopPropagation?(): void;
|
|
286
|
+
stopImmediatePropagation?(): void;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* The task methods every island worker exposes — what `definePolyWorker`
|
|
290
|
+
* registers on the worker side and what `connectIslandWorker`'s typed client
|
|
291
|
+
* calls on the main thread. Every signature leads with the instance key
|
|
292
|
+
* (`'app'` or `'app@instance'`) — `mountIsland` binds it per island.
|
|
293
|
+
*/
|
|
294
|
+
export type IslandWorkerMethods = {
|
|
295
|
+
/** Mount the instance's registry app; returns the initial op batch. Async
|
|
296
|
+
* because a React app lazily imports the reconciler on first mount. */
|
|
297
|
+
mount(instance: string, props?: Record<string, unknown>): Op[] | Promise<Op[]>;
|
|
298
|
+
/** Re-render the instance's root with new serializable props. */
|
|
299
|
+
updateProps(instance: string, props: Record<string, unknown>): Op[];
|
|
300
|
+
/** Invoke the worker handler a `__evt` ref or `listen` op points at. */
|
|
301
|
+
dispatch(handlerId: number, payload: EventPayload): Op[];
|
|
302
|
+
/**
|
|
303
|
+
* Push the island container's measured box into the instance — the
|
|
304
|
+
* always-on geometry channel (per-element rects additionally flow through
|
|
305
|
+
* `setRects` for elements that requested measurement). The driver observes
|
|
306
|
+
* the island's `el` with a ResizeObserver and calls this once at mount and
|
|
307
|
+
* on resizes (throttled ~100ms). The instance stores {w,h}; proxy-DOM
|
|
308
|
+
* geometry reads on `doc.body`/`documentElement` and on elements marked via
|
|
309
|
+
* `doc.markContainer(el)` then return it. Handlers registered via
|
|
310
|
+
* `doc.onResize` re-run on each push; their ops ride back in this method's
|
|
311
|
+
* return batch.
|
|
312
|
+
*/
|
|
313
|
+
setSize(instance: string, width: number, height: number): Op[];
|
|
314
|
+
/**
|
|
315
|
+
* Push freshly measured element rects — the answer to `measure` ops and
|
|
316
|
+
* the driver's per-element ResizeObserver ticks. Each rect updates the
|
|
317
|
+
* instance's per-element store and fires the element's `observeSize`
|
|
318
|
+
* callbacks inside the instance's scope, so their ops ride back in this
|
|
319
|
+
* method's return batch like `setSize`'s onResize handlers.
|
|
320
|
+
*/
|
|
321
|
+
setRects(instance: string, rects: MeasuredRect[]): Op[];
|
|
322
|
+
/** Drain ops committed outside a task (passive effects, timers). */
|
|
323
|
+
flush(instance: string): Op[];
|
|
324
|
+
/**
|
|
325
|
+
* Tear down one instance — unmounts its tree / disposes its proxy document
|
|
326
|
+
* and drops the instance, leaving the worker alive for its other mounts.
|
|
327
|
+
* Returned ops are the detach batch (the driver may skip applying them if
|
|
328
|
+
* the island element is already gone).
|
|
329
|
+
*/
|
|
330
|
+
unmount(instance: string): Op[];
|
|
331
|
+
/** The mounted instance's random id — the island's "worker pid" badge. */
|
|
332
|
+
whoami(instance: string): string;
|
|
333
|
+
/**
|
|
334
|
+
* The instance's renderer tag ('react', 'vue', …) from its adapter; null
|
|
335
|
+
* for imperative apps. Read by the driver only for devtools, and only
|
|
336
|
+
* when the mount declared no `framework`.
|
|
337
|
+
*/
|
|
338
|
+
renderer(instance: string): string | null;
|
|
339
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two entry points, two topologies:
|
|
3
|
+
*
|
|
4
|
+
* // render.worker.ts — a REGISTRY worker: one script, many apps
|
|
5
|
+
* import { definePolyWorker } from '@atolljs/islands/worker';
|
|
6
|
+
* export const renderWorker = definePolyWorker({
|
|
7
|
+
* apps: { controls: ControlsApp, table: TableApp },
|
|
8
|
+
* });
|
|
9
|
+
*
|
|
10
|
+
* // charts.worker.ts — a MONO worker: one script, one app (1:1)
|
|
11
|
+
* export const chartsWorker = defineMonoWorker(ChartsApp);
|
|
12
|
+
*
|
|
13
|
+
* `definePolyWorker` serves a registry — every island's worker runs the
|
|
14
|
+
* same script and `mount(instance, props)` picks a component out of `apps`.
|
|
15
|
+
* `defineMonoWorker` serves exactly one app: the shell mounts it without
|
|
16
|
+
* naming a registry key (or with any name — a single-registered-app worker
|
|
17
|
+
* resolves its sole app regardless), and the worker's bundle carries only
|
|
18
|
+
* that app's dependencies.
|
|
19
|
+
*
|
|
20
|
+
* APP REGISTRY is module-level: in a real worker each entry file loads its
|
|
21
|
+
* own module graph (a instance worker sees exactly its app); under the
|
|
22
|
+
* in-process test harness several worker entries share one graph, so
|
|
23
|
+
* registration is a UNION and mounts still resolve — same semantics both
|
|
24
|
+
* worlds.
|
|
25
|
+
*
|
|
26
|
+
* INSTANCE KEYS are `app` or `app@instance` — the part before the last '@'
|
|
27
|
+
* names the registry app (or 'main' for unnamed instance-worker mounts), the
|
|
28
|
+
* rest distinguishes instances so the same app can mount more than once —
|
|
29
|
+
* INCLUDING into one shared worker when two islands share a client
|
|
30
|
+
* (multi-island-per-worker: one worker, two mounts, two reconcilers).
|
|
31
|
+
* MountIsland mints a fresh key per island. Wire signatures carry the instance key (`updateProps(instance,
|
|
32
|
+
* props)`, `flush(instance)`, `whoami(instance)`) so a call routes to its instance —
|
|
33
|
+
* the main-thread mountIsland helper binds it, so shell code just calls
|
|
34
|
+
* `island.updateProps(props)`.
|
|
35
|
+
*
|
|
36
|
+
* IMPERATIVE INSTANCES: a registry entry can be `{ imperative: (doc, props) }`
|
|
37
|
+
* instead of a component. Those mounts hold no reconciler at all: mount
|
|
38
|
+
* hands the app a instance-scoped proxy DOM and its mutations emit ops
|
|
39
|
+
* directly (updateProps = clear + rebuild, dispatch = runInInstance + drain,
|
|
40
|
+
* flush = drain only). See worker/proxyDom.ts.
|
|
41
|
+
*
|
|
42
|
+
* RENDERED INSTANCES: `{ mount: (ctx) => handle | void }` is the same shape
|
|
43
|
+
* for EVERY framework renderer, React included — mount(ctx) renders into the
|
|
44
|
+
* instance's proxy document and returns a handle whose `update(props)` takes
|
|
45
|
+
* over updateProps (fine-grained patching instead of rebuild), whose
|
|
46
|
+
* `sync(fn)`/`flush()` let task-ordered renderers (react-reconciler) pin
|
|
47
|
+
* commits into the task batch, and whose `dispose()` runs before the
|
|
48
|
+
* document dies. This package ships no renderer: React's lives in
|
|
49
|
+
* `@atolljs/react-island/worker` — framework-neutral by construction.
|
|
50
|
+
*
|
|
51
|
+
* Ops still ride back through the pool's ordinary postMessage channel — the
|
|
52
|
+
* sharedMemory contract here is only the doorbell (see memory.ts): a commit
|
|
53
|
+
* counter the main thread observe()s to trigger flush() as a push. Drop it
|
|
54
|
+
* entirely and the islands still work on the 50ms poll — that's the
|
|
55
|
+
* message-only mode.
|
|
56
|
+
*/
|
|
57
|
+
import type { SharedMemory, WorkerDefinition } from '@atolljs/core';
|
|
58
|
+
import { type IslandContract } from '../contract.js';
|
|
59
|
+
import { type DoorbellSpec } from '../memory.js';
|
|
60
|
+
import { type ProxyDocument } from './proxyDom.js';
|
|
61
|
+
import type { IslandWorkerMethods } from '../ops.js';
|
|
62
|
+
/**
|
|
63
|
+
* Registry value shapes:
|
|
64
|
+
* - `{ imperative: (doc, props) => void }` — no framework at all. mount()
|
|
65
|
+
* hands it a proxy DOM scoped to the instance; its mutations emit ops
|
|
66
|
+
* directly.
|
|
67
|
+
* - `{ mount: (ctx) => handle | void }` — a framework adapter's output
|
|
68
|
+
* (`reactIslandApp`, `vueIslandApp`, …). Bare component functions are NOT
|
|
69
|
+
* registry values — wrap them first (defineReactPolyWorker does it).
|
|
70
|
+
*/
|
|
71
|
+
export interface ImperativeIslandApp {
|
|
72
|
+
imperative: (doc: ProxyDocument, props: Record<string, unknown>) => void;
|
|
73
|
+
/**
|
|
74
|
+
* Optional teardown — runs inside the instance's scope BEFORE its proxy
|
|
75
|
+
* document is disposed (unmount, remount, updateProps rebuild). Cancel
|
|
76
|
+
* library timers/animation loops/listeners here (e.g. `map.remove()`) so
|
|
77
|
+
* deferred work can't mutate a dead instance or a foreign ambient document
|
|
78
|
+
* after teardown.
|
|
79
|
+
*/
|
|
80
|
+
dispose?: (doc: ProxyDocument) => void;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* What a non-React framework renderer receives at mount: the instance's wire
|
|
84
|
+
* key (scopes emit() and instance-less ops), a fresh proxy document whose
|
|
85
|
+
* mutations emit ops, and the serialized props the shell mounted with.
|
|
86
|
+
*/
|
|
87
|
+
export interface RenderContext {
|
|
88
|
+
instance: string;
|
|
89
|
+
doc: ProxyDocument;
|
|
90
|
+
props: Record<string, unknown>;
|
|
91
|
+
}
|
|
92
|
+
export interface RenderedHandle {
|
|
93
|
+
/**
|
|
94
|
+
* Fine-grained prop update — the renderer patches its live tree. When
|
|
95
|
+
* omitted, updateProps falls back to imperative semantics: dispose +
|
|
96
|
+
* clear + re-mount on a fresh document.
|
|
97
|
+
*/
|
|
98
|
+
update?(props: Record<string, unknown>): void;
|
|
99
|
+
/**
|
|
100
|
+
* Run `fn` inside the renderer's synchronous commit lane (React:
|
|
101
|
+
* flushSyncFromReconciler + flushSyncWork) so its mutations emit ops
|
|
102
|
+
* inside the current task batch. Dispatch wraps event-handler invocation
|
|
103
|
+
* in this when the handle provides one; renderers without a sync lane
|
|
104
|
+
* simply omit it.
|
|
105
|
+
*/
|
|
106
|
+
sync?(fn: () => void): void;
|
|
107
|
+
/**
|
|
108
|
+
* Drain renderer work scheduled outside tasks — passive effects, async
|
|
109
|
+
* state updates (React: flushPassiveEffects + flushSyncWork). The worker's
|
|
110
|
+
* flush() method calls it inside the instance's scope, then drains the
|
|
111
|
+
* op queue.
|
|
112
|
+
*/
|
|
113
|
+
flush?(): void;
|
|
114
|
+
/**
|
|
115
|
+
* App teardown — runs inside the instance's scope BEFORE its proxy document
|
|
116
|
+
* is disposed (unmount, remount, updateProps rebuild). Same rules as
|
|
117
|
+
* ImperativeIslandApp.dispose: cancel framework roots/effects here.
|
|
118
|
+
*/
|
|
119
|
+
dispose?(): void;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A renderer-backed app: a framework renderer (React, Vue, Svelte,
|
|
123
|
+
* Solid, Angular) whose `mount` renders component output into the instance's
|
|
124
|
+
* proxy document — every proxy mutation already serializes to ops. Package
|
|
125
|
+
* adapters (e.g. @atolljs/react-island/worker, @atolljs/vue-island/worker)
|
|
126
|
+
* wrap a component into this shape so it can sit beside imperative apps in
|
|
127
|
+
* the same `apps` registry.
|
|
128
|
+
*/
|
|
129
|
+
export interface RenderedIslandApp {
|
|
130
|
+
mount(ctx: RenderContext): RenderedHandle | void;
|
|
131
|
+
/**
|
|
132
|
+
* Renderer tag ('react', 'vue', 'svelte', 'solid', 'angular'): set by the
|
|
133
|
+
* package adapters, reported by the `renderer` task so devtools can mark
|
|
134
|
+
* an island whose mount declared no `framework`. Hand-written adapters
|
|
135
|
+
* may set it; imperative apps have none.
|
|
136
|
+
*/
|
|
137
|
+
renderer?: string;
|
|
138
|
+
}
|
|
139
|
+
/** The apps an island can mount — keyed by the name the shell passes to mount(). */
|
|
140
|
+
export type IslandApp = ImperativeIslandApp | RenderedIslandApp;
|
|
141
|
+
export interface PolyWorkerRegistry {
|
|
142
|
+
/** Name → island app registry. The shell's `mountIsland({ app: name })`
|
|
143
|
+
* picks one per island. */
|
|
144
|
+
apps: Record<string, IslandApp>;
|
|
145
|
+
/**
|
|
146
|
+
* The doorbell contract the worker exposes — defaults to the package's
|
|
147
|
+
* `renderMemory`. Rarely needed: pass a different doorbell-SPEC instance
|
|
148
|
+
* if your worker module already defines one (the main side always uses
|
|
149
|
+
* `makeDoorbell()`, so the spec must match: `opsVersion` number field).
|
|
150
|
+
*/
|
|
151
|
+
sharedMemory?: SharedMemory<DoorbellSpec>;
|
|
152
|
+
}
|
|
153
|
+
interface InstanceBase {
|
|
154
|
+
/** The wire key — 'app' or 'app@instance'; op queues route by this. */
|
|
155
|
+
key: string;
|
|
156
|
+
/** The registry name — which component this instance renders. */
|
|
157
|
+
app: string;
|
|
158
|
+
/** Per-instance random id — shown in the island's badge as proof it's a
|
|
159
|
+
* distinct render instance (in production: a distinct worker). Stable across
|
|
160
|
+
* remounts of the same instance key. */
|
|
161
|
+
pid: string;
|
|
162
|
+
/** The `withContract`-stamped contract — drives props validation at
|
|
163
|
+
* mount/updateProps; mirrored into instanceContracts for `emit`. */
|
|
164
|
+
contract?: IslandContract;
|
|
165
|
+
}
|
|
166
|
+
interface ImperativeInstance extends InstanceBase {
|
|
167
|
+
rendered?: undefined;
|
|
168
|
+
imperative: {
|
|
169
|
+
build: ImperativeIslandApp['imperative'];
|
|
170
|
+
/** Optional app teardown — run before the instance's doc is disposed. */
|
|
171
|
+
dispose: ImperativeIslandApp['dispose'];
|
|
172
|
+
/** The instance's proxy document — replaced on each rebuild. */
|
|
173
|
+
doc: ProxyDocument;
|
|
174
|
+
props: Record<string, unknown>;
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
interface RenderedInstance extends InstanceBase {
|
|
178
|
+
imperative?: undefined;
|
|
179
|
+
rendered: {
|
|
180
|
+
app: RenderedIslandApp;
|
|
181
|
+
/** The mount-returned handle — drives fine-grained updates/teardown. */
|
|
182
|
+
handle: RenderedHandle | void;
|
|
183
|
+
props: Record<string, unknown>;
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/** A mounted instance — one per island worker in production. */
|
|
187
|
+
export type Instance = ImperativeInstance | RenderedInstance;
|
|
188
|
+
/**
|
|
189
|
+
* PolyWorker — one worker script serving a REGISTRY of islands:
|
|
190
|
+
* `mountIsland({ app: name })` picks one per island, and every island in
|
|
191
|
+
* the registry shares the worker's module graph, framework runtimes, and
|
|
192
|
+
* op pump. This is the bundle-optimization host shape — several islands,
|
|
193
|
+
* one worker to load and keep warm.
|
|
194
|
+
*/
|
|
195
|
+
export declare function definePolyWorker(registry: PolyWorkerRegistry, options?: {
|
|
196
|
+
sharedMemory?: SharedMemory<DoorbellSpec>;
|
|
197
|
+
}): WorkerDefinition<DoorbellSpec, IslandWorkerMethods>;
|
|
198
|
+
/**
|
|
199
|
+
* MonoWorker — one worker script pinned to ONE island app (1:1). The
|
|
200
|
+
* isolation host shape: own bundle, own failure domain, nothing reachable
|
|
201
|
+
* outside the single app it serves. `mountIsland` can omit `app` — the
|
|
202
|
+
* worker resolves its sole app regardless of the supplied name.
|
|
203
|
+
*/
|
|
204
|
+
export declare function defineMonoWorker(app: IslandApp, options?: {
|
|
205
|
+
sharedMemory?: SharedMemory<DoorbellSpec>;
|
|
206
|
+
}): WorkerDefinition<DoorbellSpec, IslandWorkerMethods>;
|
|
207
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ProxyElement } from './element.js';
|
|
2
|
+
/** The small subset of DOMTokenList the proxy implements. */
|
|
3
|
+
export interface ProxyClassList {
|
|
4
|
+
add(...tokens: string[]): void;
|
|
5
|
+
remove(...tokens: string[]): void;
|
|
6
|
+
toggle(token: string, force?: boolean): boolean;
|
|
7
|
+
contains(token: string): boolean;
|
|
8
|
+
readonly value: string;
|
|
9
|
+
}
|
|
10
|
+
/** Build (or reuse) a live classList view for the element. */
|
|
11
|
+
export declare function classListFor(el: ProxyElement): ProxyClassList;
|
|
12
|
+
/**
|
|
13
|
+
* Build (or reuse) a live CSSStyleDeclaration proxy. Writes re-send only the
|
|
14
|
+
* changed key; `delete style.x`/`style.removeProperty('x')` send '' (the
|
|
15
|
+
* protocol's "clear this key"). `setProperty`/`getPropertyValue` accept
|
|
16
|
+
* kebab-case and camelize it, like the real CSSStyleDeclaration.
|
|
17
|
+
*/
|
|
18
|
+
export declare function styleProxyFor(el: ProxyElement): CSSStyleDeclaration;
|
|
19
|
+
/**
|
|
20
|
+
* Build (or reuse) a live DOMStringMap proxy. camelCase keys map to `data-*`
|
|
21
|
+
* attribute ops: `el.dataset.foo = 'x'` sets `data-foo`, `delete el.dataset.foo`
|
|
22
|
+
* removes it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function datasetProxyFor(el: ProxyElement): DOMStringMap;
|