@atolljs/islands 0.1.0 → 0.1.1
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 +94 -285
- package/package.json +8 -5
- package/src/app.ts +7 -5
- package/src/metrics.ts +79 -0
- package/src/worker/defineWorkers.ts +140 -180
- package/src/worker/dom/document.ts +13 -0
- package/src/worker/index.ts +12 -11
- package/src/worker/instance.ts +104 -57
- package/src/worker/proxyDom.ts +2 -0
- package/src/react-reconciler.d.ts +0 -18
- package/src/worker/hostConfig.ts +0 -446
- package/src/worker/reactInstance.ts +0 -74
- package/src/worker/slot.tsx +0 -18
|
@@ -40,10 +40,13 @@
|
|
|
40
40
|
* flush = drain only). See worker/proxyDom.ts.
|
|
41
41
|
*
|
|
42
42
|
* RENDERED INSTANCES: `{ mount: (ctx) => handle | void }` is the same shape
|
|
43
|
-
* for
|
|
44
|
-
* proxy document and returns a handle whose `update(props)` takes
|
|
45
|
-
* updateProps (fine-grained patching instead of rebuild)
|
|
46
|
-
* `
|
|
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.
|
|
47
50
|
*
|
|
48
51
|
* Ops still ride back through the pool's ordinary postMessage channel — the
|
|
49
52
|
* sharedMemory contract here is only the doorbell (see memory.ts): a commit
|
|
@@ -52,8 +55,6 @@
|
|
|
52
55
|
* message-only mode.
|
|
53
56
|
*/
|
|
54
57
|
|
|
55
|
-
import type { ReactElement } from 'react';
|
|
56
|
-
import type { ReactInstance } from './reactInstance';
|
|
57
58
|
import { defineWorker } from '@atolljs/core';
|
|
58
59
|
import type { SharedMemory, WorkerDefinition } from '@atolljs/core';
|
|
59
60
|
import { islandAppNameOf } from '../app';
|
|
@@ -63,6 +64,8 @@ import {
|
|
|
63
64
|
createProxyDocument,
|
|
64
65
|
installInstanceDispatcher,
|
|
65
66
|
docForInstance,
|
|
67
|
+
peekInstanceDoc,
|
|
68
|
+
disposeInstanceDoc,
|
|
66
69
|
type InternalDocument,
|
|
67
70
|
type ProxyDocument,
|
|
68
71
|
} from './proxyDom';
|
|
@@ -72,7 +75,6 @@ import {
|
|
|
72
75
|
instances,
|
|
73
76
|
pushOp,
|
|
74
77
|
runInInstance,
|
|
75
|
-
setActiveInstance,
|
|
76
78
|
setDoorbellContract,
|
|
77
79
|
setInstanceSize,
|
|
78
80
|
takeOps,
|
|
@@ -81,13 +83,13 @@ import type { EventPayload, IslandWorkerMethods, Op } from '../ops';
|
|
|
81
83
|
|
|
82
84
|
/**
|
|
83
85
|
* Registry value shapes:
|
|
84
|
-
* -
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* - `{
|
|
88
|
-
*
|
|
86
|
+
* - `{ imperative: (doc, props) => void }` — no framework at all. mount()
|
|
87
|
+
* hands it a proxy DOM scoped to the instance; its mutations emit ops
|
|
88
|
+
* directly.
|
|
89
|
+
* - `{ mount: (ctx) => handle | void }` — a framework adapter's output
|
|
90
|
+
* (`reactIslandApp`, `vueIslandApp`, …). Bare component functions are NOT
|
|
91
|
+
* registry values — wrap them first (defineReactPolyWorker does it).
|
|
89
92
|
*/
|
|
90
|
-
export type ReactIslandApp = (props: any) => ReactElement;
|
|
91
93
|
export interface ImperativeIslandApp {
|
|
92
94
|
imperative: (doc: ProxyDocument, props: Record<string, unknown>) => void;
|
|
93
95
|
/**
|
|
@@ -117,6 +119,21 @@ export interface RenderedHandle {
|
|
|
117
119
|
* clear + re-mount on a fresh document.
|
|
118
120
|
*/
|
|
119
121
|
update?(props: Record<string, unknown>): void;
|
|
122
|
+
/**
|
|
123
|
+
* Run `fn` inside the renderer's synchronous commit lane (React:
|
|
124
|
+
* flushSyncFromReconciler + flushSyncWork) so its mutations emit ops
|
|
125
|
+
* inside the current task batch. Dispatch wraps event-handler invocation
|
|
126
|
+
* in this when the handle provides one; renderers without a sync lane
|
|
127
|
+
* simply omit it.
|
|
128
|
+
*/
|
|
129
|
+
sync?(fn: () => void): void;
|
|
130
|
+
/**
|
|
131
|
+
* Drain renderer work scheduled outside tasks — passive effects, async
|
|
132
|
+
* state updates (React: flushPassiveEffects + flushSyncWork). The worker's
|
|
133
|
+
* flush() method calls it inside the instance's scope, then drains the
|
|
134
|
+
* op queue.
|
|
135
|
+
*/
|
|
136
|
+
flush?(): void;
|
|
120
137
|
/**
|
|
121
138
|
* App teardown — runs inside the instance's scope BEFORE its proxy document
|
|
122
139
|
* is disposed (unmount, remount, updateProps rebuild). Same rules as
|
|
@@ -125,18 +142,18 @@ export interface RenderedHandle {
|
|
|
125
142
|
dispose?(): void;
|
|
126
143
|
}
|
|
127
144
|
/**
|
|
128
|
-
* A renderer-backed app: a
|
|
145
|
+
* A renderer-backed app: a framework renderer (React, Vue, Svelte,
|
|
129
146
|
* Solid, Angular) whose `mount` renders component output into the instance's
|
|
130
147
|
* proxy document — every proxy mutation already serializes to ops. Package
|
|
131
|
-
* adapters (e.g. @atolljs/vue-island/worker)
|
|
132
|
-
* this shape so it can sit beside
|
|
133
|
-
* `apps` registry.
|
|
148
|
+
* adapters (e.g. @atolljs/react-island/worker, @atolljs/vue-island/worker)
|
|
149
|
+
* wrap a component into this shape so it can sit beside imperative apps in
|
|
150
|
+
* the same `apps` registry.
|
|
134
151
|
*/
|
|
135
152
|
export interface RenderedIslandApp {
|
|
136
153
|
mount(ctx: RenderContext): RenderedHandle | void;
|
|
137
154
|
}
|
|
138
155
|
/** The apps an island can mount — keyed by the name the shell passes to mount(). */
|
|
139
|
-
export type IslandApp =
|
|
156
|
+
export type IslandApp = ImperativeIslandApp | RenderedIslandApp;
|
|
140
157
|
|
|
141
158
|
export interface PolyWorkerRegistry {
|
|
142
159
|
/** Name → island app registry. The shell's `mountIsland({ app: name })`
|
|
@@ -201,14 +218,18 @@ interface RenderedInstance extends InstanceBase {
|
|
|
201
218
|
app: RenderedIslandApp;
|
|
202
219
|
/** The mount-returned handle — drives fine-grained updates/teardown. */
|
|
203
220
|
handle: RenderedHandle | void;
|
|
204
|
-
/** The instance's proxy document — replaced on each rebuild. */
|
|
205
|
-
doc: ProxyDocument;
|
|
206
221
|
props: Record<string, unknown>;
|
|
207
222
|
};
|
|
223
|
+
// The instance's proxy document is NOT stored here: it lives in
|
|
224
|
+
// instanceDocs, materialized lazily by `ctx.doc` or `docForInstance`
|
|
225
|
+
// adoption (React mounts only need one when a library holds a ref or a
|
|
226
|
+
// portal target) — rendered instances without a doc preserve the
|
|
227
|
+
// ambient-document resolution semantics imperative instances rely on.
|
|
228
|
+
// Look it up with peekInstanceDoc; tear it down with disposeInstanceDoc.
|
|
208
229
|
}
|
|
209
230
|
|
|
210
231
|
/** A mounted instance — one per island worker in production. */
|
|
211
|
-
export type Instance =
|
|
232
|
+
export type Instance = ImperativeInstance | RenderedInstance;
|
|
212
233
|
|
|
213
234
|
const isImperativeInstance = (r: Instance): r is ImperativeInstance => r.imperative !== undefined;
|
|
214
235
|
const isRenderedInstance = (r: Instance): r is RenderedInstance => r.rendered !== undefined;
|
|
@@ -240,15 +261,6 @@ const resolveApp = (name: string): IslandApp | undefined =>
|
|
|
240
261
|
|
|
241
262
|
const newPid = (): string => `w-${Math.random().toString(36).slice(2, 8)}`;
|
|
242
263
|
|
|
243
|
-
/**
|
|
244
|
-
* react/react-reconciler arrive here — dynamically, so a worker bundle whose
|
|
245
|
-
* registry holds only Vue/Svelte/Solid/Angular/imperative apps never pays
|
|
246
|
-
* for the reconciler. Cached after the first React mount.
|
|
247
|
-
*/
|
|
248
|
-
let reactRuntime: Promise<typeof import('./reactInstance')> | undefined;
|
|
249
|
-
const loadReactRuntime = (): Promise<typeof import('./reactInstance')> =>
|
|
250
|
-
(reactRuntime ??= import('./reactInstance'));
|
|
251
|
-
|
|
252
264
|
/** 'controls' or 'data-table@7' → 'data-table' — registry name part of a instance key. */
|
|
253
265
|
const appNameOf = (instance: string): string => {
|
|
254
266
|
const at = instance.lastIndexOf('@');
|
|
@@ -274,10 +286,10 @@ function createIslandRuntime(
|
|
|
274
286
|
// installDomShim, which builds on this.
|
|
275
287
|
installInstanceDispatcher();
|
|
276
288
|
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
289
|
+
// Container creation is per-instance, not module-level, so a second mount()
|
|
290
|
+
// can't collide with the first instance's tree. Imperative mounts skip the
|
|
291
|
+
// renderer entirely — their "host environment" is the proxy DOM, and
|
|
292
|
+
// build() emits ops itself.
|
|
281
293
|
function createInstance(key: string, pid: string): Instance {
|
|
282
294
|
const app = resolveApp(appNameOf(key));
|
|
283
295
|
if (isRendered(app)) {
|
|
@@ -285,7 +297,7 @@ function createIslandRuntime(
|
|
|
285
297
|
key,
|
|
286
298
|
app: appNameOf(key),
|
|
287
299
|
pid,
|
|
288
|
-
rendered: { app, handle: undefined,
|
|
300
|
+
rendered: { app, handle: undefined, props: {} },
|
|
289
301
|
};
|
|
290
302
|
}
|
|
291
303
|
if (isImperative(app)) {
|
|
@@ -302,32 +314,12 @@ function createIslandRuntime(
|
|
|
302
314
|
};
|
|
303
315
|
}
|
|
304
316
|
throw new Error(
|
|
305
|
-
`createInstance: "${appNameOf(key)}"
|
|
317
|
+
`createInstance: "${appNameOf(key)}" is a bare component — registry values must be ` +
|
|
318
|
+
`{ imperative } or a framework adapter's RenderedIslandApp ` +
|
|
319
|
+
`(reactIslandApp/vueIslandApp/…, or the package's define*PolyWorker which wraps for you)`,
|
|
306
320
|
);
|
|
307
321
|
}
|
|
308
322
|
|
|
309
|
-
/**
|
|
310
|
-
* Commit synchronously around `fn` and return the ops it produced, scoped
|
|
311
|
-
* to `instance`'s queue.
|
|
312
|
-
*
|
|
313
|
-
* In 0.34, `updateContainer`/setState only *schedule* work — the actual
|
|
314
|
-
* render+commit happens when the root scheduler task runs (a macrotask via
|
|
315
|
-
* the `scheduler` package). `flushSyncFromReconciler` pins the update
|
|
316
|
-
* priority to the discrete/sync lane for the duration of `fn` and flushes
|
|
317
|
-
* pending sync work in its `finally`, so by the time it returns the
|
|
318
|
-
* mutation hooks have run and the instance's op queue is full.
|
|
319
|
-
*/
|
|
320
|
-
function syncCommit(instance: ReactInstance, fn: () => void): Op[] {
|
|
321
|
-
const prev = setActiveInstance(instance.key);
|
|
322
|
-
try {
|
|
323
|
-
instance.reconciler.flushSyncFromReconciler(fn);
|
|
324
|
-
instance.reconciler.flushSyncWork();
|
|
325
|
-
return takeOps(instance.key);
|
|
326
|
-
} finally {
|
|
327
|
-
setActiveInstance(prev);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
323
|
/**
|
|
332
324
|
* Rebuild an imperative instance — the simplest honest updateProps/remount
|
|
333
325
|
* semantics for code with no reconciler: dispose the old document (its
|
|
@@ -355,15 +347,27 @@ function createIslandRuntime(
|
|
|
355
347
|
* the proxy document, `clear`, then re-mount on a fresh document. Used
|
|
356
348
|
* for remounts and for updateProps when the handle exposes no `update`.
|
|
357
349
|
*/
|
|
350
|
+
/** The RenderContext handed to `app.mount` — `doc` materializes the
|
|
351
|
+
* instance's proxy document on first access so renderers that never
|
|
352
|
+
* touch it (React) register nothing in instanceDocs. */
|
|
353
|
+
function renderCtx(key: string, props: Record<string, unknown>): RenderContext {
|
|
354
|
+
return {
|
|
355
|
+
instance: key,
|
|
356
|
+
get doc() {
|
|
357
|
+
return docForInstance(key);
|
|
358
|
+
},
|
|
359
|
+
props,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
358
363
|
function rebuildRendered(instance: RenderedInstance, props: Record<string, unknown>): Op[] {
|
|
359
364
|
return runInInstance(instance.key, () => {
|
|
360
365
|
const r = instance.rendered;
|
|
361
366
|
r.handle?.dispose?.();
|
|
362
|
-
|
|
367
|
+
disposeInstanceDoc(instance.key);
|
|
363
368
|
pushOp(instance.key, { t: 'clear' });
|
|
364
|
-
r.doc = createProxyDocument(instance.key);
|
|
365
369
|
r.props = props;
|
|
366
|
-
r.handle = r.app.mount(
|
|
370
|
+
r.handle = r.app.mount(renderCtx(instance.key, props));
|
|
367
371
|
return takeOps(instance.key);
|
|
368
372
|
});
|
|
369
373
|
}
|
|
@@ -394,71 +398,40 @@ function createIslandRuntime(
|
|
|
394
398
|
);
|
|
395
399
|
}
|
|
396
400
|
|
|
397
|
-
|
|
398
|
-
//
|
|
399
|
-
//
|
|
401
|
+
const mounted = mounts.get(instance);
|
|
402
|
+
// Remount — same clear+rebuild semantics as updateProps; the instance
|
|
403
|
+
// (and pid) survives.
|
|
400
404
|
if (mounted !== undefined && isImperativeInstance(mounted)) {
|
|
401
405
|
return rebuildImperative(mounted, props);
|
|
402
406
|
}
|
|
403
|
-
if (mounted !== undefined
|
|
407
|
+
if (mounted !== undefined) {
|
|
404
408
|
return rebuildRendered(mounted, props);
|
|
405
409
|
}
|
|
406
410
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const rt = await loadReactRuntime();
|
|
412
|
-
let reactInstance: ReactInstance;
|
|
413
|
-
if (mounted !== undefined) {
|
|
414
|
-
// Remount — unmount the existing tree so React detaches its
|
|
415
|
-
// instances, then rebuild on a fresh container. The pid is kept.
|
|
416
|
-
const old = mounted as ReactInstance;
|
|
417
|
-
ops = syncCommit(old, () => old.unmountTree());
|
|
418
|
-
reactInstance = rt.createReactInstance(instance, appNameOf(instance), old.pid);
|
|
419
|
-
} else {
|
|
420
|
-
reactInstance = rt.createReactInstance(instance, appNameOf(instance), newPid());
|
|
421
|
-
}
|
|
422
|
-
mounts.set(instance, reactInstance);
|
|
423
|
-
return ops.concat(
|
|
424
|
-
syncCommit(reactInstance, () => reactInstance.render(App, props)),
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Imperative/rendered first mount. (A mounted React instance being
|
|
429
|
-
// replaced by a non-React app unmounts through syncCommit first.)
|
|
430
|
-
if (mounted !== undefined) {
|
|
431
|
-
const old = mounted as ReactInstance;
|
|
432
|
-
ops = syncCommit(old, () => old.unmountTree());
|
|
433
|
-
mounted = createInstance(instance, old.pid);
|
|
434
|
-
} else {
|
|
435
|
-
mounted = createInstance(instance, newPid());
|
|
436
|
-
}
|
|
437
|
-
mounts.set(instance, mounted);
|
|
411
|
+
// First mount. (A registry entry's kind can't change under a mounted
|
|
412
|
+
// instance key — the registry is fixed at module load.)
|
|
413
|
+
const created = createInstance(instance, newPid());
|
|
414
|
+
mounts.set(instance, created);
|
|
438
415
|
|
|
439
|
-
if (isImperativeInstance(
|
|
416
|
+
if (isImperativeInstance(created)) {
|
|
440
417
|
// First mount of an imperative instance — run build() in the instance's
|
|
441
418
|
// scope and drain the ops its proxy-DOM mutations emitted.
|
|
442
|
-
const imp =
|
|
419
|
+
const imp = created.imperative;
|
|
443
420
|
imp.props = props;
|
|
444
|
-
return
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}),
|
|
449
|
-
);
|
|
421
|
+
return runInInstance(instance, () => {
|
|
422
|
+
imp.build(imp.doc, props);
|
|
423
|
+
return takeOps(instance);
|
|
424
|
+
});
|
|
450
425
|
}
|
|
451
426
|
|
|
452
427
|
// First mount of a rendered instance — mount() in the instance's scope;
|
|
453
428
|
// the framework's proxy-DOM mutations emit the initial op batch.
|
|
454
|
-
const r =
|
|
429
|
+
const r = created.rendered;
|
|
455
430
|
r.props = props;
|
|
456
|
-
return
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
}),
|
|
461
|
-
);
|
|
431
|
+
return runInInstance(instance, () => {
|
|
432
|
+
r.handle = r.app.mount(renderCtx(instance, props));
|
|
433
|
+
return takeOps(instance);
|
|
434
|
+
});
|
|
462
435
|
},
|
|
463
436
|
|
|
464
437
|
/**
|
|
@@ -478,17 +451,13 @@ function createIslandRuntime(
|
|
|
478
451
|
if (isImperativeInstance(mounted)) return rebuildImperative(mounted, props);
|
|
479
452
|
// Rendered mounts prefer their own fine-grained update; absent a
|
|
480
453
|
// handle.update they fall back to the same rebuild.
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
const App = resolveApp(mounted.app) as (props: Record<string, unknown>) => ReactElement;
|
|
491
|
-
return syncCommit(mounted, () => mounted.render(App, props));
|
|
454
|
+
const r = mounted.rendered;
|
|
455
|
+
if (r.handle?.update === undefined) return rebuildRendered(mounted, props);
|
|
456
|
+
return runInInstance(instance, () => {
|
|
457
|
+
r.handle!.update!(props);
|
|
458
|
+
r.props = props;
|
|
459
|
+
return takeOps(instance);
|
|
460
|
+
});
|
|
492
461
|
},
|
|
493
462
|
|
|
494
463
|
/**
|
|
@@ -524,77 +493,69 @@ function createIslandRuntime(
|
|
|
524
493
|
: undefined);
|
|
525
494
|
}
|
|
526
495
|
}
|
|
527
|
-
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
return syncCommit(instance, () => {
|
|
537
|
-
entry.fn(payload);
|
|
496
|
+
return runInInstance(instance.key, () => {
|
|
497
|
+
// Task-ordered renderers (React) wrap the handler in their sync
|
|
498
|
+
// lane — the handle's `sync` hook — so handler-driven commits emit
|
|
499
|
+
// ops inside this dispatch batch. Renderers without one flush their
|
|
500
|
+
// work through the doorbell/flush path instead.
|
|
501
|
+
const sync = isRenderedInstance(instance) ? instance.rendered.handle?.sync : undefined;
|
|
502
|
+
if (sync !== undefined) sync(() => entry.fn(payload));
|
|
503
|
+
else entry.fn(payload);
|
|
504
|
+
return takeOps(instance.key);
|
|
538
505
|
});
|
|
539
506
|
},
|
|
540
507
|
|
|
541
508
|
/**
|
|
542
509
|
* The pushed-size channel: the driver measured the island's container
|
|
543
|
-
* and stores it for the instance (see
|
|
544
|
-
*
|
|
510
|
+
* and stores it for the instance (see instanceSizes). Mounts
|
|
511
|
+
* additionally fire their proxy document's onResize handlers —
|
|
545
512
|
* inside the instance's scope so their mutations emit ops, which ride
|
|
546
|
-
* back in this task's return batch.
|
|
547
|
-
* the stored size is still what a shim-installed document would read.
|
|
513
|
+
* back in this task's return batch.
|
|
548
514
|
*/
|
|
549
515
|
setSize(instance: string, width: number, height: number): Op[] {
|
|
550
516
|
setInstanceSize(instance, width, height);
|
|
551
517
|
const mounted = mounts.get(instance);
|
|
552
|
-
if (
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
return takeOps(instance);
|
|
518
|
+
if (mounted === undefined) return takeOps(instance);
|
|
519
|
+
const doc = isImperativeInstance(mounted)
|
|
520
|
+
? mounted.imperative.doc
|
|
521
|
+
: peekInstanceDoc(instance);
|
|
522
|
+
// Rendered mounts that never materialized their document (React)
|
|
523
|
+
// have no onResize listeners — the stored size still answers shim
|
|
524
|
+
// reads, so just drain.
|
|
525
|
+
if (doc === undefined) return takeOps(instance);
|
|
526
|
+
return runInInstance(mounted.key, () => {
|
|
527
|
+
(doc as InternalDocument)._notifySize(width, height);
|
|
528
|
+
return takeOps(instance);
|
|
529
|
+
});
|
|
565
530
|
},
|
|
566
531
|
|
|
567
532
|
/**
|
|
568
533
|
* Tear down ONE instance while the worker keeps serving its others —
|
|
569
|
-
* the multi-island-per-worker counterpart to process death:
|
|
570
|
-
* the
|
|
571
|
-
* drops the instance entry, and returns the
|
|
534
|
+
* the multi-island-per-worker counterpart to process death: disposes
|
|
535
|
+
* the renderer handle (or the imperative instance's proxy document),
|
|
536
|
+
* drops the instance entry, and returns the clear op. Stale
|
|
572
537
|
* handler dispatches then no-op via the mounts.get() guard.
|
|
573
538
|
*/
|
|
574
539
|
unmount(instance: string): Op[] {
|
|
575
540
|
const mounted = mounts.get(instance);
|
|
576
541
|
if (mounted === undefined) return [];
|
|
577
542
|
mounts.delete(instance);
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
543
|
+
return runInInstance(instance, () => {
|
|
544
|
+
if (isImperativeInstance(mounted)) {
|
|
545
|
+
// The app's dispose hook cancels deferred work (library timers,
|
|
546
|
+
// animation loops) BEFORE the doc dies — disposing the doc then
|
|
547
|
+
// unwinds its DOM shim and drops its handlers.
|
|
583
548
|
const imp = mounted.imperative;
|
|
584
549
|
imp.dispose?.(imp.doc);
|
|
585
550
|
imp.doc.dispose();
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
});
|
|
595
|
-
return [{ t: 'clear' }];
|
|
596
|
-
}
|
|
597
|
-
return syncCommit(mounted, () => mounted.unmountTree());
|
|
551
|
+
} else {
|
|
552
|
+
mounted.rendered.handle?.dispose?.();
|
|
553
|
+
disposeInstanceDoc(instance);
|
|
554
|
+
}
|
|
555
|
+
// Teardown ops die with the instance — the clear op covers them.
|
|
556
|
+
takeOps(instance);
|
|
557
|
+
return [{ t: 'clear' as const }];
|
|
558
|
+
});
|
|
598
559
|
},
|
|
599
560
|
|
|
600
561
|
/**
|
|
@@ -605,17 +566,16 @@ function createIslandRuntime(
|
|
|
605
566
|
flush(instance: string): Op[] {
|
|
606
567
|
const mounted = mounts.get(instance);
|
|
607
568
|
if (mounted === undefined) return [];
|
|
608
|
-
// Imperative
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
569
|
+
// Imperative mounts commit nothing outside tasks — ops pushed by
|
|
570
|
+
// timers/continuations just drain. Rendered mounts flush their
|
|
571
|
+
// renderer first (React: passive effects + scheduled sync work) so
|
|
572
|
+
// those commits drain in the same batch.
|
|
573
|
+
if (isImperativeInstance(mounted)) return takeOps(instance);
|
|
574
|
+
const r = mounted.rendered;
|
|
575
|
+
return runInInstance(mounted.key, () => {
|
|
576
|
+
r.handle?.flush?.();
|
|
615
577
|
return takeOps(instance);
|
|
616
|
-
}
|
|
617
|
-
setActiveInstance(prev);
|
|
618
|
-
}
|
|
578
|
+
});
|
|
619
579
|
},
|
|
620
580
|
|
|
621
581
|
/** The mounted instance's random id — the island's "worker pid" badge. */
|
|
@@ -569,6 +569,19 @@ export const createProxyDocument = (instance: string): ProxyDocument => {
|
|
|
569
569
|
export const docForInstance = (instance: string): InternalDocument =>
|
|
570
570
|
instanceDocs.get(instance) ?? (createProxyDocument(instance) as InternalDocument);
|
|
571
571
|
|
|
572
|
+
/** The instance's proxy document if one was materialized — never creates.
|
|
573
|
+
* Rendered instances may hold none (React's host config writes instance
|
|
574
|
+
* records, not proxy nodes, until a ref/portal adopts them). */
|
|
575
|
+
export const peekInstanceDoc = (instance: string): InternalDocument | undefined =>
|
|
576
|
+
instanceDocs.get(instance);
|
|
577
|
+
|
|
578
|
+
/** Dispose the instance's proxy document if one exists — covers docs
|
|
579
|
+
* materialized through `docForInstance`, not just `ctx.doc`, so unmount
|
|
580
|
+
* can't leave a stale entry for ambient-document resolution. */
|
|
581
|
+
export const disposeInstanceDoc = (instance: string): void => {
|
|
582
|
+
instanceDocs.get(instance)?.dispose();
|
|
583
|
+
};
|
|
584
|
+
|
|
572
585
|
/**
|
|
573
586
|
* The document `globalThis.document` should mean right now: the active
|
|
574
587
|
* instance's when a task holds one; the single registered document when only
|
package/src/worker/index.ts
CHANGED
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
* import { definePolyWorker } from '@atolljs/islands/worker';
|
|
6
6
|
* export const renderWorker = definePolyWorker({ apps: { ... } });
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* channel), `runInInstance` (instance scoping for worker-initiated work),
|
|
10
|
-
*
|
|
8
|
+
* Framework apps also use from here: `emit` (island→shell
|
|
9
|
+
* channel), `runInInstance` (instance scoping for worker-initiated work),
|
|
10
|
+
* `createProxyDocument`/`installDomShim` (imperative DOM), plus the
|
|
11
|
+
* renderer-facing instance factories (`newElement`/`newText`/`serializeProps`,
|
|
12
|
+
* `ROOT_CONTAINER`) the `*-island` worker adapters drive.
|
|
13
|
+
*
|
|
14
|
+
* Framework-neutral by construction: this entry imports no framework — React's
|
|
15
|
+
* reconciler adapter lives in `@atolljs/react-island/worker`.
|
|
11
16
|
*/
|
|
12
17
|
export { definePolyWorker, defineMonoWorker } from './defineWorkers';
|
|
13
18
|
export type {
|
|
@@ -15,7 +20,6 @@ export type {
|
|
|
15
20
|
Instance,
|
|
16
21
|
IslandApp,
|
|
17
22
|
PolyWorkerRegistry,
|
|
18
|
-
ReactIslandApp,
|
|
19
23
|
RenderContext,
|
|
20
24
|
RenderedHandle,
|
|
21
25
|
RenderedIslandApp,
|
|
@@ -23,10 +27,6 @@ export type {
|
|
|
23
27
|
|
|
24
28
|
export { islandApp, islandAppNameOf } from '../app';
|
|
25
29
|
|
|
26
|
-
// hostConfig is deliberately NOT exported — a static re-export would pull
|
|
27
|
-
// react + react-reconciler into every /worker bundle. The reconciler is
|
|
28
|
-
// reachable only through definePolyWorker's lazy import of reactInstance.
|
|
29
|
-
|
|
30
30
|
export {
|
|
31
31
|
allocId,
|
|
32
32
|
bumpOpsVersion,
|
|
@@ -37,17 +37,20 @@ export {
|
|
|
37
37
|
getLastTouchedInstance,
|
|
38
38
|
getInstanceSize,
|
|
39
39
|
instances,
|
|
40
|
+
newElement,
|
|
41
|
+
newText,
|
|
40
42
|
pushOp,
|
|
41
43
|
registerHandler,
|
|
42
44
|
ROOT_CONTAINER,
|
|
43
45
|
runInInstance,
|
|
46
|
+
serializeProps,
|
|
44
47
|
setActiveInstance,
|
|
45
48
|
setDoorbellContract,
|
|
46
49
|
setInstanceSize,
|
|
47
50
|
takeOps,
|
|
48
51
|
unregisterHandler,
|
|
49
52
|
} from './instance';
|
|
50
|
-
export type { ElementInstance, HostInstance, TextInstance } from './instance';
|
|
53
|
+
export type { ElementInstance, HostInstance, RootContainer, TextInstance } from './instance';
|
|
51
54
|
|
|
52
55
|
export {
|
|
53
56
|
createProxyDocument,
|
|
@@ -70,8 +73,6 @@ export type {
|
|
|
70
73
|
WireListenerOpts,
|
|
71
74
|
} from './proxyDom';
|
|
72
75
|
|
|
73
|
-
export { Slot } from './slot';
|
|
74
|
-
|
|
75
76
|
export type { EventPayload, IslandWorkerMethods, Op, WireProps } from '../ops';
|
|
76
77
|
export { isEventRef } from '../ops';
|
|
77
78
|
export { renderMemory } from '../memory';
|