@atolljs/islands 0.1.7 → 0.1.9
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 +3 -2
- package/package.json +2 -2
- package/src/callbackProps.ts +4 -0
- package/src/devtoolsCommands.ts +258 -0
- package/src/devtoolsPreview.ts +46 -0
- package/src/island.ts +157 -33
- package/src/ops.ts +6 -0
- package/src/worker/defineWorkers.ts +18 -2
- package/src/worker/index.ts +17 -0
- package/src/worker/instance.ts +4 -0
- package/src/worker/subIsland.ts +619 -0
package/src/island.ts
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-island mounter — the main-thread half of one React tree in one worker.
|
|
3
3
|
*
|
|
4
|
-
* `mountIsland` mounts one island scoped so several can coexist on one page
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* `mountIsland` mounts one island scoped so several can coexist on one page.
|
|
5
|
+
* A `worker:` mount builds its own client (one dedicated worker, own
|
|
6
|
+
* doorbell buffer via `makeDoorbell()`), i.e. spawns a worker; a `client:`
|
|
7
|
+
* mount adds an instance to that client's existing worker instead. Either
|
|
8
|
+
* way each island gets its own nodes/props/listeners maps (op ids are only
|
|
9
|
+
* unique within a worker — sharing maps between islands would corrupt them),
|
|
10
|
+
* and its own `onEvent` sink for `emit` ops.
|
|
9
11
|
*
|
|
10
12
|
* There is no React on this thread — the driver below just replays ops.
|
|
11
13
|
*
|
|
12
|
-
*
|
|
13
|
-
* worker's memory. A
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* One worker per island client is REQUIRED — the reconciled tree lives in
|
|
15
|
+
* one worker's memory. A pool can't serve islands: task routing is
|
|
16
|
+
* least-busy, so a second worker would receive dispatches for a tree it
|
|
17
|
+
* doesn't hold. Island clients therefore run on `workers: 1`, a dedicated
|
|
18
|
+
* worker with no pool.
|
|
16
19
|
*/
|
|
17
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
connectWorker,
|
|
22
|
+
devtoolsEnabled,
|
|
23
|
+
emitDevtools,
|
|
24
|
+
estimateCloneBytes,
|
|
25
|
+
observe,
|
|
26
|
+
previewValue,
|
|
27
|
+
} from '@atolljs/core';
|
|
18
28
|
import type {
|
|
19
29
|
ConnectWorkerConfig,
|
|
20
30
|
SharedSpec,
|
|
@@ -31,27 +41,31 @@ import {
|
|
|
31
41
|
} from './ops';
|
|
32
42
|
|
|
33
43
|
import { marshalCallbackProps, CALLBACK_EVENT } from './callbackProps';
|
|
44
|
+
import { trackIslandForDevtools } from './devtoolsCommands';
|
|
45
|
+
import { propsPreview } from './devtoolsPreview';
|
|
46
|
+
import type { ProxyElement } from './worker/dom/element';
|
|
34
47
|
|
|
35
48
|
export type Mode = 'push' | 'poll';
|
|
36
49
|
|
|
37
50
|
/** The worker definition every `definePolyWorker` call produces. */
|
|
38
51
|
export type IslandWorkerDefinition = WorkerDefinition<DoorbellSpec, IslandWorkerMethods>;
|
|
39
52
|
|
|
40
|
-
/** One island client: one
|
|
53
|
+
/** One island client: one dedicated worker, one doorbell buffer. */
|
|
41
54
|
export type IslandClient = WorkerClient<IslandWorkerDefinition, DoorbellSpec>;
|
|
42
55
|
|
|
43
56
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* `poolSize`/`worker`/`sharedMemory` (all island-internal) and the
|
|
47
|
-
* below pins `
|
|
57
|
+
* An island client is ONE worker holding its islands' trees (one per mount
|
|
58
|
+
* on it), so pooling is disabled by construction — the type omits
|
|
59
|
+
* `workers`/`poolSize`/`worker`/`sharedMemory` (all island-internal) and the
|
|
60
|
+
* literal below pins `workers: 1` after the spread, so a pool can't sneak
|
|
48
61
|
* through a cast either. `worker` is supplied per call — the package can't
|
|
49
62
|
* know where the consumer's worker entry lives — everything else
|
|
50
|
-
* (
|
|
63
|
+
* (taskTimeout, respawn, lazy…) still passes through. The pool-only
|
|
64
|
+
* `concurrency`/`maxQueue` are accepted but have no effect.
|
|
51
65
|
*/
|
|
52
66
|
export type IslandWorkerOptions = Omit<
|
|
53
67
|
ConnectWorkerConfig<SharedSpec>,
|
|
54
|
-
'sharedMemory' | 'worker' | 'poolSize'
|
|
68
|
+
'sharedMemory' | 'worker' | 'workers' | 'poolSize'
|
|
55
69
|
>;
|
|
56
70
|
|
|
57
71
|
export interface ConnectIslandWorkerConfig extends IslandWorkerOptions {
|
|
@@ -64,13 +78,13 @@ export interface ConnectIslandWorkerConfig extends IslandWorkerOptions {
|
|
|
64
78
|
/**
|
|
65
79
|
* Default true — the client carries a SharedArrayBuffer doorbell for
|
|
66
80
|
* push-mode flush (requires COOP/COEP cross-origin isolation). Pass
|
|
67
|
-
* `false` for a message-only
|
|
81
|
+
* `false` for a message-only client: `setMode('push')` falls back to
|
|
68
82
|
* polling and the page needs no special headers.
|
|
69
83
|
*/
|
|
70
84
|
doorbell?: boolean;
|
|
71
85
|
}
|
|
72
86
|
|
|
73
|
-
/** One island client: one
|
|
87
|
+
/** One island client: one dedicated worker, one doorbell buffer. */
|
|
74
88
|
export const connectIslandWorker = <W extends IslandWorkerDefinition = IslandWorkerDefinition>({
|
|
75
89
|
worker,
|
|
76
90
|
doorbell,
|
|
@@ -80,12 +94,17 @@ export const connectIslandWorker = <W extends IslandWorkerDefinition = IslandWor
|
|
|
80
94
|
...options,
|
|
81
95
|
sharedMemory: doorbell === false ? undefined : makeDoorbell(),
|
|
82
96
|
worker,
|
|
83
|
-
|
|
97
|
+
workers: 1,
|
|
84
98
|
});
|
|
85
99
|
|
|
86
100
|
interface MountIslandBaseOptions {
|
|
87
|
-
/**
|
|
88
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Container element the island's ops are applied into — a real element on
|
|
103
|
+
* the shell side, or a `ProxyElement` inside a worker-rendered island
|
|
104
|
+
* (nested mount: the sub-island's ops replay into the parent instance's
|
|
105
|
+
* shadow tree and tunnel upward through its op stream). Required.
|
|
106
|
+
*/
|
|
107
|
+
el: HTMLElement | ProxyElement;
|
|
89
108
|
/**
|
|
90
109
|
* Registry app name — a key of the `apps` map passed to
|
|
91
110
|
* `definePolyWorker`. Optional against a instance worker
|
|
@@ -103,6 +122,10 @@ interface MountIslandBaseOptions {
|
|
|
103
122
|
* events, zero wire). Called again with `null` when the worker removes or
|
|
104
123
|
* renames the element, so the shell can tear down. The element itself
|
|
105
124
|
* stays worker-owned — its box/layout props apply normally.
|
|
125
|
+
*
|
|
126
|
+
* Nested mounts (a ProxyElement `el`) receive the anchor's ProxyElement
|
|
127
|
+
* instead, and only claim names present in this object (`name in slots`);
|
|
128
|
+
* unclaimed slot names bubble up to the outer island's `slots`.
|
|
106
129
|
*/
|
|
107
130
|
slots?: Record<string, (el: HTMLElement | null) => void>;
|
|
108
131
|
/** Fired after each applied op batch — the shell uses it for stats. */
|
|
@@ -123,9 +146,10 @@ interface MountIslandBaseOptions {
|
|
|
123
146
|
mode?: Mode;
|
|
124
147
|
/**
|
|
125
148
|
* Optional renderer/framework tag — pure observability metadata forwarded
|
|
126
|
-
* to devtools on `island:mount` ('react', 'vue', 'svelte', …).
|
|
127
|
-
*
|
|
128
|
-
*
|
|
149
|
+
* to devtools on `island:mount` ('react', 'vue', 'svelte', …). When
|
|
150
|
+
* omitted (and devtools is on), the driver asks the worker: package
|
|
151
|
+
* adapters report their renderer, imperative apps report none and the
|
|
152
|
+
* map draws them as plain workers. An explicit tag always wins.
|
|
129
153
|
*/
|
|
130
154
|
framework?: string;
|
|
131
155
|
/**
|
|
@@ -155,6 +179,11 @@ export interface IslandHandle {
|
|
|
155
179
|
readonly app: string;
|
|
156
180
|
/** The worker-side instance's random id — proof each island is a distinct worker. */
|
|
157
181
|
readonly pid: string;
|
|
182
|
+
/**
|
|
183
|
+
* The wire instance key — `app@N` for a top-level island, `parent~app@N`
|
|
184
|
+
* when mounted into a proxy element inside a worker island.
|
|
185
|
+
*/
|
|
186
|
+
readonly instance: string;
|
|
158
187
|
readonly mode: Mode;
|
|
159
188
|
opsApplied: number;
|
|
160
189
|
flushCalls: number;
|
|
@@ -247,6 +276,37 @@ function buildEventPayload(
|
|
|
247
276
|
};
|
|
248
277
|
}
|
|
249
278
|
|
|
279
|
+
/**
|
|
280
|
+
* User Timing entry for one main-thread op replay (devtools only). The
|
|
281
|
+
* `detail.devtools` track-entry extension puts it on an 'atoll › islands'
|
|
282
|
+
* track in Chrome's Performance panel; other engines keep a plain measure.
|
|
283
|
+
* Same shape as core's task measures (`src/userTiming.ts`), including the
|
|
284
|
+
* immediate clear: per-batch entries would otherwise grow the buffer.
|
|
285
|
+
*/
|
|
286
|
+
const measureReplay = (instance: string, via: string, count: number, start: number, end: number): void => {
|
|
287
|
+
if (typeof performance?.measure !== 'function') return;
|
|
288
|
+
const name = `atoll replay ${instance}`;
|
|
289
|
+
try {
|
|
290
|
+
performance.measure(name, {
|
|
291
|
+
start,
|
|
292
|
+
end,
|
|
293
|
+
detail: {
|
|
294
|
+
devtools: {
|
|
295
|
+
dataType: 'track-entry',
|
|
296
|
+
trackGroup: 'atoll',
|
|
297
|
+
track: 'islands',
|
|
298
|
+
color: 'primary',
|
|
299
|
+
tooltipText: `${instance} · ${via} · ${count} ops`,
|
|
300
|
+
properties: [['via', via], ['ops', count]],
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
performance.clearMeasures?.(name);
|
|
305
|
+
} catch {
|
|
306
|
+
/* no User Timing L3 — skip */
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
250
310
|
/** Per-island instance counter — see the instance key note in mountIsland. */
|
|
251
311
|
let islandSeq = 0;
|
|
252
312
|
|
|
@@ -296,7 +356,7 @@ const uncloneablePath = (value: unknown, path: string): string => {
|
|
|
296
356
|
return path;
|
|
297
357
|
};
|
|
298
358
|
|
|
299
|
-
const assertCloneableProps = (props: Record<string, unknown>, context: string): void => {
|
|
359
|
+
export const assertCloneableProps = (props: Record<string, unknown>, context: string): void => {
|
|
300
360
|
if (canClone(props)) return;
|
|
301
361
|
const path = uncloneablePath(props, '');
|
|
302
362
|
throw new Error(
|
|
@@ -327,8 +387,10 @@ const UNITLESS_CSS = new Set([
|
|
|
327
387
|
]);
|
|
328
388
|
|
|
329
389
|
/** Coerce a wire style value to a CSS string — px-suffixes bare numbers
|
|
330
|
-
* on non-unitless keys (custom properties pass verbatim).
|
|
331
|
-
|
|
390
|
+
* on non-unitless keys (custom properties pass verbatim). Exported for the
|
|
391
|
+
* worker-side nested driver (worker/subIsland.ts), which applies the same
|
|
392
|
+
* unit semantics when replaying style values into proxy elements. */
|
|
393
|
+
export const cssStyleValue = (key: string, value: unknown): string => {
|
|
332
394
|
if (
|
|
333
395
|
typeof value === 'number' &&
|
|
334
396
|
value !== 0 &&
|
|
@@ -340,12 +402,48 @@ const cssStyleValue = (key: string, value: unknown): string => {
|
|
|
340
402
|
return String(value);
|
|
341
403
|
};
|
|
342
404
|
|
|
405
|
+
/**
|
|
406
|
+
* The worker-side mounter — registered by `@atolljs/islands/worker`
|
|
407
|
+
* (`worker/subIsland.ts`) so `mountIsland` serves BOTH mount targets with
|
|
408
|
+
* one signature: a proxy `el` (inside a worker-rendered island) takes the
|
|
409
|
+
* nested path, which replays the sub-worker's ops into the parent
|
|
410
|
+
* instance's shadow tree. Kept behind a registration slot rather than an
|
|
411
|
+
* import so the main-thread bundle never loads the proxy DOM.
|
|
412
|
+
*/
|
|
413
|
+
let subMounter: ((opts: MountIslandOptions) => Promise<IslandHandle>) | null = null;
|
|
414
|
+
export const registerSubIslandMounter = (
|
|
415
|
+
fn: (opts: MountIslandOptions) => Promise<IslandHandle>,
|
|
416
|
+
): void => {
|
|
417
|
+
subMounter = fn;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Structural discriminant — a proxy element's `doc.instance` is the parent
|
|
422
|
+
* instance key, a real element has no `doc` property at all. Type beats
|
|
423
|
+
* environment sniffing: in-process harnesses run worker and shell in one
|
|
424
|
+
* module graph where globals lie, but the mount target never does.
|
|
425
|
+
*/
|
|
426
|
+
const isProxyMountTarget = (el: unknown): el is ProxyElement =>
|
|
427
|
+
typeof (el as { doc?: { instance?: unknown } } | null)?.doc?.instance === 'string';
|
|
428
|
+
|
|
343
429
|
export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandle> {
|
|
344
430
|
const {
|
|
345
|
-
el, onEvent, onActivity, onOps, slots, mode: initialMode, mountTimeout,
|
|
431
|
+
el: target, onEvent, onActivity, onOps, slots, mode: initialMode, mountTimeout,
|
|
346
432
|
client: givenClient, worker, app: givenApp, props: givenProps, framework,
|
|
347
433
|
...workerConfig
|
|
348
434
|
} = opts;
|
|
435
|
+
if (isProxyMountTarget(target)) {
|
|
436
|
+
if (subMounter === null) {
|
|
437
|
+
throw new Error(
|
|
438
|
+
'mountIsland: a proxy-element mount target means a nested island — ' +
|
|
439
|
+
'the worker entry (`@atolljs/islands/worker`) must be loaded to register the sub-mounter.',
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
return subMounter(opts);
|
|
443
|
+
}
|
|
444
|
+
// Real DOM from here on (the proxy path delegated above) — the declared
|
|
445
|
+
// type keeps closures honest; the guard did the proving.
|
|
446
|
+
const el = target as HTMLElement;
|
|
349
447
|
// Shell→worker callables: callbackProp() markers marshal to {__cb:id}
|
|
350
448
|
// wire handles; the island's table keeps the real functions and the
|
|
351
449
|
// driver's emit-case routes worker-side invocations back here.
|
|
@@ -356,6 +454,8 @@ export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandl
|
|
|
356
454
|
return cbSeq;
|
|
357
455
|
};
|
|
358
456
|
const props = marshalCallbackProps(givenProps ?? {}, registerCb) as Record<string, unknown>;
|
|
457
|
+
/** The props as the caller last gave them — devtools previews/commands read these. */
|
|
458
|
+
let currentProps: Record<string, unknown> = givenProps ?? {};
|
|
359
459
|
if (givenClient === undefined && worker === undefined) {
|
|
360
460
|
throw new Error(
|
|
361
461
|
'mountIsland: pass `worker` (a `() => new Worker(...)`/URL entry — this call builds the ' +
|
|
@@ -365,6 +465,7 @@ export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandl
|
|
|
365
465
|
const client: IslandClient =
|
|
366
466
|
givenClient ??
|
|
367
467
|
connectIslandWorker({
|
|
468
|
+
name: givenApp,
|
|
368
469
|
...workerConfig,
|
|
369
470
|
worker,
|
|
370
471
|
// Poll-mode shorthand islands don't need SharedArrayBuffer at all —
|
|
@@ -777,7 +878,9 @@ export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandl
|
|
|
777
878
|
break;
|
|
778
879
|
}
|
|
779
880
|
// Not a DOM mutation — the island→shell channel.
|
|
780
|
-
|
|
881
|
+
if (devtoolsEnabled()) {
|
|
882
|
+
emitDevtools({ type: 'island:event', instance, name: op.name, payload: previewValue(op.payload, 1024) });
|
|
883
|
+
}
|
|
781
884
|
onEvent?.(op.name, op.payload);
|
|
782
885
|
break;
|
|
783
886
|
}
|
|
@@ -789,8 +892,15 @@ export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandl
|
|
|
789
892
|
onActivity?.();
|
|
790
893
|
const t0 = performance.now();
|
|
791
894
|
for (const op of ops) applyOp(op);
|
|
792
|
-
const
|
|
793
|
-
|
|
895
|
+
const t1 = performance.now();
|
|
896
|
+
const replayMs = t1 - t0;
|
|
897
|
+
if (devtoolsEnabled()) {
|
|
898
|
+
emitDevtools({
|
|
899
|
+
type: 'island:ops', instance, via, count: ops.length, replayMs,
|
|
900
|
+
bytes: estimateCloneBytes(ops),
|
|
901
|
+
});
|
|
902
|
+
measureReplay(instance, via, ops.length, t0, t1);
|
|
903
|
+
}
|
|
794
904
|
onOps?.(ops, replayMs);
|
|
795
905
|
}
|
|
796
906
|
|
|
@@ -879,7 +989,12 @@ export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandl
|
|
|
879
989
|
try {
|
|
880
990
|
applyOps(await raced(islandCall('mount', client.mount(instance, props))), 'mount');
|
|
881
991
|
pid = await raced(islandCall('whoami', client.whoami(instance)));
|
|
882
|
-
|
|
992
|
+
if (devtoolsEnabled()) {
|
|
993
|
+
// an explicit tag wins; otherwise ask the worker which renderer the app uses
|
|
994
|
+
const fw = framework ?? (await client.renderer(instance).catch(() => null)) ?? undefined;
|
|
995
|
+
emitDevtools({ type: 'island:mount', instance, app, pid, poolId: client.pool?.poolId, framework: fw });
|
|
996
|
+
emitDevtools({ type: 'island:props', instance, props: propsPreview(currentProps) });
|
|
997
|
+
}
|
|
883
998
|
} catch (err) {
|
|
884
999
|
// The instance may exist worker-side with ops we never applied — release it.
|
|
885
1000
|
// An island-owned client also terminates (stops a crash→respawn loop on
|
|
@@ -941,6 +1056,7 @@ export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandl
|
|
|
941
1056
|
const handle: IslandHandle = {
|
|
942
1057
|
app,
|
|
943
1058
|
pid,
|
|
1059
|
+
instance,
|
|
944
1060
|
get mode() {
|
|
945
1061
|
return mode;
|
|
946
1062
|
},
|
|
@@ -955,12 +1071,16 @@ export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandl
|
|
|
955
1071
|
const marshalled = marshalCallbackProps(next, registerCb) as Record<string, unknown>;
|
|
956
1072
|
assertCloneableProps(marshalled, `updateProps(${app})`);
|
|
957
1073
|
applyOps(await islandCall('updateProps', client.updateProps(instance, marshalled)), 'updateProps');
|
|
1074
|
+
currentProps = next;
|
|
1075
|
+
if (devtoolsEnabled()) emitDevtools({ type: 'island:props', instance, props: propsPreview(next) });
|
|
958
1076
|
},
|
|
959
1077
|
flush: async () => {
|
|
960
1078
|
applyOps(await islandCall('flush', client.flush(instance)), 'flush');
|
|
961
1079
|
},
|
|
962
1080
|
destroy: () => {
|
|
963
1081
|
destroyed = true;
|
|
1082
|
+
untrackDevtools?.();
|
|
1083
|
+
untrackDevtools = null;
|
|
964
1084
|
emitDevtools({ type: 'island:unmount', instance });
|
|
965
1085
|
unsubscribe?.();
|
|
966
1086
|
resizeObserver?.disconnect();
|
|
@@ -992,5 +1112,9 @@ export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandl
|
|
|
992
1112
|
}
|
|
993
1113
|
},
|
|
994
1114
|
};
|
|
1115
|
+
// Dashboard commands (`island.*`) — registered only while devtools is on.
|
|
1116
|
+
let untrackDevtools: (() => void) | null = devtoolsEnabled()
|
|
1117
|
+
? trackIslandForDevtools({ handle, el, props: () => currentProps })
|
|
1118
|
+
: null;
|
|
995
1119
|
return handle;
|
|
996
1120
|
}
|
package/src/ops.ts
CHANGED
|
@@ -212,4 +212,10 @@ export type IslandWorkerMethods = {
|
|
|
212
212
|
unmount(instance: string): Op[];
|
|
213
213
|
/** The mounted instance's random id — the island's "worker pid" badge. */
|
|
214
214
|
whoami(instance: string): string;
|
|
215
|
+
/**
|
|
216
|
+
* The instance's renderer tag ('react', 'vue', …) from its adapter; null
|
|
217
|
+
* for imperative apps. Read by the driver only for devtools, and only
|
|
218
|
+
* when the mount declared no `framework`.
|
|
219
|
+
*/
|
|
220
|
+
renderer(instance: string): string | null;
|
|
215
221
|
};
|
|
@@ -153,6 +153,13 @@ export interface RenderedHandle {
|
|
|
153
153
|
*/
|
|
154
154
|
export interface RenderedIslandApp {
|
|
155
155
|
mount(ctx: RenderContext): RenderedHandle | void;
|
|
156
|
+
/**
|
|
157
|
+
* Renderer tag ('react', 'vue', 'svelte', 'solid', 'angular'): set by the
|
|
158
|
+
* package adapters, reported by the `renderer` task so devtools can mark
|
|
159
|
+
* an island whose mount declared no `framework`. Hand-written adapters
|
|
160
|
+
* may set it; imperative apps have none.
|
|
161
|
+
*/
|
|
162
|
+
renderer?: string;
|
|
156
163
|
}
|
|
157
164
|
/** The apps an island can mount — keyed by the name the shell passes to mount(). */
|
|
158
165
|
export type IslandApp = ImperativeIslandApp | RenderedIslandApp;
|
|
@@ -266,10 +273,13 @@ const resolveApp = (name: string): IslandApp | undefined =>
|
|
|
266
273
|
|
|
267
274
|
const newPid = (): string => `w-${Math.random().toString(36).slice(2, 8)}`;
|
|
268
275
|
|
|
269
|
-
/** 'controls' or 'data-table@7' → 'data-table' — registry name part of a instance key.
|
|
276
|
+
/** 'controls' or 'data-table@7' → 'data-table' — registry name part of a instance key.
|
|
277
|
+
* Nested keys ('host@1~counter@1', one '~' per level) resolve the last segment. */
|
|
270
278
|
const appNameOf = (instance: string): string => {
|
|
271
279
|
const at = instance.lastIndexOf('@');
|
|
272
|
-
|
|
280
|
+
const name = at === -1 ? instance : instance.slice(0, at);
|
|
281
|
+
const tilde = name.lastIndexOf('~');
|
|
282
|
+
return tilde === -1 ? name : name.slice(tilde + 1);
|
|
273
283
|
};
|
|
274
284
|
|
|
275
285
|
/**
|
|
@@ -617,6 +627,12 @@ function createIslandRuntime(
|
|
|
617
627
|
whoami(instance: string): string {
|
|
618
628
|
return mounts.get(instance)?.pid ?? 'unmounted';
|
|
619
629
|
},
|
|
630
|
+
|
|
631
|
+
/** The instance's renderer tag (an adapter's `renderer`); null for imperative or unknown. */
|
|
632
|
+
renderer(instance: string): string | null {
|
|
633
|
+
const mounted = mounts.get(instance);
|
|
634
|
+
return (mounted && isRenderedInstance(mounted) && mounted.rendered.app.renderer) || null;
|
|
635
|
+
},
|
|
620
636
|
},
|
|
621
637
|
});
|
|
622
638
|
}
|
package/src/worker/index.ts
CHANGED
|
@@ -47,6 +47,7 @@ export {
|
|
|
47
47
|
instances,
|
|
48
48
|
newElement,
|
|
49
49
|
newText,
|
|
50
|
+
proxyInstanceStats,
|
|
50
51
|
pushOp,
|
|
51
52
|
registerHandler,
|
|
52
53
|
ROOT_CONTAINER,
|
|
@@ -85,3 +86,19 @@ export type { EventPayload, IslandWorkerMethods, Op, WireProps } from '../ops';
|
|
|
85
86
|
export { isEventRef } from '../ops';
|
|
86
87
|
export { renderMemory } from '../memory';
|
|
87
88
|
export type { DoorbellSpec } from '../memory';
|
|
89
|
+
|
|
90
|
+
export { mountSubIsland } from './subIsland';
|
|
91
|
+
export type { MountSubIslandOptions, SubIslandHandle } from './subIsland';
|
|
92
|
+
|
|
93
|
+
// Nested-island clients: `connectIslandWorker` is thread-agnostic — a worker
|
|
94
|
+
// shell calls it to spawn a sub-worker. `mountIsland` is the ONE surface:
|
|
95
|
+
// a proxy `el` (inside a worker-rendered tree) delegates to the registered
|
|
96
|
+
// sub-mounter in ./subIsland; a real `el` drives the main-thread applier.
|
|
97
|
+
export { connectIslandWorker, mountIsland } from '../island';
|
|
98
|
+
export type {
|
|
99
|
+
ConnectIslandWorkerConfig,
|
|
100
|
+
IslandClient,
|
|
101
|
+
IslandHandle,
|
|
102
|
+
Mode,
|
|
103
|
+
MountIslandOptions,
|
|
104
|
+
} from '../island';
|
package/src/worker/instance.ts
CHANGED
|
@@ -237,10 +237,14 @@ export const proxyInstanceStats = (): {
|
|
|
237
237
|
liveNodes: number;
|
|
238
238
|
handlers: number;
|
|
239
239
|
queuedOps: number;
|
|
240
|
+
queuedByInstance: Record<string, number>;
|
|
240
241
|
} => ({
|
|
241
242
|
liveNodes: instances.size,
|
|
242
243
|
handlers: handlers.size,
|
|
243
244
|
queuedOps: [...opsByInstance.values()].reduce((a, q) => a + q.length, 0),
|
|
245
|
+
queuedByInstance: Object.fromEntries(
|
|
246
|
+
[...opsByInstance.entries()].map(([k, q]) => [k, q.length]),
|
|
247
|
+
),
|
|
244
248
|
});
|
|
245
249
|
|
|
246
250
|
export const getHandler = (id: number): HandlerEntry | undefined => handlers.get(id);
|