@xmachines/play
v2.0.0
Published
Universal Player Architecture for XMachines
Maintainers
Readme
@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
pnpm add @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 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
| 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:
pnpm --filter @xmachines/play testOr from the package directory:
pnpm testTests use Vitest and cover the PlayError class construction, inheritance, cause support, and subclassing patterns.
License
MIT © Mikael Karon
See LICENSE for details.
