Skip to content

Headless runtime

Every igniteCore(...) registration is also a headless runtime. The same value you call with a tag name to register a custom element exposes methods that drive and inspect the component contract without a DOM — the surface agents and tests use before any selector is involved.

The object passed to igniteCore(config) is executable authoring input. It can contain source actors or factories, callbacks, selectors, effects, and other application-only values. It is not the serialized runtime contract. Use runtime.getSchema() for the sole public compiled, JSON-safe Ignite blueprint.

import { igniteCore } from 'ignite-element/xstate';
const counter = igniteCore({
source: counterMachine,
view: ({ snapshot }) => ({
isOn: snapshot.matches('on'),
}),
commands: ({ actor }) => ({
toggle: () => actor.send({ type: 'TOGGLE' }),
}),
events: (event) => ({
toggled: event<{ isOn: boolean }>(),
}),
effects: ({ emit, select }) => {
const isOn = select((snapshot) => snapshot.matches('on'));
if (!isOn.changed) return;
emit({ type: 'toggled', isOn: isOn.current });
},
});
// Register a DOM projection later — the runtime is available immediately.
const { snapshot, events } = await counter.execute({ command: 'toggle' });
const isToggleAvailable = counter.canExecute('toggle');
const current = counter.getSnapshot();
const view = counter.getView();
const schema = counter.getSchema();
LayerContract
AuthoringigniteCore(config) accepts executable application input, including source ownership and callbacks.
BlueprintgetSchema() is the sole public compiled JSON-safe description of commands, declared events, snapshot, and view.
Focused readsgetSnapshot(), getView(), and canExecute(...) answer one live question at a time.
Subscriptionson(...), watchSnapshot(...), and watchView(...) keep long-lived consumers synchronized.
Private machineryEach projection validation and commit attempt captures one coherent inspection inside Ignite; it is not a public API.

Separate focused getter calls are non-atomic. A source transition between calls can make them observe different revisions; use subscriptions and execute() results for ongoing synchronization.

Runs a command by name and resolves at command acknowledgement, after the command window closes and the transition result is available. It does not wait for long-running or asynchronous effects. The input type is inferred from the command function’s first parameter, so commands that take no argument omit input.

Returns Promise<{ snapshot, events }> — the snapshot after the transition plus the ordered events emitted by that single transition (an IgniteAgentExecutionResult).

const result = await counter.execute({ command: 'toggle' });

The shape:

// IgniteAgentExecutionResult<State, Events>
{
snapshot: { value: 'on', context: {} },
events: [{ type: 'toggled', isOn: true }],
}

events is the same ordered stream the runtime emitted for that transition, so assertions stay deterministic and align with the events delivered to on(...) listeners.

For sources with a native emit stream (Actor-Web today), domain events the source emits during the command window are captured too as flat { type, ...fields } members, independent of the events: declaration. See declared vs source-emitted events.

Returns whether a command is available for the current snapshot. Commands without a canExecute predicate are available by default; commands with command(..., { canExecute }) evaluate that predicate on demand.

Returns boolean.

const canToggle = counter.canExecute('toggle');

Unknown command names throw the same unknown-command error shape as execute(...).

Returns the current raw snapshot of the underlying source.

Returns State — the underlying source snapshot (the XState snapshot, the Redux state, or the MobX observable value).

const snapshot = counter.getSnapshot();

The pre-stable getState() alias was removed at v3.0.0 — use getSnapshot().

Returns the current projected view — the object your view(...) builder derives from the snapshot.

Returns View — the projected view object, or {} when no view is configured.

const view = counter.getView();

Subscribes to a public emitted event. The handler receives the flat emitted event member.

Returns a subscription — { unsubscribe(): void }. Call unsubscribe() to stop listening.

const subscription = counter.on('toggled', (event) => {
console.log(event.isOn);
});
subscription.unsubscribe();

Use on(...) for the public events your component emits through effects. When the source has a native emit stream (Actor-Web today), on(...) also receives the source’s emitted domain events as the same flat { type, ...fields } shape, and the names are typed from the source’s Emitted union. (The pre-stable subscribe(eventName, handler) alias was removed at v3.0.0 — use on.)

Subscribes to raw snapshot updates. The handler receives (snapshot, prevSnapshot) on every source update.

Returns a subscription — { unsubscribe(): void }.

const subscription = counter.watchSnapshot((snapshot, prevSnapshot) => {
console.log(prevSnapshot.value, '->', snapshot.value);
});

The pre-stable watch(handler) alias was removed at v3.0.0 — use watchSnapshot().

Subscribes to projected view updates. The handler receives (view, prevView) on every view change.

Returns a subscription — { unsubscribe(): void }.

const subscription = counter.watchView((view, prevView) => {
console.log(prevView.isOn, '->', view.isOn);
});

Returns the sole public compiled, JSON-safe Ignite blueprint describing the component’s commands, declared public events, and current state. Command metadata declared with command(...) is included here.

“Blueprint” is Ignite vocabulary, not a claim that this object is a formal JSON Schema document. A command’s input metadata may use JSON-Schema-like fragments, but the complete blueprint is an Ignite discovery contract.

Returns { commands, events, snapshot, view } — a JSON-serializable IgniteAgentSchema: commands maps each name to its metadata ({} when none), events is the array of declared event descriptors such as { type }, snapshot is the current snapshot value, and view is the projected view. Commands with an availability predicate include gated: true; call canExecute(commandName) for the current boolean result.

The blueprint excludes source actors and factories, effects, callbacks, selectors, registries, projection bindings, committers, and executable model-authored content.

const schema = counter.getSchema();

The shape:

// getSchema() returns:
{
commands: {
toggle: {
description: 'Toggle the panel open state.',
},
setMode: {
description: 'Switch the device mode.',
input: {
type: 'string',
enum: ['auto', 'manual', 'off'],
},
},
armAlarm: {
description: 'Arm the alarm when every zone is closed.',
gated: true,
},
},
events: [{ type: 'toggled' }, { type: 'alarm-armed' }],
snapshot: {
value: 'idle',
context: {
mode: 'auto',
zonesOpen: 0,
armed: false,
},
},
view: {
mode: 'auto',
canArmAlarm: true,
statusLabel: 'Ready',
},
}

How tools usually consume it:

  1. Read commands to decide which actions exist and whether any carry input schemas or descriptions.
  2. Use input to shape payloads before calling execute({ command, input? }).
  3. Treat gated: true as “availability is dynamic” and call canExecute(commandName) before offering or invoking that command.
  4. Use snapshot when the raw source state matters, and view when the projected UI-facing state is the better contract.
  5. Observe outcomes with execute().events, on(...), or watchView(...) instead of assuming a command succeeded silently.

Example runtime flow:

// Install subscriptions before the initial read so a transition during the
// bootstrap handoff is observed.
const eventSubscription = panel.on('alarm-armed', (event) => {
console.log('Alarm status:', event);
});
const viewSubscription = panel.watchView((view) => {
console.log('Status label:', view.statusLabel);
});
const schema = panel.getSchema();
const armMetadata = schema.commands.armAlarm;
if (armMetadata.gated && !panel.canExecute('armAlarm')) {
throw new Error('Alarm cannot be armed while a zone is open.');
}
const result = await panel.execute({ command: 'armAlarm' });
console.log('Command events:', result.events);
eventSubscription.unsubscribe();
viewSubscription.unsubscribe();

This separation is intentional:

  • getSchema() tells you what the runtime exposes right now in JSON-safe form.
  • canExecute() answers dynamic command availability.
  • execute() performs the transition and returns the resulting snapshot plus emitted events.
  • on(...) and watchView(...) keep long-lived observers in sync across bootstrap and after the initial schema read.

Ignite does not expose a second getBlueprint() alias, a public inspect() method, or a public inspection-bundle type. Internally, each projection validation and commit attempt captures one private coherent inspection, so the document and behavior facts for that attempt come from the same captured revision. An asynchronous external commit is not transactional with source transitions that occur later; the captured inspection is not part of the headless API.

Starts a named story recorder over the same runtime.

Returns a story — an object that records command, snapshot, view, and event activity as one ordered behavior trace (execute, until, trace, lifecycle, summary, stop).

const story = counter.record('toggles on');

The runtime seeds prevSnapshot on attach, never replays history, and runs effects(...) once per update in a stable order — which is what keeps execute() and the testing helpers reproducible. The full semantics are documented once in Deterministic effects.