Corefunction

Module inspection

Observe detached ownership snapshots and bounded activity traces, with an optional development iframe bridge.

Ownership snapshots

const snapshot = host.inspect()
const stop = host.subscribe(() => console.log(host.inspect()))

// Stop observing when the inspector closes.
stop()

inspect() returns detached, JSON-safe ModuleSnapshot[]. Each snapshot contains id, constructor name, optional string label (from the module's id property), status, inherited priority, selection count, declared capabilities, and children. Node IDs are stable within a host; constructor names may be minified in production. Inspection does not execute debug() or expose selected values.

subscribe(listener) runs after root mount, unmount, and retirement. It does not run immediately, on state changes, or on frames. Failed mounts do not publish a tree. Retirement publishes an empty tree and releases ownership subscribers.

Activity sampling

import type { ModuleActivity } from '@joycostudio/xyz'

const samples: ModuleActivity[] = []
const stop = host.subscribeActivity((sample) => {
  samples.push(sample)
  if (samples.length > 10) samples.shift()
})

// Releases the recorder and timer when the last observer disconnects.
stop()

Activity recording is opt-in and disabled when NODE_ENV is production. One timer per host delivers a sample every second to all observers. No recorder or timer is allocated without a subscriber. With no process global, explicit activity subscriptions still work; unbundled imports do not require a Node shim.

Each sample has:

FieldContents
sequenceIncreasing sample number for the current recording session.
elapsedMilliseconds since the previous sample or recorder creation.
countsPer-module counts of tick, emit, receive, selection, and sync.
entriesAt most 200 event/state trace entries per interval.
droppedNumber of older entries dropped when the interval exceeded its bound.
connectionsCurrent listener edges with source, target, event, and count. External endpoints are null.

Entries include a timestamp, module and peer IDs, kind, name, bounded text detail, and causal entry IDs. They follow synchronous emission through reception, store notification, selection changes, and coalesced sync. Tick activity increments counters without producing trace rows. Counts and entries reset after each sample; retain bounded history in your inspector if desired.

Payload previews do not retain live state or invoke ordinary getters or toJSON(). Object change previews examine at most 64 enumerated keys per level and fall back to a short value preview when the scan is truncated. This bounds comparisons and retained keys; JavaScript key enumeration and custom proxy traps can still do additional work.

Same-origin preview bridge

import { publishModuleTree } from '@joycostudio/xyz'

const disconnect = publishModuleTree(host, 'Scene preview')
// On preview cleanup:
disconnect()

The bridge is active only in NODE_ENV=development inside an iframe. It publishes the initial tree and later ownership changes to the same-origin parent. Outside that environment, it returns a no-op cleanup. It accepts requests only from that parent window with the matching origin. When crypto.randomUUID() is unavailable (such as plain HTTP on a LAN address), the bridge uses a timestamp/random session identifier. Session IDs correlate messages; origin and source checks enforce the connection boundary.

Build a parent inspector with the exported constants and TreeMessage union:

ConstantMessage
TREE_REQUESTParent sends { type: TREE_REQUEST } to request a snapshot and start activity.
TREE_SNAPSHOTPreview replies { type: TREE_SNAPSHOT, tree }, where tree has session, name, and roots; cleanup sends tree: null.
TREE_ACTIVITYPreview sends { type: TREE_ACTIVITY, session, activity } once per second while requested.
TREE_RELEASEParent sends { type: TREE_RELEASE } to stop activity without removing the preview.

Parent consumers should check both event.origin and event.source === iframe.contentWindow, and associate activity with the current tree's session. Use the constants on both sides: the package's protocol is namespaced xyz:module-tree:*, replacing the source app's bzrp:module-tree:*.

The package provides inspection data and transport. Inspector UI, graph layout, source-file discovery, and application-specific Studio integration remain in the consuming app.