@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.
@@ -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
- if (instance !== '') touchedInstance = instance;
149
- let queue = opsByInstance.get(instance);
150
- if (!queue) opsByInstance.set(instance, (queue = []));
151
- queue.push(op);
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
- const queue = opsByInstance.get(instance);
189
- if (!queue || queue.length === 0) return [];
190
- opsByInstance.set(instance, []);
191
- return queue;
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
- const id = nextHandlerId++;
231
- handlers.set(id, { fn, instance, instanceId });
232
- return id;
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
- handlers.delete(id);
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
- pushOp(activeInstance, { t: 'emit', name, payload });
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
- const out: WireProps = {};
264
- for (const [name, value] of Object.entries(props)) {
265
- if (value === undefined || value === null) continue;
266
- if (name === 'children' || name === 'key' || name === 'ref' || name === 'dangerouslySetInnerHTML') {
267
- continue;
268
- }
269
- if (typeof value === 'function') {
270
- if (name.length > 2 && name.startsWith('on')) {
271
- let slot = instance.listenerSlots[name];
272
- if (slot === undefined) {
273
- slot = nextHandlerId++;
274
- instance.listenerSlots[name] = slot;
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
- handlers.set(slot, {
277
- fn: value as (payload: unknown) => void,
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
- // Non-event functions (render props, callbacks) can't cross the wire — dropped.
284
- continue;
328
+ out[name] = value; // style objects, numbers, strings, booleans all clone fine
285
329
  }
286
- out[name] = value; // style objects, numbers, strings, booleans all clone fine
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
- const record: ElementInstance = {
300
- kind: 'element',
301
- id: allocId(),
302
- type,
303
- ns,
304
- instance,
305
- props: {},
306
- listenerSlots: {},
307
- };
308
- record.props = serializeProps(record, props);
309
- instances.set(record.id, record);
310
- return record;
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
- const record: TextInstance = { kind: 'text', id: allocId(), text, instance };
315
- instances.set(record.id, record);
316
- return record;
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
  }
@@ -13,6 +13,8 @@ export type { ProxyClassList } from './dom/css';
13
13
  export {
14
14
  createProxyDocument,
15
15
  docForInstance,
16
+ peekInstanceDoc,
17
+ disposeInstanceDoc,
16
18
  } from './dom/document';
17
19
  export type {
18
20
  ProxyDocument,
@@ -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
- }