@xmachines/play
API / @xmachines/play
Core protocol layer for the Universal Player Architecture — defines
PlayEvent,PlayError, and architectural contracts enabling loose coupling between business logic and runtime adapters.
Part of the XMachines JS monorepo.
Installation
npm install @xmachines/playNode.js
>= 22.0.0required. All packages are ES modules ("type": "module").
Overview
@xmachines/play is the foundational package in the XMachines ecosystem. It defines the minimal set of types and utilities that all other @xmachines/* packages build upon:
PlayEvent<TPayload>— the universal event contract for Actor ↔ Infrastructure communicationPlayError— the typed base class for all@xmachines/*runtime errorsNonNullableError— thrown when a required value isnullorundefinedassertNonNullable()— assertion utility that narrowsT | null | undefinedtoT
These protocols implement the architectural invariants defined in the Play RFC:
| # | Invariant | Description |
|---|---|---|
| INV-01 | Actor Authority | The Actor is the final authority; guards decide all transitions |
| INV-02 | Strict Separation | Business logic never imports UI frameworks or routing libraries |
| INV-04 | Passive Infrastructure | Infrastructure observes Actor signals; it never enforces guards |
| INV-05 | Signal-Only Reactivity | TC39 Signals are the exclusive cross-boundary communication medium |
Usage
PlayEvent<TPayload>
The minimal event contract: any object with a type: string property. Framework-agnostic — works with XState, Robot, and any other state machine library.
import type { PlayEvent } from "@xmachines/play";
// Flexible (accepts any additional fields):const event: PlayEvent = { type: "auth.login", userId: "user123" };
// Type-safe (with generic payload):type LoginEvent = PlayEvent<{ userId: string; timestamp: number }>;
const loginEvent: LoginEvent = { type: "auth.login", userId: "user123", timestamp: Date.now(),};
// TypeScript error: missing required fieldconst invalid: LoginEvent = { type: "auth.login" }; // Error!PlayError
Base class for all @xmachines/* runtime errors. Every error has a stable scope (throwing class/module) and code (machine-readable identifier). Always branch on .code or subclass — never on .message.
import { PlayError } from "@xmachines/play";import { NonNullableError } from "@xmachines/play/errors";
try { bridge.connect();} catch (err) { if (err instanceof NonNullableError) { // err.scope === "assertNonNullable" // err.code === "PLAY_NON_NULLABLE" console.error(`Missing value: ${err.message}`); } else if (err instanceof PlayError) { // Any other @xmachines/* error console.error(`[${err.scope}:${err.code}] ${err.message}`); } else { throw err; }}Extend PlayError in your own @xmachines/*-compatible packages:
import { PlayError } from "@xmachines/play";
export class MyPackageError extends PlayError { constructor(message: string, options?: ErrorOptions) { super("MyScope", "MY_PACKAGE_ERROR_CODE", message, options); this.name = "MyPackageError"; }}assertNonNullable(value, name?)
Assertion utility that returns value typed as NonNullable<V> or throws NonNullableError. Eliminates unsafe ! non-null assertions.
import { assertNonNullable } from "@xmachines/play";
// Inject + assert in one line — no intermediate variable or `!` needed:const actor = assertNonNullable(inject<AuthActor>("actor"), "actor");
// DOM element lookup:const el = assertNonNullable(document.getElementById("app"), "#app");API Summary
Exported from @xmachines/play
| Export | Kind | Description |
|---|---|---|
PlayEvent<TPayload> | type | Universal event contract — { type: string } & TPayload |
PlayError | class | Base class for all @xmachines/* typed errors |
NonNullableError | class | Thrown by assertNonNullable when a value is null/undefined |
assertNonNullable | function | Asserts non-null, returns narrowed value |
Exported from @xmachines/play/errors
| Export | Kind | Description |
|---|---|---|
PlayError | class | Re-exported base error class |
NonNullableError | class | scope: "assertNonNullable", code: "PLAY_NON_NULLABLE" |
Error Codes
| Code | Class | Thrown When |
|---|---|---|
PLAY_NON_NULLABLE | NonNullableError | assertNonNullable() receives null or undefined |
Other @xmachines/* packages export their own error subclasses from their respective ./errors subpath:
| Package | Import path |
|---|---|
@xmachines/play | @xmachines/play/errors |
@xmachines/play-router | @xmachines/play-router/errors |
@xmachines/play-xstate | @xmachines/play-xstate/errors |
@xmachines/play-react | @xmachines/play-react/errors |
@xmachines/play-solid | @xmachines/play-solid/errors |
@xmachines/play-vue-router | @xmachines/play-vue-router/errors |
Testing
Run tests for this package in isolation:
npm test -w @xmachines/playOr from the package directory:
npm testTests use Vitest and cover the PlayError class construction, inheritance, cause support, and subclassing patterns.
License
MIT © Mikael Karon
See LICENSE for details.
@xmachines/play - Core Protocol Layer
Defines architectural contracts enabling Actor ↔ Infrastructure communication without direct dependencies. Per RFC section 5.2, these protocols establish the foundation for loose coupling between business logic and runtime adapters.
Exports
PlayEvent
- Any object with a
type: stringproperty - Generic
TPayloadparameter for type-safe event shapes (optional) - Defaults to
Record<string, unknown>for maximum flexibility - Framework-agnostic (not tied to XState or any specific library)
Usage:
// Flexible (default):const event: PlayEvent = { type: "auth.login", userId: "123" };
// Type-safe (with generic):type LoginEvent = PlayEvent<{ userId: string }>;const event: LoginEvent = { type: "auth.login", userId: "123" };Common Event Patterns:
- Domain events:
{ type: 'auth.login', userId: '123' } - Custom events:
{ type: 'form.submit', data: {...} }
Routing Events are provided by @xmachines/play-router:
- PlayRouteEvent: Enhanced routing with parameters and state ID targeting
- RouterBridge: Protocol for router adapters to connect with actors
Browser Navigation: Browser back/forward buttons are handled by router adapters
via the popstate event. When users press back/forward, the router detects the URL
change and sends a PlayRouteEvent to the actor for validation.
import type { PlayRouteEvent, RouterBridge } from "@xmachines/play-router";Architectural Invariants
These protocols enforce the following invariants:
- Actor Authority: Infrastructure proposes intents, Actor decides validity
- Strict Separation: No direct dependencies between layers
- Passive Infrastructure: Infrastructure observes Actor signals, never controls
- Signal-Only Reactivity: All state changes flow through TC39 Signals
- State-Driven Reset: Navigation follows state machine transition rules