Skip to content

@xmachines/play-actor

API / @xmachines/play-actor

The actor contract of the XMachines Play Architecture. It names no state machine library.

License: MIT Version

Installation

Terminal window
pnpm add @xmachines/play-actor

Peer dependencies. Install them with the package:

Terminal window
pnpm add @xmachines/play-atom

Overview

@xmachines/play-actor gives you PlayActor, the contract that every layer above reads. An actor of XMachines holds exactly two things:

MemberTypeDescription
stateAtom<TSnapshot>The reactive snapshot of the current state
send(event: TEvent) => voidThe typed event dispatch

It is an interface, and no base class. A state machine library is the concern of an adapter, and @xmachines/play-xstate is the adapter of XState v5. This package therefore names xstate nowhere.

The contract was an abstract CLASS that extended Actor of xstate, and the class carried no implementation at all: both of its members were abstract. Its whole effect was to force the inheritance of XState on every consumer of the contract, including @xmachines/play-view and @xmachines/play-router, which read currentView, currentRoute and send, and no other member.

The capabilities are optional, and separate

A capability lives with the package that holds its shared machinery. Neither one implies the other, and neither one names an engine.

CapabilityInterfacePackageAdds
RoutingRoutable@xmachines/play-routercurrentRoute, initialRoute
ViewViewable@xmachines/play-viewcurrentView

API Summary

PlayActor<TSnapshot, TEvent>

Both type parameters carry their real type through, and neither one erases it.

import type { PlayActor } from "@xmachines/play-actor";
import { createAtom } from "@xmachines/play-atom";
import type { Atom } from "@xmachines/play-atom";
type Snapshot = { readonly value: string };
type AuthEvent =
{ readonly type: "auth.login"; readonly username: string } | { readonly type: "auth.logout" };
class AuthActor implements PlayActor<Snapshot, AuthEvent> {
readonly state = createAtom<Snapshot>({ value: "guest" });
send(event: AuthEvent): void {
if (event.type === "auth.login") this.state.set({ value: "authenticated" });
}
}
// The snapshot type reaches the reader, and it is no `unknown`:
const actor: PlayActor<Snapshot, AuthEvent> = new AuthActor();
const value: string = actor.state.get().value;

A plain object satisfies the contract as well. Nothing here asks for a class.

import type { PlayActor } from "@xmachines/play-actor";
import { createAtom } from "@xmachines/play-atom";
type Snapshot = { readonly value: string };
type AuthEvent =
{ readonly type: "auth.login"; readonly username: string } | { readonly type: "auth.logout" };
const actor: PlayActor<Snapshot, AuthEvent> = {
state: createAtom<Snapshot>({ value: "guest" }),
send: () => {},
};

ActorEvent

The minimal shape of an event that an actor receives: { readonly type: string }. It is the constraint of TEvent, and it is the base of PlayEvent of @xmachines/play.

An actor with a capability

A concrete actor implements the contract and the capabilities that it needs. It extends the actor class of its own engine, and the contract stays out of the inheritance.

import type { PlayActor } from "@xmachines/play-actor";
import type { Routable } from "@xmachines/play-router";
import { createAtom } from "@xmachines/play-atom";
import { Actor, type AnyActorLogic } from "xstate";
class MyRoutableActor
extends Actor<AnyActorLogic>
implements PlayActor<unknown, { readonly type: string }>, Routable
{
readonly state = createAtom<unknown>({});
readonly currentRoute = createAtom<string | null>(() => null);
readonly initialRoute = "/";
}

@xmachines/play-xstate does exactly this, and it composes each capability with compose(PlayerActor, withRouting, withView).

Requirements

  • Node.js >=24.0.0
  • TypeScript >=5.7 (strict mode)
  • ESM only — "type": "module"

@xmachines/play-actor - the actor contract of the Play Architecture

The contract is minimal, and it names no engine: state and send. A state machine library is the concern of an adapter, and @xmachines/play-xstate is the adapter of XState v5.

A capability is optional, and each one lives with the package that holds its shared machinery:

  • Routable, for an actor with a routing support, is in @xmachines/play-router, with the bridge that reads it
  • Viewable, for an actor with a view rendering, is in @xmachines/play-view, with the view store lifecycle, the context projection and the provider guards

A consumer that needs one capability installs one package, and a consumer that needs neither installs neither. Neither capability implies the other.

See

Play RFC

Interfaces