JavaScript Target Guide

JavaScript Target Guide

Complete guide for generating and running SCXML state machines in JavaScript environments (Node.js, Browser).

Table of Contents

  1. Overview
  2. Quick Start
  3. Code Generation
  4. Installation
  5. Datamodel Support
  6. Framework Integration
  7. Invoke Children
  8. Runtime Interpreter
  9. Event Processing
  10. API Reference
  11. Troubleshooting

Overview

The JavaScript target generates ES6+ classes from SCXML state machines for Node.js and browser environments.

Key Features

Feature Description
ES6+ Classes Modern JavaScript with clean API
Native Event Loop Uses setTimeout for delayed events
Tree-Shakable ES modules with minimal bundle impact
W3C Compliant 100% ECMAScript datamodel (209/209 tests)
React/Vue Ready Framework-agnostic with state change callbacks

Execution Modes

Mode Description Use Case
Transpiled SCXML compiled to JS at build time Production
Runtime SCXML interpreted at runtime Tools, hot-reload

Quick Start

1. Generate Code

bash
scxml-gen traffic.scxml -t js -o TrafficLight.js

2. Install Runtime (Optional)

bash
npm install @scxml-gen/runtime

3. Use in Application

javascript
import { TrafficLight } from './TrafficLight.js';

const machine = new TrafficLight();

// Subscribe to state changes
machine.onStateChange(() => {
    console.log('States:', [...machine.getActiveStateIds()]);
});

// Start and send events
machine.start();
machine.send('timer');

// Check state
if (machine.isInState('green')) {
    console.log('Go!');
}

Code Generation

Basic Generation

bash
# Generate ES module
scxml-gen traffic.scxml -t js -o TrafficLight.js

# Generate with custom class name
scxml-gen traffic.scxml -t js -o traffic.js --class TrafficController

Generated Output

For traffic.scxml:

javascript
// TrafficLight.js
export class TrafficLight {
    // Event constants for O(1) dispatch
    static EVT_TIMER = 1;
    static EVT_EMERGENCY = 2;
    static EVT_UNKNOWN = 9999;

    // Lifecycle
    start() { ... }
    send(name, data) { ... }
    sendById(id, data) { ... }

    // State inspection
    isInState(stateId) { ... }
    getActiveStateIds() { ... }
    isFinished() { ... }

    // Callbacks
    onStateChange(callback) { ... }
}

Invoke Children Bundling

When using <invoke> with external sources:

bash
# Auto-discover all children (recommended)
scxml-gen parent.scxml -t js -o Parent.js --bundle-auto

# Manual bundling
scxml-gen parent.scxml -t js -o Parent.js --bundle child1.scxml,child2.scxml

Installation

Transpiled Output (Default - No NPM Package Needed)

When you generate code with scxml-gen, the output is self-contained:

bash
scxml-gen traffic.scxml -t js -o TrafficLight.js

This generates:

  • TrafficLight.js - Self-contained state machine class
  • scxml-runtime.js - Base class (bundled automatically)
  • package.json - Ready to use with npm start
javascript
// Just import and use - no npm install required!
import { TrafficLight } from './TrafficLight.js';

const machine = new TrafficLight();
machine.start();

NPM Runtime Package (Optional - For Runtime Interpretation)

The @scxml-gen/runtime package is only needed if you want to interpret SCXML at runtime (dynamic loading, hot-reload, SCXML editors):

bash
npm install @scxml-gen/runtime

When you need it:

  • Loading SCXML from strings/files at runtime
  • Building SCXML editors or design tools
  • Development hot-reload without code regeneration

When you DON'T need it:

  • Using transpiled output (the default)
  • Production deployments with pre-compiled state machines

Package Contents

@scxml-gen/runtime
├── scxml-runtime.js      # Transpiled base class
├── runtime/
│   ├── ScxmlParser.js    # SCXML parser
│   └── RuntimeInterpreter.js  # Runtime interpreter

Usage Examples

Transpiled (no npm package):

javascript
// Generated code is self-contained
import { MyMachine } from './MyMachine.js';

Runtime interpretation (requires npm package):

javascript
// For dynamic SCXML loading
import { ScxmlParser, RuntimeInterpreter } from '@scxml-gen/runtime/runtime';

Datamodel Support

Supported Datamodels

Datamodel Description Performance
native-js Native JavaScript expressions Best
null No data, In() only Best
ecmascript Full W3C ECMAScript compliance Good

Direct JavaScript expressions in SCXML:

xml
<scxml datamodel="native-js" initial="start">
    <datamodel>
        <data id="count" expr="0"/>
        <data id="items" expr="[]"/>
        <data id="config" expr="{ timeout: 5000 }"/>
    </datamodel>
    <state id="start">
        <onentry>
            <script>
                count++;
                items.push({ id: count, time: Date.now() });
            </script>
        </onentry>
        <transition cond="count >= config.maxItems" target="done"/>
    </state>
</scxml>

Null Datamodel

Minimal overhead for pure event-driven machines:

xml
<scxml datamodel="null" initial="idle">
    <state id="idle">
        <transition event="start" target="running"/>
    </state>
    <state id="running">
        <transition event="stop" target="idle"/>
        <transition cond="In('running')" event="check" target="running"/>
    </state>
</scxml>

ECMAScript Datamodel

Full W3C compliance with _event, _sessionid, etc.:

xml
<scxml datamodel="ecmascript" initial="start">
    <datamodel>
        <data id="x" expr="0"/>
    </datamodel>
    <state id="start">
        <onentry>
            <script>
                // Access W3C system variables
                console.log('Session:', _sessionid);
                console.log('Machine:', _name);
            </script>
        </onentry>
        <transition event="click">
            <script>
                // Access event data
                x = _event.data.x || 0;
            </script>
        </transition>
    </state>
</scxml>

Framework Integration

React

jsx
import { useState, useEffect, useMemo } from 'react';
import { TrafficLight } from './TrafficLight.js';

function TrafficLightComponent() {
    const machine = useMemo(() => new TrafficLight(), []);
    const [activeStates, setActiveStates] = useState(new Set());

    useEffect(() => {
        const unsubscribe = machine.onStateChange(() => {
            setActiveStates(new Set(machine.getActiveStateIds()));
        });
        machine.start();
        return () => {
            unsubscribe();
            // Cleanup if needed
        };
    }, [machine]);

    return (
        <div className="traffic-light">
            <div className={`light red ${activeStates.has('red') ? 'active' : ''}`} />
            <div className={`light yellow ${activeStates.has('yellow') ? 'active' : ''}`} />
            <div className={`light green ${activeStates.has('green') ? 'active' : ''}`} />
            <button onClick={() => machine.send('timer')}>
                Next
            </button>
        </div>
    );
}

Vue 3 (Composition API)

vue
<template>
    <div class="traffic-light">
        <div v-for="state in ['red', 'yellow', 'green']" :key="state"
             :class="['light', state, { active: activeStates.has(state) }]" />
        <button @click="machine.send('timer')">Next</button>
    </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { TrafficLight } from './TrafficLight.js';

const machine = new TrafficLight();
const activeStates = ref(new Set());

let unsubscribe;

onMounted(() => {
    unsubscribe = machine.onStateChange(() => {
        activeStates.value = new Set(machine.getActiveStateIds());
    });
    machine.start();
});

onUnmounted(() => {
    unsubscribe?.();
});
</script>

Vue 2 (Options API)

vue
<template>
    <div>
        <div v-for="s in activeStates" :key="s">{{ s }}</div>
        <button @click="sendEvent('timer')">Next</button>
    </div>
</template>

<script>
import { TrafficLight } from './TrafficLight.js';

export default {
    data() {
        return {
            machine: null,
            activeStates: []
        };
    },
    mounted() {
        this.machine = new TrafficLight();
        this.machine.onStateChange(() => {
            this.activeStates = [...this.machine.getActiveStateIds()];
        });
        this.machine.start();
    },
    methods: {
        sendEvent(name) {
            this.machine.send(name);
        }
    }
};
</script>

Svelte

svelte
<script>
    import { onMount, onDestroy } from 'svelte';
    import { TrafficLight } from './TrafficLight.js';

    const machine = new TrafficLight();
    let activeStates = new Set();
    let unsubscribe;

    onMount(() => {
        unsubscribe = machine.onStateChange(() => {
            activeStates = new Set(machine.getActiveStateIds());
        });
        machine.start();
    });

    onDestroy(() => {
        unsubscribe?.();
    });
</script>

<div class="traffic-light">
    {#each ['red', 'yellow', 'green'] as color}
        <div class="light {color}" class:active={activeStates.has(color)} />
    {/each}
    <button on:click={() => machine.send('timer')}>Next</button>
</div>

Invoke Children

Why Bundle?

By default, external <invoke src="..."> requires runtime SCXML loading. Bundling pre-compiles children for:

  • No runtime parsing - Children compiled at build time
  • Single output file - All children included with parent
  • Full transpilation - Children run as native JS, not interpreted
  • Offline support - No network requests for SCXML files

Usage

bash
# Automatic discovery (recommended)
scxml-gen parent.scxml -t js -o Parent.js --bundle-auto

# Manual specification
scxml-gen parent.scxml -t js -o Parent.js --bundle child1.scxml --bundle child2.scxml

How It Works

  1. Each bundled SCXML is transpiled to a JavaScript class
  2. A static _invokeRegistry maps src paths to classes
  3. When <invoke> executes, registry is checked first
  4. Children run as fully transpiled code

Example

Parent SCXML:

xml
<scxml name="Parent" initial="running">
    <state id="running">
        <invoke src="file:child.scxml" id="child1"/>
        <transition event="done.invoke.child1" target="done"/>
    </state>
    <final id="done"/>
</scxml>

Generated Code (simplified):

javascript
// Bundled child
class Child_file_child_scxml extends ScxmlStateMachine { ... }

export class Parent extends ScxmlStateMachine {
    static _invokeRegistry = {
        'file:child.scxml': Child_file_child_scxml
    };

    _startInvoke(invokeid, options) {
        const registry = this.constructor._invokeRegistry;
        if (registry?.[options.src]) {
            const ChildClass = registry[options.src];
            return new ChildClass();
        }
        // Fallback to runtime if not bundled
    }
}

Inline vs External Children

Type Bundling Performance
Inline (<content><scxml>) Always transpiled Best
External with --bundle Transpiled Best
External without bundle Runtime interpreter Slower

Runtime Interpreter

For dynamic SCXML loading without pre-compilation:

javascript
import { ScxmlParser, RuntimeInterpreter } from '@scxml-gen/runtime/runtime';

// Parse SCXML string
const scxmlSource = `
    <scxml initial="start">
        <state id="start">
            <transition event="go" target="end"/>
        </state>
        <final id="end"/>
    </scxml>
`;

const parser = new ScxmlParser();
const model = parser.parse(scxmlSource);

// Create and run interpreter
const interpreter = new RuntimeInterpreter(model);
interpreter.start();
interpreter.send('go');
console.log('Finished:', interpreter.isFinished());

Use Cases

  • SCXML Editors - Live preview of designs
  • Hot Reload - Development without rebuilding
  • Dynamic Loading - User-provided state machines
  • Testing - Quick iteration without code generation

Event Processing

Generated constants provide O(1) dispatch:

javascript
import { TrafficLight } from './TrafficLight.js';

const machine = new TrafficLight();
machine.start();

// Fast - integer comparison
machine.sendById(TrafficLight.EVT_TIMER);

// Slower - string lookup
machine.send('timer');

Generated Constants

javascript
// Each machine generates:
static EVT_TIMER = 1;
static EVT_EMERGENCY = 2;
static EVT_BUTTON_CLICK = 3;  // Dots become underscores
static EVT_UNKNOWN = 9999;   // For dynamic events

Delayed Events

JavaScript uses native setTimeout:

xml
<state id="waiting">
    <onentry>
        <send event="timeout" delay="5s"/>
    </onentry>
    <transition event="timeout" target="done"/>
</state>

Generated code automatically uses setTimeout:

javascript
// Generated in onentry handler
setTimeout(() => this._send('timeout'), 5000);

Event Data

javascript
// Send event with data
machine.send('click', { x: 100, y: 200 });

// Access in SCXML
// <script>var pos = _event.data;</script>

API Reference

ScxmlStateMachine (Generated Class)

javascript
class MyMachine {
    // Event constants
    static EVT_EVENT_NAME = 1;
    static EVT_UNKNOWN = 9999;

    // Lifecycle
    start();
    isFinished();

    // Events
    send(name, data);
    sendById(eventId, data);
    processDelayedEvents();

    // State inspection
    isInState(stateId);
    getActiveStateIds();  // Returns Set<string>

    // Callbacks
    onStateChange(callback);  // Returns unsubscribe function
}

RuntimeInterpreter

javascript
class RuntimeInterpreter {
    constructor(model);

    start();
    send(name, data);
    isFinished();
    getActiveStateIds();
    isInState(stateId);

    // Trace support
    addTraceListener(listener);
    removeTraceListener(listener);
}

ScxmlParser

javascript
class ScxmlParser {
    parse(scxmlString);  // Returns model object
}

Event Introspection

javascript
const all = machine.getAllEvents();              // Set<string>
const enabled = machine.getEnabledEvents();      // Set<string>, guard-aware
const forState = machine.getEventsForState('s1'); // Set<string>
const enabledFor = machine.getEnabledEventsForState('s1'); // Set<string>

Trace API

javascript
import {
    TraceListener,
    ConsoleTraceListener,
    InvokeAwareTraceListener,
    JsonlTraceWriter,
    TraceRecorder,
    TraceReader
} from '@scxml-gen/runtime/runtime';

// Console debugging
const consoleListener = new ConsoleTraceListener({ verbose: true });
interpreter.addTraceListener(consoleListener);

// JSONL file output (Node.js)
import fs from 'fs';
const stream = fs.createWriteStream('trace.jsonl');
const jsonlWriter = new JsonlTraceWriter(line => stream.write(line + '\n'));
interpreter.addTraceListener(jsonlWriter);

// Array collection (Browser)
const lines = [];
const writer = new JsonlTraceWriter(line => lines.push(line));
interpreter.addTraceListener(writer);

// Recording and playback
const recorder = new TraceRecorder();
interpreter.addTraceListener(recorder);
interpreter.start();
interpreter.send('go');
const trace = recorder.toJSON();

InvokeAwareTraceListener

Extended trace listener for parent/child machine context:

javascript
import { InvokeAwareTraceListener, InvokeContextWrapper } from '@scxml-gen/runtime/runtime';

class MyInvokeAwareListener extends InvokeAwareTraceListener {
    onStateEnterWithInvoke(stateId, activeStates, timestamp, invokeId) {
        if (invokeId) {
            console.log(`Child ${invokeId} entered: ${stateId}`);
        } else {
            console.log(`Parent entered: ${stateId}`);
        }
    }
}

// Wrap listener for child machine
const parentListener = new JsonlTraceWriter(line => console.log(line));
parent.addTraceListener(parentListener);
const childListener = new InvokeContextWrapper(parentListener, 'childId');
childMachine.addTraceListener(childListener);

Executor API

javascript
import {
    StateMachineExecutor,
    RunToCompletionExecutor,
    ContinuousExecutor
} from '@scxml-gen/runtime';

// Synchronous execution
const syncExecutor = new RunToCompletionExecutor(machine);
syncExecutor.start();
syncExecutor.send('go');
console.log(syncExecutor.getActiveStateIds());

// Async execution with event-driven wakeup
const asyncExecutor = new ContinuousExecutor(machine);
asyncExecutor.start();
await asyncExecutor.sendAsync('go');
await asyncExecutor.waitForCompletion();  // Wait for delayed events
asyncExecutor.stop();

StateMachineExecutor Interface

javascript
class StateMachineExecutor {
    get machine();              // Underlying state machine
    get isRunning();            // Whether executor is running
    get isFinished();           // Whether machine has finished

    start(initData);            // Start the machine
    stop();                     // Stop the executor

    send(name, data);      // Synchronous event firing
    sendAsync(name, data); // Async event firing (returns Promise)

    pumpEvents();               // Process pending delayed events
    getActiveStateIds();        // Get active states
    isInState(stateId);         // Check state

    addTraceListener(listener);
    removeTraceListener(listener);
}

Troubleshooting

"Module not found" errors

Symptom: Cannot find module '@scxml-gen/runtime'

Solution: Install the runtime package:

bash
npm install @scxml-gen/runtime

Or use self-contained transpiled output (no package needed).

Delayed events not firing

Symptom: <send delay="..."> events never arrive.

Solutions:

  1. Check setTimeout is available (browser/Node.js)
  2. Use ContinuousExecutor which automatically handles delayed events via event-driven wakeup (no polling needed)
  3. For manual control, call pumpEvents() on the executor

State changes not updating UI

Symptom: React/Vue component doesn't re-render.

Solution: Ensure state change callback triggers framework update:

javascript
// React - use setState
machine.onStateChange(() => {
    setActiveStates(new Set(machine.getActiveStateIds()));
});

// Vue - use reactive ref
machine.onStateChange(() => {
    activeStates.value = new Set(machine.getActiveStateIds());
});

Invoke children not working

Symptom: External <invoke src="..."> fails.

Solution: Bundle children at build time:

bash
scxml-gen parent.scxml -t js -o Parent.js --bundle-auto

Without bundling, external sources require runtime SCXML loading.

TypeScript errors

Symptom: Type errors when importing generated code.

Solution: Add type declaration or use // @ts-ignore:

typescript
// @ts-ignore - Generated SCXML code
import { MyMachine } from './MyMachine.js';

Or create a .d.ts file:

typescript
// MyMachine.d.ts
export class MyMachine {
    static EVT_EVENT: number;
    start(): void;
    send(name: string, data?: any): void;
    isInState(id: string): boolean;
    getActiveStateIds(): Set<string>;
    isFinished(): boolean;
    onStateChange(cb: () => void): () => void;
}

See Also

Target Guides

Reference

Tutorials