Skip to content

@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

Terminal window
npm install xstate@^5.0.0
npm install @xmachines/play-actor

Current Exports

  • AbstractActor
  • Routable (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 snapshot
console.log(actor.currentRoute.get()); // Derived route
console.log(actor.currentView.get()); // Derived view structure

API 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 actor
  • start(): void - Start the actor
  • stop(): void - Stop the actor
  • getSnapshot() - Get current XState snapshot (typed as SnapshotFrom<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:

  1. 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
  2. Signal-Only Reactivity (INV-05):

    • All reactive state exposed via TC39 Signals
    • Infrastructure uses Signal.subtle.Watcher to observe
    • No direct queries (getSnapshot() for internal use only)
  3. 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 AnyActorLogic parameter
  • 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.

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.

See

RFC Play v1 Section 5.3

Classes

Interfaces