Skip to content

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

License: MIT Version


Installation

Terminal window
pnpm add @xmachines/play

This package requires Node.js >= 22.0.0. Every package is an ES module ("type": "module").


Overview

@xmachines/play is the base package of the XMachines ecosystem. It defines the smallest set of types and utilities that every other @xmachines/* package builds on:

  • PlayEvent<TPayload> — the universal event contract for Actor ↔ Infrastructure communication
  • PlayError — the typed base class for all @xmachines/* runtime errors
  • NonNullableError — the error that a package throws when a required value is null or undefined
  • assertNonNullable() — the assertion utility that narrows T | null | undefined to T

These protocols implement the architectural invariants that the Play RFC defines:

#InvariantDescription
INV-01Actor AuthorityThe Actor is the final authority. Guards decide all transitions
INV-02Strict SeparationThe business logic never imports a UI framework or a routing library
INV-04Passive InfrastructureThe infrastructure observes the Actor signals. It never enforces a guard
INV-05Signal-Only ReactivityTC39 Signals are the only medium that crosses a boundary

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
const invalid: LoginEvent = { type: "auth.login" }; // Error!

PlayError

Base class for every @xmachines/* runtime error. 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 { PlayError, assertNonNullable } from "@xmachines/play";
import { NonNullableError } from "@xmachines/play/errors";
try {
assertNonNullable(document.getElementById("app"), "#app");
} 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?)

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";
// DOM element lookup — no intermediate variable or `!` needed:
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
NonNullableErrorclassassertNonNullable throws it when a value is null or undefined
assertNonNullablefunctionAsserts that the value is not null. It returns the narrowed value

Exported from @xmachines/play/errors

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

Error Codes

CodeClassThrown When
PLAY_NON_NULLABLENonNullableErrorassertNonNullable() receives null or undefined

Every other @xmachines/* package exports its own error subclasses from its ./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-vue-router@xmachines/play-vue-router/errors

Testing

Run tests for this package in isolation:

Terminal window
pnpm --filter @xmachines/play test

Or from the package directory:

Terminal window
pnpm 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 - the core protocol layer

This package defines the architectural contracts that carry the communication between the Actor and the infrastructure, with no direct dependency between them. RFC section 5.2 gives these protocols. They are the base of the loose coupling between the business logic and the runtime adapters.

The exports

PlayEvent - the generic event type of the Actor communication

  • It is every object with a type: string property
  • The generic TPayload parameter gives a type-safe event shape, and it is optional
  • The default is Record<string, unknown>, which accepts each shape
  • It is framework-agnostic, and it is not bound to XState or to another library

Use:

// Flexible, the default:
const event: PlayEvent = { type: "auth.login", userId: "123" };
// Type-safe, with the generic parameter:
type LoginEvent = PlayEvent<{ userId: string }>;
const event: LoginEvent = { type: "auth.login", userId: "123" };

The common event patterns:

  • A domain event: { type: 'auth.login', userId: '123' }
  • Your own event: { type: 'form.submit', data: {...} }

The routing events come from @xmachines/play-router:

  • PlayRouteEvent: the routing event with the parameters and the target state ID
  • RouterBridge: the protocol that connects a router adapter to an actor

The browser navigation: a router adapter handles the browser BACK and FORWARD buttons, through the popstate event. The user presses BACK or FORWARD, the router detects the new URL, and it sends a PlayRouteEvent to the actor. The actor then checks the event.

import type { PlayRouteEvent, RouterBridge } from "@xmachines/play-router";

The architectural invariants

These protocols enforce the invariants below:

  1. Actor Authority: the infrastructure makes a request, and the Actor decides the validity
  2. Strict Separation: no layer depends on another layer directly
  3. Passive Infrastructure: the infrastructure observes the Actor signals. It never controls them
  4. Signal-Only Reactivity: every state change goes through a TC39 Signal
  5. State-Driven Reset: each navigation follows the transition rules of the state machine

Classes

Type Aliases

Functions