@atolljs/islands 0.1.11 → 0.1.12
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/package.json +2 -2
- package/src/island.ts +59 -28
- package/src/worker/subIsland.ts +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atolljs/islands",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
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",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"htmlparser2": "^12.0.0"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
|
-
"@atolljs/core": "0.1.
|
|
22
|
+
"@atolljs/core": "0.1.12"
|
|
23
23
|
},
|
|
24
24
|
"repository": {
|
|
25
25
|
"type": "git",
|
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
|
/**
|
|
@@ -716,6 +736,10 @@ export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandl
|
|
|
716
736
|
* edges stay ordered after the moves they coalesce past. */
|
|
717
737
|
const COALESCE_EVENTS = new Set(['mousemove', 'pointermove', 'touchmove', 'drag', 'dragover']);
|
|
718
738
|
const MAX_IN_FLIGHT_DISPATCH = 4;
|
|
739
|
+
/* How long destroy() waits for the worker's unmount batch before force-
|
|
740
|
+
* tearing down. Generous enough for a busy worker draining real tasks;
|
|
741
|
+
* short enough that a wedged worker is visibly gone soon. */
|
|
742
|
+
const UNMOUNT_GRACE_MS = 1500;
|
|
719
743
|
const dispatchQueue: Array<{ handlerId: number; key: string | null; payload: EventPayload }> = [];
|
|
720
744
|
let inFlightDispatches = 0;
|
|
721
745
|
|
|
@@ -1322,32 +1346,39 @@ export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandl
|
|
|
1322
1346
|
measuredIds.clear();
|
|
1323
1347
|
const remaining = (clientMounts.get(client) ?? 1) - 1;
|
|
1324
1348
|
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
|
-
|
|
1349
|
+
/* One bounded teardown for both cases. The worker's unmount batch is
|
|
1350
|
+
* still awaited so the app's dispose hook cancels deferred work and
|
|
1351
|
+
* the `clear` op can fire slots[name](null) for slot anchors — but
|
|
1352
|
+
* the wait is capped by UNMOUNT_GRACE_MS. A wedged worker (a task
|
|
1353
|
+
* that never settles) must not pin the worker, this island's driver
|
|
1354
|
+
* state, or the pending-call closures forever. finish() releases the
|
|
1355
|
+
* maps eagerly so ops arriving late replay against empty maps (all
|
|
1356
|
+
* lookups miss → no-ops), and on the last mount it terminates the
|
|
1357
|
+
* worker unconditionally — as long as no remount registered while
|
|
1358
|
+
* teardown was in flight (app/client swap does destroy → mount
|
|
1359
|
+
* back-to-back; terminate only when the client is truly idle). */
|
|
1360
|
+
let finished = false;
|
|
1361
|
+
const finish = () => {
|
|
1362
|
+
if (finished) return;
|
|
1363
|
+
finished = true;
|
|
1364
|
+
nodes.clear();
|
|
1365
|
+
detached.clear();
|
|
1366
|
+
detachedIds.clear();
|
|
1367
|
+
prevProps.clear();
|
|
1368
|
+
nodeListeners.clear();
|
|
1369
|
+
cbTable.clear();
|
|
1370
|
+
slotNodes.clear();
|
|
1371
|
+
dispatchQueue.length = 0;
|
|
1372
|
+
// keepAlive clients stay resident — the instance is unmounted but
|
|
1373
|
+
// the thread idles so the next mount is a warm start.
|
|
1374
|
+
if (remaining <= 0 && !isKeepAliveClient(client) && (clientMounts.get(client) ?? 0) === 0)
|
|
1375
|
+
client.terminate();
|
|
1376
|
+
};
|
|
1377
|
+
void islandCall('unmount', client.unmount(instance))
|
|
1378
|
+
.then((ops) => applyOps(ops, 'unmount'))
|
|
1379
|
+
.catch(() => {})
|
|
1380
|
+
.finally(finish);
|
|
1381
|
+
setTimeout(finish, UNMOUNT_GRACE_MS);
|
|
1351
1382
|
},
|
|
1352
1383
|
};
|
|
1353
1384
|
// 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,10 @@ 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 clients release the instance but keep the thread warm;
|
|
654
|
+
// worker-owned and last-mount clients terminate outright.
|
|
655
|
+
if (!isKeepAliveClient(client) && (givenClient === undefined || remaining <= 0))
|
|
656
|
+
client.terminate();
|
|
653
657
|
else void islandCall('unmount', client.unmount(instance)).catch(() => {});
|
|
654
658
|
},
|
|
655
659
|
};
|