Skip to content

@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.

License: MIT npm

Part of the XMachines JS monorepo.


Installation

Terminal window
npm install @xmachines/play

Node.js >= 22.0.0 required. 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 communication
  • PlayError — the typed base class for all @xmachines/* runtime errors
  • NonNullableError — thrown when a required value is null or undefined
  • assertNonNullable() — assertion utility that narrows T | null | undefined to T

These protocols implement the architectural invariants defined in the Play RFC:

#InvariantDescription
INV-01Actor AuthorityThe Actor is the final authority; guards decide all transitions
INV-02Strict SeparationBusiness logic never imports UI frameworks or routing libraries
INV-04Passive InfrastructureInfrastructure observes Actor signals; it never enforces guards
INV-05Signal-Only ReactivityTC39 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 field
const 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

ExportKindDescription
PlayEvent<TPayload>typeUniversal event contract — { type: string } & TPayload
PlayErrorclassBase class for all @xmachines/* typed errors
NonNullableErrorclassThrown by assertNonNullable when a value is null/undefined
assertNonNullablefunctionAsserts non-null, returns narrowed value

Exported from @xmachines/play/errors

ExportKindDescription
PlayErrorclassRe-exported base error class
NonNullableErrorclassscope: "assertNonNullable", code: "PLAY_NON_NULLABLE"

Error Codes

CodeClassThrown When
PLAY_NON_NULLABLENonNullableErrorassertNonNullable() receives null or undefined

Other @xmachines/* packages export their own error subclasses from their respective ./errors subpath:

PackageImport 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:

Terminal window
npm test -w @xmachines/play

Or from the package directory:

Terminal window
npm test

Tests 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 - Generic event type for Actor communication

  • Any object with a type: string property
  • Generic TPayload parameter 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:

  1. Actor Authority: Infrastructure proposes intents, Actor decides validity
  2. Strict Separation: No direct dependencies between layers
  3. Passive Infrastructure: Infrastructure observes Actor signals, never controls
  4. Signal-Only Reactivity: All state changes flow through TC39 Signals
  5. State-Driven Reset: Navigation follows state machine transition rules

Classes

Type Aliases

Functions