@xmachines/play-actor
Documentation / @xmachines/play-actor
Abstract Actor base class with signal protocol for XMachines Play Architecture
Foundation for all actor implementations, enforcing XState compatibility and reactive signal contracts.
Overview
@xmachines/play-actor provides AbstractActor, a base class that extends XState’s Actor while enforcing the Play Architecture’s signal protocol. It maintains XState ecosystem compatibility (inspection tools, devtools) while exposing reactive signals for infrastructure layer communication.
Per RFC Play v1, this package implements:
- Actor Authority (INV-01): Actor is sole source of truth for state transitions
- Signal-Only Reactivity (INV-05): Infrastructure observes via TC39 Signals, never directly queries
- Passive Infrastructure (INV-04): Infrastructure reflects, never decides
Note: This is an abstract base class. Concrete implementations are provided by adapters (see @xmachines/play-xstate).
Installation
npm install xstate@^5.0.0npm install @xmachines/play-actorCurrent Exports
AbstractActorRoutable(type)Viewable(type)
Peer dependencies:
xstate^5.0.0 — State machine runtime (XState compatibility)@xmachines/play-signals- TC39 Signals primitives@xmachines/play- Protocol types (PlayEvent, etc.)
Quick Start
Usage: This is an abstract base class — use concrete implementations:
import { definePlayer } from "@xmachines/play-xstate";
// definePlayer returns PlayerActor (extends AbstractActor)const createPlayer = definePlayer({ machine, catalog });const actor = createPlayer();actor.start();
// Signal protocol properties (from AbstractActor)console.log(actor.state.get()); // Current snapshotconsole.log(actor.currentRoute.get()); // Derived routeconsole.log(actor.currentView.get()); // Derived view structureAPI Reference
AbstractActor
Abstract base class defining signal protocol:
Abstract Properties (must implement):
state: Signal.State<unknown>- Reactive snapshot of current state
Optional capability interfaces:
Implement Routable to add routing support:
currentRoute: Signal.Computed<string | null>- Derived navigation path
Implement Viewable to add view rendering support:
currentView: Signal.State<ViewMetadata | null>- Current UI structure (updated at state entry)catalog: Record<string, unknown>- Component catalog
Inherited from XState Actor:
send(event): void- Send event to actorstart(): void- Start the actorstop(): void- Stop the actorgetSnapshot()- Get current XState snapshot (typed asSnapshotFrom<TLogic>)
Example implementation pattern:
import { AbstractActor, type Routable, type Viewable, type ViewMetadata } from "@xmachines/play-actor";import { Signal } from "@xmachines/play-signals";import type { AnyActorLogic, AnyMachineSnapshot } from "xstate";
class PlayerActor<TLogic extends AnyActorLogic> extends AbstractActor<TLogic> implements Routable, Viewable{ // Required: reactive state snapshot state = new Signal.State<AnyMachineSnapshot>(this.getSnapshot() as AnyMachineSnapshot);
// Routable: derived navigation path currentRoute = new Signal.Computed(() => { return deriveRoute(this.state.get()); });
// Viewable: current UI structure — Signal.State, updated at state entry (not computed) currentView = new Signal.State<ViewMetadata | null>(null);
// Viewable: component catalog catalog: Record<string, unknown>;
constructor(logic: TLogic, catalog: Record<string, unknown>) { super(logic); this.catalog = catalog;
// Subscribe to XState transitions and update signals this.subscribe((snapshot) => { this.state.set(snapshot as AnyMachineSnapshot); // Update currentView based on snapshot meta... }); }}}Examples
Infrastructure Observing Signals
import { AbstractActor } from "@xmachines/play-actor";import { Signal } from "@xmachines/play-signals";
function syncUrlToActor(actor: AbstractActor<any>) { // Infrastructure passively observes actor's route signal const watcher = new Signal.subtle.Watcher(() => { queueMicrotask(() => { const pending = watcher.getPending(); if (pending.length > 0) { const route = actor.currentRoute.get(); if (route !== null) { // Update browser URL (Passive Infrastructure) window.history.replaceState(null, "", route); } watcher.watch(...pending); // Re-watch } }); });
watcher.watch(actor.currentRoute); actor.currentRoute.get(); // Initial read
return () => watcher.unwatch(actor.currentRoute);}Browser Navigation Sending Events
import { AbstractActor } from "@xmachines/play-actor";
function connectBrowserNavigation(actor: AbstractActor<any>) { const handlePopstate = () => { const path = window.location.pathname;
// Browser event sent to actor (Actor Authority) // Actor guards decide if navigation is valid actor.send({ type: "play.route", to: path }); };
window.addEventListener("popstate", handlePopstate);
return () => { window.removeEventListener("popstate", handlePopstate); };}Architecture
This base class enforces three architectural invariants:
-
Actor Authority (INV-01):
- Actor decides all state transitions via guards
- Infrastructure sends events, actor validates and processes
- Actor’s decision is final — no override by infrastructure
-
Signal-Only Reactivity (INV-05):
- All reactive state exposed via TC39 Signals
- Infrastructure uses
Signal.subtle.Watcherto observe - No direct queries (
getSnapshot()for internal use only)
-
Passive Infrastructure (INV-04):
- Infrastructure reflects actor state (via signals)
- Infrastructure never decides transitions
- Browser/router events sent as commands to actor
XState Compatibility
AbstractActor extends XState’s Actor<TLogic> to maintain:
- Type Safety: Generic
TLogic extends AnyActorLogicparameter - Inspection API: XState Inspector can attach to actors
- DevTools Integration: Standard XState devtools work
- Ecosystem Tools: Works with XState visualization, testing libraries
Snapshot Format: Standard XState snapshots (state + context) remain unchanged — signals are accessible via actor properties, not snapshots.
Related Packages
- @xmachines/play-xstate - Concrete PlayerActor implementation
- @xmachines/play-signals - TC39 Signals primitives
- @xmachines/play - Protocol types (PlayEvent, RouterBridge)
License
Copyright (c) 2016 Mikael Karon. All rights reserved.
This work is licensed under the terms of the MIT license.
For a copy, see https://opensource.org/licenses/MIT.
@xmachines/play-actor - Abstract Actor base class for Play Architecture
This package provides AbstractActor, a minimal base class that extends XState Actor while enforcing the Play Architecture’s signal protocol (RFC section 5.3).
The core protocol is minimal (state + send). Optional capabilities are provided via interfaces:
- Routable: For actors that support routing
- Viewable: For actors that support view rendering
Maintains XState ecosystem compatibility (inspection, devtools) while exposing reactive signals for Infrastructure layer communication.