@atolljs/islands 0.1.0
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/README.md +346 -0
- package/package.json +29 -0
- package/src/app.ts +81 -0
- package/src/callbackProps.ts +75 -0
- package/src/index.ts +40 -0
- package/src/island.ts +905 -0
- package/src/memory.ts +30 -0
- package/src/ops.ts +215 -0
- package/src/react-reconciler.d.ts +18 -0
- package/src/worker/defineWorkers.ts +666 -0
- package/src/worker/dom/css.ts +152 -0
- package/src/worker/dom/document.ts +589 -0
- package/src/worker/dom/element.ts +656 -0
- package/src/worker/dom/html.ts +107 -0
- package/src/worker/dom/node.ts +481 -0
- package/src/worker/dom/selectors.ts +108 -0
- package/src/worker/dom/window.ts +528 -0
- package/src/worker/hostConfig.ts +446 -0
- package/src/worker/index.ts +78 -0
- package/src/worker/instance.ts +317 -0
- package/src/worker/proxyDom.ts +28 -0
- package/src/worker/reactInstance.ts +74 -0
- package/src/worker/slot.tsx +18 -0
|
@@ -0,0 +1,317 @@
|
|
|
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
|
+
|
|
16
|
+
import { renderMemory } from '../memory';
|
|
17
|
+
import type { Op, WireProps } from '../ops';
|
|
18
|
+
|
|
19
|
+
/* ── Host instances ─────────────────────────────────────────────────────── */
|
|
20
|
+
|
|
21
|
+
export interface ElementInstance {
|
|
22
|
+
kind: 'element';
|
|
23
|
+
id: number;
|
|
24
|
+
type: string;
|
|
25
|
+
/** Which mounted app this node belongs to — routes its ops. */
|
|
26
|
+
instance: string;
|
|
27
|
+
/** Namespace URI for non-HTML elements (svg/mathml) — rides the `create`
|
|
28
|
+
* op so the driver uses `createElementNS`. Tracked via host context. */
|
|
29
|
+
ns?: string;
|
|
30
|
+
/** Last serialized prop set — needed so hide/unhide can resend it. */
|
|
31
|
+
props: WireProps;
|
|
32
|
+
/** propName → stable handler id (keeps DOM listeners stable across updates). */
|
|
33
|
+
listenerSlots: Record<string, number>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TextInstance {
|
|
37
|
+
kind: 'text';
|
|
38
|
+
id: number;
|
|
39
|
+
text: string;
|
|
40
|
+
instance: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type HostInstance = ElementInstance | TextInstance;
|
|
44
|
+
|
|
45
|
+
/** The root container is a sentinel — op `parent: 0` means "the root".
|
|
46
|
+
* definePolyWorker stamps `instance` on the container it hands
|
|
47
|
+
* createContainer, so createInstance can bind every element to the instance
|
|
48
|
+
* that rendered it — deterministic even when a commit runs outside a instance
|
|
49
|
+
* task (passive-effect renders, scheduler flushes), where activeInstance is
|
|
50
|
+
* ''. Instance-bound ops then always reach their island's queue. */
|
|
51
|
+
export interface RootContainer {
|
|
52
|
+
id: 0;
|
|
53
|
+
instance?: string;
|
|
54
|
+
}
|
|
55
|
+
export const ROOT_CONTAINER = Object.freeze({ id: 0 }) as RootContainer;
|
|
56
|
+
|
|
57
|
+
/* ── Per-instance op queues + instance/handler tables ──────────────────────── */
|
|
58
|
+
|
|
59
|
+
/** instance (app name) → queued ops. Each island flushes only its own queue. */
|
|
60
|
+
const opsByInstance = new Map<string, Op[]>();
|
|
61
|
+
/** The instance a task is currently executing under — see setActiveInstance. */
|
|
62
|
+
let activeInstance = '';
|
|
63
|
+
/**
|
|
64
|
+
* instance id → host record. Exported so the proxy DOM (worker/proxyDom.ts)
|
|
65
|
+
* shares the SAME instance space as the reconciler: a proxy-created element
|
|
66
|
+
* can nest inside a React-rendered parent and vice versa, because ops are
|
|
67
|
+
* id-addressed and ids come from one counter.
|
|
68
|
+
*/
|
|
69
|
+
export const instances = new Map<number, HostInstance>();
|
|
70
|
+
interface HandlerEntry {
|
|
71
|
+
fn: (payload: unknown) => void;
|
|
72
|
+
instance: string;
|
|
73
|
+
/** The element this handler was attached to — the event's currentTarget. */
|
|
74
|
+
instanceId?: number;
|
|
75
|
+
}
|
|
76
|
+
/** handler id → prop function + owning instance. Lives worker-side; never serialized. */
|
|
77
|
+
const handlers = new Map<number, HandlerEntry>();
|
|
78
|
+
let nextId = 1;
|
|
79
|
+
let nextHandlerId = 1;
|
|
80
|
+
|
|
81
|
+
/** Allocate an instance id — the shared counter React and the proxy DOM both draw from. */
|
|
82
|
+
export const allocId = (): number => nextId++;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Marks which instance the currently-running task belongs to; returns the
|
|
86
|
+
* previous value for restore. Structural ops don't need it — they route by
|
|
87
|
+
* their instance's instance — but instance-less ops (`clear`, `emit`) and
|
|
88
|
+
* handler dispatch do. Worker tasks run synchronously, so a single pointer
|
|
89
|
+
* is safe even when several mounts coexist in one module.
|
|
90
|
+
*/
|
|
91
|
+
let lastInstance = '';
|
|
92
|
+
export const setActiveInstance = (instance: string): string => {
|
|
93
|
+
const prev = activeInstance;
|
|
94
|
+
activeInstance = instance;
|
|
95
|
+
if (instance !== '') lastInstance = instance;
|
|
96
|
+
return prev;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** The instance the current task is running under — '' outside a instance task. */
|
|
100
|
+
export const getActiveInstance = (): string => activeInstance;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The most recent instance a task ran under — used by the instance dispatcher to
|
|
104
|
+
* route deferred library work (timers, promise continuations) to the right
|
|
105
|
+
* document when several mounts share one module (in-process tests).
|
|
106
|
+
*/
|
|
107
|
+
export const getLastActiveInstance = (): string => lastInstance;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The most recent instance an op was pushed for — stamped in `pushOp`, so
|
|
111
|
+
* every proxy-DOM mutation records it. This is the last rung of the
|
|
112
|
+
* ambient-resolution chain (active → lastActive → lastTouched): a renderer
|
|
113
|
+
* whose scheduler flushes outside any task still lands on the instance whose
|
|
114
|
+
* tree it just mutated.
|
|
115
|
+
*/
|
|
116
|
+
let touchedInstance = '';
|
|
117
|
+
export const getLastTouchedInstance = (): string => touchedInstance;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Marks `instance` as the ambient instance for out-of-task readers (timers,
|
|
121
|
+
* continuations, shim consumers outside instance work). Called by
|
|
122
|
+
* installDomShim so its document becomes the ambient document.
|
|
123
|
+
*/
|
|
124
|
+
export const markInstanceActive = (instance: string): void => {
|
|
125
|
+
if (instance) lastInstance = instance;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Run `fn` while `instance` is the active instance — the imperative-code twin of
|
|
130
|
+
* the reconciler's syncCommit wrapper. Imperative DOM writes (`emit`, the
|
|
131
|
+
* proxy DOM's `emit` calls, `clear`) only route correctly while a instance
|
|
132
|
+
* task holds the active instance: mount, updateProps, and dispatch all wrap
|
|
133
|
+
* their work in this, and imperative apps should do the same for any
|
|
134
|
+
* worker-initiated work (timers, promise continuations).
|
|
135
|
+
*/
|
|
136
|
+
export const runInInstance = <T>(instance: string, fn: () => T): T => {
|
|
137
|
+
const prev = setActiveInstance(instance);
|
|
138
|
+
try {
|
|
139
|
+
return fn();
|
|
140
|
+
} finally {
|
|
141
|
+
setActiveInstance(prev);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** Queue an op onto a instance's queue — instance-bound ops pass their
|
|
146
|
+
* instance's instance, instance-less ops (`clear`, `emit`) the active one. */
|
|
147
|
+
export const pushOp = (instance: string, op: Op): void => {
|
|
148
|
+
if (instance !== '') touchedInstance = instance;
|
|
149
|
+
let queue = opsByInstance.get(instance);
|
|
150
|
+
if (!queue) opsByInstance.set(instance, (queue = []));
|
|
151
|
+
queue.push(op);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** The minimum surface `bumpOpsVersion` needs off the doorbell contract. */
|
|
155
|
+
interface DoorbellContract {
|
|
156
|
+
readonly bound: boolean;
|
|
157
|
+
connector(path: string): { read(): unknown; write(v: unknown): void };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The contract `bumpOpsVersion` writes to — `renderMemory` by default, or
|
|
162
|
+
* whatever doorbell-shaped contract `definePolyWorker({ sharedMemory })`
|
|
163
|
+
* was handed. Overriding requires the SAME spec as the main-side
|
|
164
|
+
* `makeDoorbell()` (the pool sizes the buffer for it), so this is a rarely
|
|
165
|
+
* needed escape hatch, not a general field contract.
|
|
166
|
+
*/
|
|
167
|
+
let doorbell: DoorbellContract = renderMemory;
|
|
168
|
+
|
|
169
|
+
/** Point the doorbell writes at a different doorbell-spec contract. */
|
|
170
|
+
export const setDoorbellContract = (contract: DoorbellContract): void => {
|
|
171
|
+
doorbell = contract;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The doorbell write resetAfterCommit performs, exported so non-React op
|
|
176
|
+
* producers (the proxy DOM) can ring it too — a commit made outside any
|
|
177
|
+
* task still needs to wake the island's observe() loop. No-op while the
|
|
178
|
+
* contract is unbound (before the pool's INIT_MEMORY handshake).
|
|
179
|
+
*/
|
|
180
|
+
export const bumpOpsVersion = (): void => {
|
|
181
|
+
if (!doorbell.bound) return;
|
|
182
|
+
const bell = doorbell.connector('opsVersion');
|
|
183
|
+
bell.write(Number(bell.read() ?? 0) + 1);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/** Drain one instance's queued ops — called by the worker's task methods. */
|
|
187
|
+
export const takeOps = (instance: string): Op[] => {
|
|
188
|
+
const queue = opsByInstance.get(instance);
|
|
189
|
+
if (!queue || queue.length === 0) return [];
|
|
190
|
+
opsByInstance.set(instance, []);
|
|
191
|
+
return queue;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const getHandler = (id: number): HandlerEntry | undefined => handlers.get(id);
|
|
195
|
+
|
|
196
|
+
/* ── Pushed container size (setSize channel) ────────────────────────────── */
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* instance key → the island container's last measured {w,h}, pushed by the
|
|
200
|
+
* driver's `setSize` task. This is the ONLY geometry channel: the main
|
|
201
|
+
* thread can only measure the island's root box, so the proxy DOM serves
|
|
202
|
+
* this value to `doc.body`/`documentElement` and to elements explicitly
|
|
203
|
+
* marked `doc.markContainer(el)` — every other element keeps the honest 0.
|
|
204
|
+
* Module-level (not per-document) so it survives imperative rebuilds that
|
|
205
|
+
* swap in a fresh proxy document.
|
|
206
|
+
*/
|
|
207
|
+
const instanceSizes = new Map<string, { w: number; h: number }>();
|
|
208
|
+
|
|
209
|
+
/** The instance's last pushed container size, or undefined if none arrived. */
|
|
210
|
+
export const getInstanceSize = (instance: string): { w: number; h: number } | undefined =>
|
|
211
|
+
instanceSizes.get(instance);
|
|
212
|
+
|
|
213
|
+
/** Store the container size a `setSize` task pushed. */
|
|
214
|
+
export const setInstanceSize = (instance: string, w: number, h: number): void => {
|
|
215
|
+
instanceSizes.set(instance, { w, h });
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Register a function in the handler table outside prop serialization —
|
|
220
|
+
* the proxy DOM's addEventListener uses this. The instance is stamped on the
|
|
221
|
+
* entry so a dispatched event routes its re-render ops to the right queue.
|
|
222
|
+
* `listen`/`unlisten` ops carry the returned id so the main thread can wire
|
|
223
|
+
* and later detach the matching DOM listener.
|
|
224
|
+
*/
|
|
225
|
+
export const registerHandler = (
|
|
226
|
+
fn: (payload: unknown) => void,
|
|
227
|
+
instance: string,
|
|
228
|
+
instanceId?: number,
|
|
229
|
+
): number => {
|
|
230
|
+
const id = nextHandlerId++;
|
|
231
|
+
handlers.set(id, { fn, instance, instanceId });
|
|
232
|
+
return id;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export const unregisterHandler = (id: number): void => {
|
|
236
|
+
handlers.delete(id);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The island→shell channel. Apps call `emit(name, payload)` inside event
|
|
241
|
+
* handlers or commit-phase effects — it just queues an `emit` op, which the
|
|
242
|
+
* main-thread driver routes to the island's `onEvent` callback instead of
|
|
243
|
+
* the DOM. Call it while a task holds the instance (handlers, layout effects);
|
|
244
|
+
* a bare emit from a passive effect has no instance to route to in the
|
|
245
|
+
* many-mounts-per-module case and would be dropped.
|
|
246
|
+
*/
|
|
247
|
+
export const emit = (name: string, payload?: unknown): void => {
|
|
248
|
+
pushOp(activeInstance, { t: 'emit', name, payload });
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/* ── Prop serialization ─────────────────────────────────────────────────── */
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Props must cross postMessage, so: `children` is skipped (structure comes
|
|
255
|
+
* from append/remove ops), `key`/`ref` are skipped, functions named `on*`
|
|
256
|
+
* become `{ __evt: id }`, everything else passes through structured clone.
|
|
257
|
+
* Each (instance, propName) pair reuses one handler-id slot, so the id the
|
|
258
|
+
* main thread sees never changes across re-renders — it can attach the DOM
|
|
259
|
+
* listener exactly once while the handler id keeps pointing at the latest
|
|
260
|
+
* closure.
|
|
261
|
+
*/
|
|
262
|
+
export function serializeProps(instance: ElementInstance, props: Record<string, unknown>): WireProps {
|
|
263
|
+
const out: WireProps = {};
|
|
264
|
+
for (const [name, value] of Object.entries(props)) {
|
|
265
|
+
if (value === undefined || value === null) continue;
|
|
266
|
+
if (name === 'children' || name === 'key' || name === 'ref' || name === 'dangerouslySetInnerHTML') {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (typeof value === 'function') {
|
|
270
|
+
if (name.length > 2 && name.startsWith('on')) {
|
|
271
|
+
let slot = instance.listenerSlots[name];
|
|
272
|
+
if (slot === undefined) {
|
|
273
|
+
slot = nextHandlerId++;
|
|
274
|
+
instance.listenerSlots[name] = slot;
|
|
275
|
+
}
|
|
276
|
+
handlers.set(slot, {
|
|
277
|
+
fn: value as (payload: unknown) => void,
|
|
278
|
+
instance: instance.instance,
|
|
279
|
+
instanceId: instance.id,
|
|
280
|
+
});
|
|
281
|
+
out[name] = { __evt: slot };
|
|
282
|
+
}
|
|
283
|
+
// Non-event functions (render props, callbacks) can't cross the wire — dropped.
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
out[name] = value; // style objects, numbers, strings, booleans all clone fine
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* ── Small factories ────────────────────────────────────────────────────── */
|
|
292
|
+
|
|
293
|
+
export function newElement(
|
|
294
|
+
type: string,
|
|
295
|
+
props: Record<string, unknown>,
|
|
296
|
+
ns: string | undefined,
|
|
297
|
+
instance: string,
|
|
298
|
+
): ElementInstance {
|
|
299
|
+
const record: ElementInstance = {
|
|
300
|
+
kind: 'element',
|
|
301
|
+
id: allocId(),
|
|
302
|
+
type,
|
|
303
|
+
ns,
|
|
304
|
+
instance,
|
|
305
|
+
props: {},
|
|
306
|
+
listenerSlots: {},
|
|
307
|
+
};
|
|
308
|
+
record.props = serializeProps(record, props);
|
|
309
|
+
instances.set(record.id, record);
|
|
310
|
+
return record;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function newText(text: string, instance: string): TextInstance {
|
|
314
|
+
const record: TextInstance = { kind: 'text', id: allocId(), text, instance };
|
|
315
|
+
instances.set(record.id, record);
|
|
316
|
+
return record;
|
|
317
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ProxyNode,
|
|
3
|
+
ProxyText,
|
|
4
|
+
ProxyComment,
|
|
5
|
+
ProxyFragment,
|
|
6
|
+
} from './dom/node';
|
|
7
|
+
export type { ProxyEventHandler, WireListenerOpts } from './dom/node';
|
|
8
|
+
export {
|
|
9
|
+
ProxyElement,
|
|
10
|
+
} from './dom/element';
|
|
11
|
+
export type { AdjacentPosition } from './dom/element';
|
|
12
|
+
export type { ProxyClassList } from './dom/css';
|
|
13
|
+
export {
|
|
14
|
+
createProxyDocument,
|
|
15
|
+
docForInstance,
|
|
16
|
+
} from './dom/document';
|
|
17
|
+
export type {
|
|
18
|
+
ProxyDocument,
|
|
19
|
+
InternalDocument,
|
|
20
|
+
} from './dom/document';
|
|
21
|
+
export {
|
|
22
|
+
installInstanceDispatcher,
|
|
23
|
+
installDomShim,
|
|
24
|
+
} from './dom/window';
|
|
25
|
+
export type {
|
|
26
|
+
WindowShim,
|
|
27
|
+
WindowFacadeBundle,
|
|
28
|
+
} from './dom/window';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The React-runtime half of the island worker — `react`, `react-reconciler`
|
|
3
|
+
* and the host config live HERE so a worker bundle whose registry holds only
|
|
4
|
+
* non-React apps (Vue/Svelte/Solid/Angular/imperative) never pulls the
|
|
5
|
+
* reconciler in. `definePolyWorker` reaches this module only through
|
|
6
|
+
* `await import('./reactInstance')` when a mounted app resolves to a component
|
|
7
|
+
* function — first mount of a React instance pays one microtask's worth of
|
|
8
|
+
* module init, everything after is the same sync path.
|
|
9
|
+
*/
|
|
10
|
+
import { createElement, type ReactElement } from 'react';
|
|
11
|
+
import Reconciler from 'react-reconciler';
|
|
12
|
+
import { hostConfig } from './hostConfig';
|
|
13
|
+
import { ROOT_CONTAINER } from './instance';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A mounted React instance — carries its own reconciler + container and the
|
|
17
|
+
* `render`/`unmountTree` verbs, so the runtime module never needs the
|
|
18
|
+
* `react` import itself (element creation is the only place it was used).
|
|
19
|
+
*/
|
|
20
|
+
export interface ReactInstance {
|
|
21
|
+
/** The wire key — 'app' or 'app@instance'; op queues route by this. */
|
|
22
|
+
key: string;
|
|
23
|
+
/** The registry name — which component this instance renders. */
|
|
24
|
+
app: string;
|
|
25
|
+
/** Per-instance random id — the island's "worker pid" badge. */
|
|
26
|
+
pid: string;
|
|
27
|
+
imperative?: undefined;
|
|
28
|
+
rendered?: undefined;
|
|
29
|
+
reconciler: ReturnType<typeof Reconciler>;
|
|
30
|
+
container: unknown;
|
|
31
|
+
/** Mount/re-render the component into this instance's container. */
|
|
32
|
+
render(App: (props: Record<string, unknown>) => ReactElement, props: Record<string, unknown>): void;
|
|
33
|
+
/** Unmount the tree — emits the detach ops the driver replays. */
|
|
34
|
+
unmountTree(): void;
|
|
35
|
+
/** Passive effects + pending sync work — the `flush` task's React branch. */
|
|
36
|
+
flush(): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Legacy root (tag 0) — no concurrent features. Container creation is
|
|
41
|
+
* per-instance, not module-level, so a second mount() can't collide with the
|
|
42
|
+
* first instance's tree.
|
|
43
|
+
*/
|
|
44
|
+
export function createReactInstance(key: string, app: string, pid: string): ReactInstance {
|
|
45
|
+
const reconciler = Reconciler(hostConfig);
|
|
46
|
+
const container = reconciler.createContainer(
|
|
47
|
+
// Instance-stamped root container — createInstance reads `.instance` off it,
|
|
48
|
+
// binding every element to this instance even in out-of-task commits.
|
|
49
|
+
{ id: 0, instance: key } as typeof ROOT_CONTAINER,
|
|
50
|
+
0,
|
|
51
|
+
null,
|
|
52
|
+
false,
|
|
53
|
+
null,
|
|
54
|
+
'',
|
|
55
|
+
console.error, // onUncaughtError
|
|
56
|
+
console.error, // onCaughtError
|
|
57
|
+
console.error, // onRecoverableError
|
|
58
|
+
null, // onDefaultTransitionIndicator
|
|
59
|
+
);
|
|
60
|
+
return {
|
|
61
|
+
key,
|
|
62
|
+
app,
|
|
63
|
+
pid,
|
|
64
|
+
reconciler,
|
|
65
|
+
container,
|
|
66
|
+
render: (App, props) =>
|
|
67
|
+
reconciler.updateContainer(createElement(App, props), container, null, null),
|
|
68
|
+
unmountTree: () => reconciler.updateContainer(null, container, null, null),
|
|
69
|
+
flush: () => {
|
|
70
|
+
reconciler.flushPassiveEffects();
|
|
71
|
+
reconciler.flushSyncWork();
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A transclusion slot — renders a leaf element marked `data-atoll-slot`.
|
|
3
|
+
* The element's BOX belongs to this worker tree (layout/style ops apply as
|
|
4
|
+
* usual); its CONTENTS belong to the shell: when the create op lands on the
|
|
5
|
+
* main thread, the island's `slots[name]` callback gets the real element
|
|
6
|
+
* and can mount anything inside — a canvas, Monaco, a main-thread React
|
|
7
|
+
* root — all real DOM with real events and zero wire traffic.
|
|
8
|
+
*
|
|
9
|
+
* Must stay a LEAF in this tree: if worker-side children were appended here
|
|
10
|
+
* their ops would interleave with shell-owned content. There is also no
|
|
11
|
+
* measurement channel — the island knows the box it laid out, not the
|
|
12
|
+
* pixel size the shell put inside it.
|
|
13
|
+
*/
|
|
14
|
+
import type { ReactElement } from 'react';
|
|
15
|
+
|
|
16
|
+
export function Slot({ name, style }: { name: string; style?: Record<string, unknown> }): ReactElement {
|
|
17
|
+
return <div data-atoll-slot={name} style={style} />;
|
|
18
|
+
}
|