Skip to content

@wity/graph-player

Temporal simulation engine. Replays a graph snapshot progressively, emitting events as nodes and edges appear and as occurant lifecycle timestamps are crossed.

No DOM. No rendering. Works with any presentation layer.

js
import { GraphPlayer, NODE_STATUS } from '@wity/graph-player';

Concept

GraphPlayer takes a snapshot — a { nodes, edges } object with optional timestamp fields — and turns it into a timed event stream. You wire the events into a GraphStore to build the graph progressively.

This separation is intentional: the player owns scheduling and event emission. The store owns state. The presentation layer owns DOM.

GraphPlayer  ──── 'node:appear'  ────→  store.addNode()
             ──── 'edge:appear'  ────→  store.addEdge()
             ──── 'node:update'  ────→  store.setNodeStatus()

Setup

js
const player = new GraphPlayer(snapshot, options);

// Wire events to the store
player.on('node:appear', ({ node })       => store.addNode(node));
player.on('edge:appear', ({ edge })       => store.addEdge(edge));
player.on('node:update', ({ uid, status}) => store.setNodeStatus(uid, status));
player.on('complete',    ()               => console.log('playback complete'));
player.on('reset',       ()               => store.destroy());

player.play();

Constructor

js
new GraphPlayer(snapshot, options)

snapshot

js
{
  nodes:  node[],
  edges:  edge[],
  events: event[],   // optional — any custom events slotted into the timeline
}

Temporal fields on nodes (all optional, epoch ms):

FieldApplies toMeaning
createdAtcontinuant, occurantWhen the node appears
startedAtoccurantWhen execution began → emits NODE_STATUS.RUNNING
completedAtoccurantWhen it succeeded → emits NODE_STATUS.COMPLETED
erroredAtoccurantWhen it failed → emits NODE_STATUS.ERRORED
cancelledAtoccurantWhen it was cancelled → emits NODE_STATUS.CANCELLED

Temporal field on edges:

FieldDefaultMeaning
createdAtmax(src.createdAt, target.createdAt)When the edge appears

If createdAt is absent on a node, it defaults to 0 (appears at the start).

snapshot.events — custom timeline events

Any additional event type — cursor movements, selections, agent signals — can be scripted into the timeline alongside nodes and edges.

js
{
  type:     string,        // emitted as-is — 'cursor:moved', 'selection:changed', anything
  t:        number,        // timestamp in ms; defaults to 0 if absent
  payload:  object,        // emitted verbatim, merged with actorId if present
  actorId:  string,        // optional — merged into emitted payload for convenience
}

actorId being merged into payload means subscribers receive a flat object:

js
// Snapshot entry:
{ type: 'cursor:moved', actorId: 'user-1', t: 500, payload: { x: 320, y: 210 } }

// What the subscriber receives:
player.on('cursor:moved', ({ actorId, x, y }) => { ... })
// → { actorId: 'user-1', x: 320, y: 210 }

Same-t ordering: nodes → edges → custom events (insertion order preserved within each group).

Backward compatible — snapshots without an events array behave identically to before.

options

OptionTypeDefaultDescription
modestring'sequential'Playback mode (see below)
speednumber1Speed multiplier for 'speed' mode
maxGapnumber3000Max gap in ms for 'maxGap' mode
intervalnumber800Fixed delay in ms for 'sequential' mode
loopbooleanfalseAuto-restart after 'complete'
loopDelaynumber2000ms to wait before restarting on loop

Playback modes

ModeBehaviour
'sequential'Ignores timestamps. Each event fires interval ms after the previous.
'speed'Real timestamp gaps divided by speed. speed: 2 plays at 2× real time.
'realtime'Exact real timestamp gaps. A 5-second gap is 5 seconds of waiting.
'maxGap'Real timestamp gaps, but any gap longer than maxGap ms is clamped. Prevents stalls on long-running processes.
js
// Show a 30-minute agentic run in about 10 seconds
new GraphPlayer(snapshot, { mode: 'speed', speed: 180 });

// Same, but cap any single gap at 2 seconds
new GraphPlayer(snapshot, { mode: 'maxGap', maxGap: 2000 });

// Demo mode — one event per 600ms regardless of timestamps
new GraphPlayer(snapshot, { mode: 'sequential', interval: 600, loop: true, loopDelay: 1500 });

Methods

MethodDescription
play()Start or resume playback
pause()Pause (cursor position is preserved)
reset()Pause and reset cursor to start; emits 'reset'

Properties

PropertyTypeDescription
isPlayingbooleanWhether playback is active
eventCountnumberTotal number of events in the timeline
progressnumber0–1, current position in the timeline

Events

js
player.on('node:appear', ({ node }) => {
  // Add node to the graph
  store.addNode(node);
});

player.on('edge:appear', ({ edge }) => {
  // Draw edge (both endpoints already exist)
  store.addEdge(edge);
});

player.on('node:update', ({ uid, status }) => {
  // Occurant lifecycle transition
  store.setNodeStatus(uid, status);
});

player.on('reset', () => {
  // Player was reset — clear the graph
  store.destroy();
});

player.on('complete', () => {
  // All events fired
});

NODE_STATUS

Constants for occurant lifecycle status values.

js
import { NODE_STATUS } from '@wity/graph-player';

NODE_STATUS.CREATED    // 'created'
NODE_STATUS.RUNNING    // 'running'
NODE_STATUS.COMPLETED  // 'completed'
NODE_STATUS.ERRORED    // 'errored'
NODE_STATUS.CANCELLED  // 'cancelled'

These match what GraphPlayer emits in 'node:update' payloads, and what store.setNodeStatus() expects.


Timeline construction

The player builds a sorted event array at construction time:

  1. One 'node:appear' per node (at createdAt or 0)
  2. One 'node:update' per lifecycle field present (startedAt, completedAt, erroredAt, cancelledAt)
  3. One 'edge:appear' per edge (at edge.createdAt or max(src.createdAt, tgt.createdAt))
  4. Sort by timestamp; same-timestamp events keep insertion order (nodes before edges)

Full example

js
import { GraphStore, SelectionManager } from '@wity/graph-headless';
import { GraphPlayer, NODE_STATUS }     from '@wity/graph-player';

const store  = new GraphStore({ viewport: { width: 1200, height: 800 } });

const snapshot = {
  nodes: [
    { uid: 'task-1', type: 'occurant', title: 'Fetch data',     createdAt: 0,    startedAt: 100,  completedAt: 800  },
    { uid: 'task-2', type: 'occurant', title: 'Process',        createdAt: 0,    startedAt: 850,  completedAt: 2000 },
    { uid: 'result', type: 'continuant', title: 'Final output', createdAt: 2100 },
  ],
  edges: [
    { uid: 'e1', srcUid: 'task-1', targetUid: 'task-2' },
    { uid: 'e2', srcUid: 'task-2', targetUid: 'result' },
  ],
};

const player = new GraphPlayer(snapshot, {
  mode:      'speed',
  speed:     3,
  loop:      true,
  loopDelay: 2000,
});

player.on('node:appear', ({ node }) => {
  store.addNode(node);
  store.computeLayout();
});
player.on('edge:appear', ({ edge }) => store.addEdge(edge));
player.on('node:update', ({ uid, status }) => store.setNodeStatus(uid, status));
player.on('reset',       () => { store.destroy(); });

player.play();