@atolljs/islands 0.1.0 → 0.1.2
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/island.ts +64 -9
- 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
package/src/worker/instance.ts
CHANGED
|
@@ -14,8 +14,25 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { renderMemory } from '../memory';
|
|
17
|
+
import { enterProxyFrame, exitProxyFrame, proxyMetrics } from '../metrics';
|
|
17
18
|
import type { Op, WireProps } from '../ops';
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Run `body` as one instrumented proxy-engine frame while metrics are
|
|
22
|
+
* enabled — a direct call otherwise (one predictable branch per chokepoint).
|
|
23
|
+
* `enter`/`exit` share a depth guard so nested frames (a dom/* method
|
|
24
|
+
* calling pushOp, serializeProps inside newElement) never double-count.
|
|
25
|
+
*/
|
|
26
|
+
const timed = <T>(body: () => T): T => {
|
|
27
|
+
if (!proxyMetrics.enabled) return body();
|
|
28
|
+
const t0 = enterProxyFrame();
|
|
29
|
+
try {
|
|
30
|
+
return body();
|
|
31
|
+
} finally {
|
|
32
|
+
exitProxyFrame(t0);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
19
36
|
/* ── Host instances ─────────────────────────────────────────────────────── */
|
|
20
37
|
|
|
21
38
|
export interface ElementInstance {
|
|
@@ -144,12 +161,14 @@ export const runInInstance = <T>(instance: string, fn: () => T): T => {
|
|
|
144
161
|
|
|
145
162
|
/** Queue an op onto a instance's queue — instance-bound ops pass their
|
|
146
163
|
* instance's instance, instance-less ops (`clear`, `emit`) the active one. */
|
|
147
|
-
export const pushOp = (instance: string, op: Op): void =>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
164
|
+
export const pushOp = (instance: string, op: Op): void =>
|
|
165
|
+
timed(() => {
|
|
166
|
+
if (instance !== '') touchedInstance = instance;
|
|
167
|
+
let queue = opsByInstance.get(instance);
|
|
168
|
+
if (!queue) opsByInstance.set(instance, (queue = []));
|
|
169
|
+
queue.push(op);
|
|
170
|
+
proxyMetrics.opsPushed++;
|
|
171
|
+
});
|
|
153
172
|
|
|
154
173
|
/** The minimum surface `bumpOpsVersion` needs off the doorbell contract. */
|
|
155
174
|
interface DoorbellContract {
|
|
@@ -184,12 +203,29 @@ export const bumpOpsVersion = (): void => {
|
|
|
184
203
|
};
|
|
185
204
|
|
|
186
205
|
/** Drain one instance's queued ops — called by the worker's task methods. */
|
|
187
|
-
export const takeOps = (instance: string): Op[] =>
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
206
|
+
export const takeOps = (instance: string): Op[] =>
|
|
207
|
+
timed(() => {
|
|
208
|
+
const queue = opsByInstance.get(instance);
|
|
209
|
+
if (!queue || queue.length === 0) return [];
|
|
210
|
+
opsByInstance.set(instance, []);
|
|
211
|
+
return queue;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Structural memory snapshot of the proxy layer — how much shadow state the
|
|
216
|
+
* engine retains for live + previously-allocated nodes, handlers, and ops
|
|
217
|
+
* still queued. `instances` is not pruned on removeChild (event dispatch may
|
|
218
|
+
* still resolve detached target ids), so liveNodes is nodes-ever-allocated.
|
|
219
|
+
*/
|
|
220
|
+
export const proxyInstanceStats = (): {
|
|
221
|
+
liveNodes: number;
|
|
222
|
+
handlers: number;
|
|
223
|
+
queuedOps: number;
|
|
224
|
+
} => ({
|
|
225
|
+
liveNodes: instances.size,
|
|
226
|
+
handlers: handlers.size,
|
|
227
|
+
queuedOps: [...opsByInstance.values()].reduce((a, q) => a + q.length, 0),
|
|
228
|
+
});
|
|
193
229
|
|
|
194
230
|
export const getHandler = (id: number): HandlerEntry | undefined => handlers.get(id);
|
|
195
231
|
|
|
@@ -226,14 +262,17 @@ export const registerHandler = (
|
|
|
226
262
|
fn: (payload: unknown) => void,
|
|
227
263
|
instance: string,
|
|
228
264
|
instanceId?: number,
|
|
229
|
-
): number =>
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
265
|
+
): number =>
|
|
266
|
+
timed(() => {
|
|
267
|
+
const id = nextHandlerId++;
|
|
268
|
+
handlers.set(id, { fn, instance, instanceId });
|
|
269
|
+
return id;
|
|
270
|
+
});
|
|
234
271
|
|
|
235
272
|
export const unregisterHandler = (id: number): void => {
|
|
236
|
-
|
|
273
|
+
timed(() => {
|
|
274
|
+
handlers.delete(id);
|
|
275
|
+
});
|
|
237
276
|
};
|
|
238
277
|
|
|
239
278
|
/**
|
|
@@ -245,7 +284,9 @@ export const unregisterHandler = (id: number): void => {
|
|
|
245
284
|
* many-mounts-per-module case and would be dropped.
|
|
246
285
|
*/
|
|
247
286
|
export const emit = (name: string, payload?: unknown): void => {
|
|
248
|
-
|
|
287
|
+
timed(() => {
|
|
288
|
+
pushOp(activeInstance, { t: 'emit', name, payload });
|
|
289
|
+
});
|
|
249
290
|
};
|
|
250
291
|
|
|
251
292
|
/* ── Prop serialization ─────────────────────────────────────────────────── */
|
|
@@ -260,32 +301,34 @@ export const emit = (name: string, payload?: unknown): void => {
|
|
|
260
301
|
* closure.
|
|
261
302
|
*/
|
|
262
303
|
export function serializeProps(instance: ElementInstance, props: Record<string, unknown>): WireProps {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
slot
|
|
274
|
-
|
|
304
|
+
return timed(() => {
|
|
305
|
+
const out: WireProps = {};
|
|
306
|
+
for (const [name, value] of Object.entries(props)) {
|
|
307
|
+
if (value === undefined || value === null) continue;
|
|
308
|
+
if (name === 'children' || name === 'key' || name === 'ref' || name === 'dangerouslySetInnerHTML') {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (typeof value === 'function') {
|
|
312
|
+
if (name.length > 2 && name.startsWith('on')) {
|
|
313
|
+
let slot = instance.listenerSlots[name];
|
|
314
|
+
if (slot === undefined) {
|
|
315
|
+
slot = nextHandlerId++;
|
|
316
|
+
instance.listenerSlots[name] = slot;
|
|
317
|
+
}
|
|
318
|
+
handlers.set(slot, {
|
|
319
|
+
fn: value as (payload: unknown) => void,
|
|
320
|
+
instance: instance.instance,
|
|
321
|
+
instanceId: instance.id,
|
|
322
|
+
});
|
|
323
|
+
out[name] = { __evt: slot };
|
|
275
324
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
instance: instance.instance,
|
|
279
|
-
instanceId: instance.id,
|
|
280
|
-
});
|
|
281
|
-
out[name] = { __evt: slot };
|
|
325
|
+
// Non-event functions (render props, callbacks) can't cross the wire — dropped.
|
|
326
|
+
continue;
|
|
282
327
|
}
|
|
283
|
-
|
|
284
|
-
continue;
|
|
328
|
+
out[name] = value; // style objects, numbers, strings, booleans all clone fine
|
|
285
329
|
}
|
|
286
|
-
out
|
|
287
|
-
}
|
|
288
|
-
return out;
|
|
330
|
+
return out;
|
|
331
|
+
});
|
|
289
332
|
}
|
|
290
333
|
|
|
291
334
|
/* ── Small factories ────────────────────────────────────────────────────── */
|
|
@@ -296,22 +339,26 @@ export function newElement(
|
|
|
296
339
|
ns: string | undefined,
|
|
297
340
|
instance: string,
|
|
298
341
|
): ElementInstance {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
342
|
+
return timed(() => {
|
|
343
|
+
const record: ElementInstance = {
|
|
344
|
+
kind: 'element',
|
|
345
|
+
id: allocId(),
|
|
346
|
+
type,
|
|
347
|
+
ns,
|
|
348
|
+
instance,
|
|
349
|
+
props: {},
|
|
350
|
+
listenerSlots: {},
|
|
351
|
+
};
|
|
352
|
+
record.props = serializeProps(record, props);
|
|
353
|
+
instances.set(record.id, record);
|
|
354
|
+
return record;
|
|
355
|
+
});
|
|
311
356
|
}
|
|
312
357
|
|
|
313
358
|
export function newText(text: string, instance: string): TextInstance {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
359
|
+
return timed(() => {
|
|
360
|
+
const record: TextInstance = { kind: 'text', id: allocId(), text, instance };
|
|
361
|
+
instances.set(record.id, record);
|
|
362
|
+
return record;
|
|
363
|
+
});
|
|
317
364
|
}
|
package/src/worker/proxyDom.ts
CHANGED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* react-reconciler ships no TypeScript types. It is a CommonJS factory —
|
|
3
|
-
* `Reconciler(hostConfig)` returns the reconciler API (createContainer,
|
|
4
|
-
* updateContainer, flushSyncWork, flushSyncFromReconciler, flushPassiveEffects…).
|
|
5
|
-
* The host config surface is documented in hostConfig.ts.
|
|
6
|
-
*/
|
|
7
|
-
declare module 'react-reconciler' {
|
|
8
|
-
const createReconciler: (hostConfig: unknown) => {
|
|
9
|
-
createContainer: (...args: unknown[]) => unknown;
|
|
10
|
-
updateContainer: (...args: unknown[]) => unknown;
|
|
11
|
-
updateContainerSync: (...args: unknown[]) => unknown;
|
|
12
|
-
flushSyncWork: () => unknown;
|
|
13
|
-
flushSyncFromReconciler: <T>(fn: () => T) => T;
|
|
14
|
-
flushPassiveEffects: () => boolean;
|
|
15
|
-
[key: string]: unknown;
|
|
16
|
-
};
|
|
17
|
-
export default createReconciler;
|
|
18
|
-
}
|