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