JavaScript Target Guide
Complete guide for generating and running SCXML state machines in JavaScript environments (Node.js, Browser).
Table of Contents
- Overview
- Quick Start
- Code Generation
- Installation
- Datamodel Support
- Framework Integration
- Invoke Children
- Runtime Interpreter
- Event Processing
- API Reference
- 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
scxml-gen traffic.scxml -t js -o TrafficLight.js
2. Install Runtime (Optional)
npm install @scxml-gen/runtime
3. Use in Application
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
# 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:
// 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:
# 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:
scxml-gen traffic.scxml -t js -o TrafficLight.js
This generates:
TrafficLight.js- Self-contained state machine classscxml-runtime.js- Base class (bundled automatically)package.json- Ready to use withnpm start
// 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):
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):
// Generated code is self-contained
import { MyMachine } from './MyMachine.js';
Runtime interpretation (requires npm package):
// 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 |
Native-JS Datamodel (Recommended)
Direct JavaScript expressions in SCXML:
<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:
<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.:
<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
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)
<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)
<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
<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
# 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
- Each bundled SCXML is transpiled to a JavaScript class
- A static
_invokeRegistrymapssrcpaths to classes - When
<invoke>executes, registry is checked first - Children run as fully transpiled code
Example
Parent SCXML:
<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):
// 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:
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
Event Constants (Recommended)
Generated constants provide O(1) dispatch:
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
// 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:
<state id="waiting">
<onentry>
<send event="timeout" delay="5s"/>
</onentry>
<transition event="timeout" target="done"/>
</state>
Generated code automatically uses setTimeout:
// Generated in onentry handler
setTimeout(() => this._send('timeout'), 5000);
Event Data
// 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)
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
class RuntimeInterpreter {
constructor(model);
start();
send(name, data);
isFinished();
getActiveStateIds();
isInState(stateId);
// Trace support
addTraceListener(listener);
removeTraceListener(listener);
}
ScxmlParser
class ScxmlParser {
parse(scxmlString); // Returns model object
}
Event Introspection
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
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:
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
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
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:
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:
- Check
setTimeoutis available (browser/Node.js) - Use
ContinuousExecutorwhich automatically handles delayed events via event-driven wakeup (no polling needed) - 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:
// 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:
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:
// @ts-ignore - Generated SCXML code
import { MyMachine } from './MyMachine.js';
Or create a .d.ts file:
// 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
- Java Target - JVM integration, executors, GraalVM
- C Target - Embedded, Arduino, MISRA
Reference
- Datamodels Guide - Detailed datamodel comparison
- Feature Matrix - Supported SCXML elements
- W3C Compliance - Test results
Tutorials
- User Guide - Complete toolchain guide
- Tutorial - Step-by-step introduction