@xmachines/play
API / @xmachines/play
Core protocol layer for the Universal Player Architecture. It defines
PlayEvent,PlayError, and the contracts that keep the business logic loosely coupled to the runtime adapters.
Installation
pnpm add @xmachines/play @xmachines/corePeer dependencies. Install them with the package:
@xmachines/core— the base of the errors.PlayErrorextendsBaseErrorof itserrorssubpath.
This package requires Node.js
>= 24.0.0. Every package is an ES module ("type": "module").
Overview
@xmachines/play is the protocol package of the XMachines ecosystem. It defines the smallest set of types and utilities that every other Play package builds on:
PlayEvent<TPayload>— the universal event contract for Actor ↔ Infrastructure communicationPlayError— the typed base class for the runtime errors of every Play package. It extendsBaseErrorof@xmachines/coreNonNullableError— the error that a package throws when a required value isnullorundefinedassertNonNullable()— the assertion utility that narrowsT | null | undefinedtoT
These protocols implement the architectural invariants that the Play RFC defines:
| # | Invariant | Description |
|---|---|---|
| INV-01 | Actor Authority | The Actor is the final authority. Guards decide all transitions |
| INV-02 | Strict Separation | The business logic never imports a UI framework or a routing library |
| INV-03 | Atom-Only Reactivity | Atoms are the only medium that crosses a boundary |
| INV-04 | Passive Infrastructure | The infrastructure observes the Actor atoms. It never enforces a guard |
| INV-05 | State-Driven Reset | An invalid external navigation is overwritten by the state of the Actor |
Usage
PlayEvent<TPayload>
The minimal event contract is any object with a type: string property. The contract is framework-agnostic. It works with XState, with Robot, and with every 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// @ts-expect-error -- `userId` and `timestamp` are missingconst invalid: LoginEvent = { type: "auth.login" };PlayError
Base class for the runtime errors of every Play package. It extends BaseError of @xmachines/core/errors. Each error has a stable scope (the class or the module that throws it) and a stable code (a machine-readable identifier). Always branch on .code or on the subclass. Never branch on .message.
import { assertNonNullable } from "@xmachines/play";import { NonNullableError, PlayError } from "@xmachines/play/errors";
const settings = new Map<string, string>([["apiUrl", "https://api.example.com"]]);
try { assertNonNullable(settings.get("timeout"), "timeout");} 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) { // Every other error of a Play package console.error(`[${err.scope}:${err.code}] ${err.message}`); } else { throw err; }}Extend PlayError in your own @xmachines/*-compatible packages:
import { PlayError } from "@xmachines/play/errors";
export class MyPackageError extends PlayError { constructor(message: string, options?: ErrorOptions) { super("MyScope", "MY_PACKAGE_ERROR_CODE", message, options); this.name = "MyPackageError"; }}assertNonNullable(value, name?)
The assertion utility returns value with the type NonNullable<V>, or it throws NonNullableError. It removes the unsafe ! non-null assertion.
import { assertNonNullable } from "@xmachines/play";
const settings = new Map<string, string>([["apiUrl", "https://api.example.com"]]);
// A lookup that can miss — no intermediate variable or `!` needed:const apiUrl = assertNonNullable(settings.get("apiUrl"), "apiUrl");The release protocol
The release protocol moved to @xmachines/core/utils. A subscription of the ecosystem hands back a plain release, and asDisposable of that package publishes the release for a using scope.
API Summary
Exported from @xmachines/play
| Export | Kind | Description |
|---|---|---|
PlayEvent<TPayload> | type | Universal event contract — { type: string } & TPayload |
assertNonNullable | function | Asserts that the value is not null. It returns the narrowed value |
Exported from @xmachines/play/errors
| Export | Kind | Description |
|---|---|---|
PlayError | class | The base class of every typed error of a Play package |
NonNullableError | class | scope: "assertNonNullable", code: "PLAY_NON_NULLABLE" |
Error Codes
| Code | Class | Thrown When |
|---|---|---|
PLAY_NON_NULLABLE | NonNullableError | assertNonNullable() receives null or undefined |
Every other Play package exports its own error subclasses from its ./errors subpath. @xmachines/core/errors holds BaseError, the base of PlayError:
License
MIT © Mikael Karon
See LICENSE for details.