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 @xmachines/core

Peer dependencies. Install them with the package:

  • @xmachines/core — the base of the errors. PlayError extends BaseError of its errors subpath.

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 communication
  • PlayError — the typed base class for the runtime errors of every Play package. It extends BaseError of @xmachines/core
  • 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-03Atom-Only ReactivityAtoms are the only medium that crosses a boundary
INV-04Passive InfrastructureThe infrastructure observes the Actor atoms. It never enforces a guard
INV-05State-Driven ResetAn 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 missing
const 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

ExportKindDescription
PlayEvent<TPayload>typeUniversal event contract — { type: string } & TPayload
assertNonNullablefunctionAsserts that the value is not null. It returns the narrowed value

Exported from @xmachines/play/errors

ExportKindDescription
PlayErrorclassThe base class of every typed error of a Play package
NonNullableErrorclassscope: "assertNonNullable", code: "PLAY_NON_NULLABLE"

Error Codes

CodeClassThrown When
PLAY_NON_NULLABLENonNullableErrorassertNonNullable() 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:

PackageImport path
@xmachines/play@xmachines/play/errors
@xmachines/play-router@xmachines/play-router/errors
@xmachines/play-url@xmachines/play-url/errors
@xmachines/play-view@xmachines/play-view/errors
@xmachines/play-xstate@xmachines/play-xstate/errors
@xmachines/play-vue-router@xmachines/play-vue-router/errors

License

MIT © Mikael Karon

See LICENSE for details.

Modules