@atolljs/islands 0.1.6 → 0.1.7

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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/island.ts +54 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolljs/islands",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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.6"
22
+ "@atolljs/core": "0.1.7"
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",
package/src/island.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  * is least-busy round-robin, so a second worker would receive dispatches for
15
15
  * a tree it doesn't hold (sticky routing is future work).
16
16
  */
17
- import { connectWorker, observe } from '@atolljs/core';
17
+ import { connectWorker, emitDevtools, observe } from '@atolljs/core';
18
18
  import type {
19
19
  ConnectWorkerConfig,
20
20
  SharedSpec,
@@ -121,6 +121,13 @@ interface MountIslandBaseOptions {
121
121
  * cross-origin isolation requirement. Switch later with `handle.setMode`.
122
122
  */
123
123
  mode?: Mode;
124
+ /**
125
+ * Optional renderer/framework tag — pure observability metadata forwarded
126
+ * to devtools on `island:mount` ('react', 'vue', 'svelte', …). Framework
127
+ * shell bindings can set it automatically; direct mountIsland callers
128
+ * declare it themselves.
129
+ */
130
+ framework?: string;
124
131
  /**
125
132
  * Milliseconds the mount handshake (mount + whoami) may take before the
126
133
  * returned promise rejects — default 15_000, `0` disables. A worker entry
@@ -336,7 +343,7 @@ const cssStyleValue = (key: string, value: unknown): string => {
336
343
  export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandle> {
337
344
  const {
338
345
  el, onEvent, onActivity, onOps, slots, mode: initialMode, mountTimeout,
339
- client: givenClient, worker, app: givenApp, props: givenProps,
346
+ client: givenClient, worker, app: givenApp, props: givenProps, framework,
340
347
  ...workerConfig
341
348
  } = opts;
342
349
  // Shell→worker callables: callbackProp() markers marshal to {__cb:id}
@@ -442,9 +449,8 @@ export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandl
442
449
  );
443
450
  // The whole point: an event = one postMessage round-trip. The worker
444
451
  // re-renders, we apply whatever ops come back.
445
- void client
446
- .dispatch(handlerId, payload)
447
- .then(applyOps)
452
+ void islandCall('dispatch', client.dispatch(handlerId, payload))
453
+ .then((ops) => applyOps(ops, 'dispatch'))
448
454
  .catch((err) => console.error(`[island ${instance}] dispatch failed`, err));
449
455
  };
450
456
  }
@@ -771,24 +777,53 @@ export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandl
771
777
  break;
772
778
  }
773
779
  // Not a DOM mutation — the island→shell channel.
780
+ emitDevtools({ type: 'island:event', instance, name: op.name });
774
781
  onEvent?.(op.name, op.payload);
775
782
  break;
776
783
  }
777
784
  }
778
785
  }
779
786
 
780
- function applyOps(ops: Op[]): void {
787
+ function applyOps(ops: Op[], via: 'mount' | 'updateProps' | 'dispatch' | 'setSize' | 'flush'): void {
781
788
  opsApplied += ops.length;
782
789
  onActivity?.();
783
- const t0 = onOps !== undefined ? performance.now() : 0;
790
+ const t0 = performance.now();
784
791
  for (const op of ops) applyOp(op);
785
- onOps?.(ops, performance.now() - t0);
792
+ const replayMs = performance.now() - t0;
793
+ emitDevtools({ type: 'island:ops', instance, via, count: ops.length, replayMs });
794
+ onOps?.(ops, replayMs);
786
795
  }
787
796
 
797
+ /** Times a main→worker round-trip for the devtools inspector. */
798
+ const islandCall = <T>(
799
+ method: 'mount' | 'whoami' | 'dispatch' | 'setSize' | 'updateProps' | 'flush' | 'unmount',
800
+ p: Promise<T>,
801
+ ): Promise<T> => {
802
+ const t0 = performance.now();
803
+ return p.then(
804
+ (r) => {
805
+ emitDevtools({
806
+ type: 'island:task', instance, method,
807
+ ms: performance.now() - t0,
808
+ ops: Array.isArray(r) ? r.length : undefined,
809
+ });
810
+ return r;
811
+ },
812
+ (err) => {
813
+ emitDevtools({
814
+ type: 'island:task', instance, method,
815
+ ms: performance.now() - t0,
816
+ error: err instanceof Error ? err.message : String(err),
817
+ });
818
+ throw err;
819
+ },
820
+ );
821
+ };
822
+
788
823
  const doFlush = (): void => {
789
824
  if (destroyed) return;
790
825
  flushCalls++;
791
- void client.flush(instance).then(applyOps);
826
+ void islandCall('flush', client.flush(instance)).then((ops) => applyOps(ops, 'flush'));
792
827
  };
793
828
 
794
829
  /* ── Transport: push (shared-memory doorbell) vs poll ─────────────────── */
@@ -842,15 +877,16 @@ export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandl
842
877
 
843
878
  let pid: string;
844
879
  try {
845
- applyOps(await raced(client.mount(instance, props)));
846
- pid = await raced(client.whoami(instance));
880
+ applyOps(await raced(islandCall('mount', client.mount(instance, props))), 'mount');
881
+ pid = await raced(islandCall('whoami', client.whoami(instance)));
882
+ emitDevtools({ type: 'island:mount', instance, app, pid, poolId: client.pool?.poolId, framework });
847
883
  } catch (err) {
848
884
  // The instance may exist worker-side with ops we never applied — release it.
849
885
  // An island-owned client also terminates (stops a crash→respawn loop on
850
886
  // a permanently-broken entry); a shared client just drops the instance.
851
887
  clientMounts.set(client, (clientMounts.get(client) ?? 1) - 1);
852
888
  if (givenClient === undefined) client.terminate();
853
- else void client.unmount(instance).catch(() => {});
889
+ else void islandCall('unmount', client.unmount(instance)).catch(() => {});
854
890
  throw err;
855
891
  }
856
892
 
@@ -879,7 +915,7 @@ export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandl
879
915
  if (w === lastW && h === lastH) return;
880
916
  lastW = w;
881
917
  lastH = h;
882
- void client.setSize(instance, w, h).then(applyOps);
918
+ void islandCall('setSize', client.setSize(instance, w, h)).then((ops) => applyOps(ops, 'setSize'));
883
919
  };
884
920
  const resizeObserver =
885
921
  typeof ResizeObserver === 'function'
@@ -918,13 +954,14 @@ export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandl
918
954
  updateProps: async (next: Record<string, unknown>) => {
919
955
  const marshalled = marshalCallbackProps(next, registerCb) as Record<string, unknown>;
920
956
  assertCloneableProps(marshalled, `updateProps(${app})`);
921
- applyOps(await client.updateProps(instance, marshalled));
957
+ applyOps(await islandCall('updateProps', client.updateProps(instance, marshalled)), 'updateProps');
922
958
  },
923
959
  flush: async () => {
924
- applyOps(await client.flush(instance));
960
+ applyOps(await islandCall('flush', client.flush(instance)), 'flush');
925
961
  },
926
962
  destroy: () => {
927
963
  destroyed = true;
964
+ emitDevtools({ type: 'island:unmount', instance });
928
965
  unsubscribe?.();
929
966
  resizeObserver?.disconnect();
930
967
  if (pollTimer !== null) clearInterval(pollTimer);
@@ -936,7 +973,7 @@ export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandl
936
973
  // detach ops are irrelevant if our element is already gone, so the
937
974
  // return batch is dropped. Best-effort: the instance map and its
938
975
  // handlers release regardless.
939
- void client.unmount(instance).catch(() => {});
976
+ void islandCall('unmount', client.unmount(instance)).catch(() => {});
940
977
  } else {
941
978
  // Last island — still run the instance's teardown first so the app's
942
979
  // dispose hook cancels deferred work (library timers, animation
@@ -944,8 +981,7 @@ export async function mountIsland(opts: MountIslandOptions): Promise<IslandHandl
944
981
  // kill the thread before unmount lands — equally fine, process
945
982
  // death is teardown — but in-process workers need this or the
946
983
  // instance zombies on and its pending callbacks mutate foreign docs.
947
- void client
948
- .unmount(instance)
984
+ void islandCall('unmount', client.unmount(instance))
949
985
  .catch(() => {})
950
986
  .finally(() => {
951
987
  // A remount may have registered while teardown was in flight