npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xmachines/play-actor

v2.1.1

Published

Abstract Actor base class for XMachines Play Architecture

Readme

@xmachines/play-actor

Abstract Actor base class for XMachines Play Architecture.

License: MIT Version

Installation

pnpm add @xmachines/play-actor

Peer dependencies. Install them with the package:

pnpm add xstate @xmachines/play @xmachines/play-signals @xmachines/json-render-core

Overview

@xmachines/play-actor gives you AbstractActor, a minimal base class. The class extends the XState Actor class, and it enforces the signal protocol of the Play Architecture (RFC section 5.3). It exposes reactive TC39 Signals for the infrastructure layer. It also keeps the complete compatibility with the XState ecosystem, which includes the devtools and the inspection.

The core protocol is small on purpose:

| Property | Type | Description | | -------- | ------------------------- | ---------------------------------------- | | state | Signal.State<unknown> | Reactive snapshot of current actor state | | send | (event: TEvent) => void | Event dispatch method |

Separate interfaces declare the optional capabilities. A concrete actor implements only the interfaces that it needs:

| Interface | Property | Description | | ---------- | ----------------------------------------------- | ------------------------------------- | | Routable | currentRoute: Signal.Computed<string \| null> | Current route path derived from state | | Routable | initialRoute: string \| null | The route where the actor starts | | Viewable | currentView: Signal.State<PlaySpec \| null> | Current JSON-render view spec |

An adapter, such as @xmachines/play-xstate, makes the concrete implementations.

API Summary

AbstractActor<TLogic, TEvent>

The abstract base class extends the XState Actor<TLogic> class.

A subclass is the actor. Give the logic and its options to super(), so that one instance holds the running machine. Reach the send method of XState through the prototype. This class declares send as abstract for one reason only: to narrow the event type. TypeScript forbids a super call to an abstract member.

import { AbstractActor } from "@xmachines/play-actor";
import { Signal } from "@xmachines/play-signals";
import { Actor, type ActorOptions, type AnyActorLogic } from "xstate";

class MyActor extends AbstractActor<AnyActorLogic> {
	// Required: reactive state signal
	state: Signal.State<unknown>;

	constructor(logic: AnyActorLogic, options?: ActorOptions<AnyActorLogic>) {
		super(logic, options);
		this.state = new Signal.State(this.getSnapshot());
		super.subscribe((snapshot) => this.state.set(snapshot));
	}

	// Required: typed event dispatch
	override send(event: { type: string }): void {
		Actor.prototype.send.call(this, event);
	}
}

With a typed event union:

// imports as in the previous example
type AuthEvent = { type: "auth.login"; username: string } | { type: "auth.logout" };

class AuthActor extends AbstractActor<AnyActorLogic, AuthEvent> {
	state = new Signal.State({ isAuthenticated: false, username: null });

	override send(event: AuthEvent): void {
		Actor.prototype.send.call(this, event);
	}
}

typedSpec(spec)

This identity helper gives a view-spec literal the type PlaySpec at the definition site. The XState meta field has the type Record<string, unknown>. Therefore this helper is the place where the spec shape receives the compile-time check and the IDE autocomplete. The helper has no cost at run time.

import { typedSpec } from "@xmachines/play-actor";

// In an XState machine meta block:
meta: {
  view: typedSpec({
    root: "root",
    elements: {
      root: {
        type: "Dashboard",
        props: { username: { $state: "/context/username" } },
        children: [],
      },
    },
  }),
}

PlaySpec

This type extends the Spec type of @xmachines/json-render-core. Each derived view receives the complete machine context in its state store, under the read-only /context subtree. A spec therefore reads the context through the ordinary { $state: "/context/…" } grammar: in a prop, in a visible condition, and in repeat.statePath.

/context is read-only by design. Nothing can write to it. The machine context changes through an event only. A $bindState write or a setState write under /context throws an error, and the error names the event to send. This is the model: the bindable ephemeral state is at the root of the store, from spec.state; the domain state is in the machine, and it changes through events that are meaningful and easy to inspect.

The path shows the origin of each value. /context/params/username comes from the URL. /context/username belongs to the machine. One value can never hide the other.

The model projects everything, and this has two consequences. The first consequence is exposure. The complete context is visible to the client in the view store, which includes a debug panel, an inspector, and a validator. The context is a client-side value in each case, so keep a secret out of it.

The second consequence is emission granularity. The emit gate compares the context field by field, at the top level only. Therefore an event that changes any field emits the view again with the same viewKey. A provider refreshes /context in the live store, and it does not seed the store again. The component does not remount, and the ephemeral view state and the focus stay. However, a new emission is still a render pass in the framework layer. Keep high-frequency ephemeral data, such as a draft for each keystroke or a timer, in the view store (spec.state with $bindState) or in a child actor. The domain state belongs in the context. A keystroke does not.

import type { PlaySpec } from "@xmachines/play-actor";

const spec: PlaySpec = {
	root: "root",
	elements: {
		root: {
			type: "Profile",
			props: { username: { $state: "/context/username" } },
			children: [],
		},
	},
};

Historical note: an earlier version had a contextProps field. At first the field drove an implicit prop-enrichment pass. That pass merged the allowlisted context fields and the URL params into the props of every element. We removed it, because it put values into components that never asked for them, and it let URL data from the user hide machine-owned state. The field was then a projection filter for a short time. We removed that filter too, because a limit on what a view can read added machinery without a real problem to solve. Always validate the derived view (actor.currentView.get()), not the raw meta.view. The state of the derived spec carries the projection, so a tool such as validateSpec sees a spec that is consistent with itself.

Routable

Interface for actors that support routing.

import { AbstractActor, type Routable } from "@xmachines/play-actor";
import { Signal } from "@xmachines/play-signals";
import type { AnyActorLogic, EventObject } from "xstate";

// Implement in a concrete actor (note: RoutableActor interface is exported from @xmachines/play-router):
class MyRoutableActor extends AbstractActor<AnyActorLogic> implements Routable {
	state = new Signal.State<{ path?: string }>({});
	currentRoute = new Signal.Computed<string | null>(() => this.state.get().path ?? null);
	initialRoute = "/";
	override send(event: EventObject): void {
		/* dispatch */
	}
}

Viewable

Interface for actors that expose a renderable view signal.

import type { Viewable } from "@xmachines/play-actor";
import type { PlaySpec } from "@xmachines/play-actor";
import { Signal } from "@xmachines/play-signals";

// currentView carries PlaySpec | null
const signal = new Signal.State<PlaySpec | null>(null);
const viewable: Viewable = { currentView: signal };

BaseActorProviderProps<TRegistry>

The framework-agnostic base props. Every ActorProvider implementation shares them: React, Vue, Solid, and Svelte. Each framework renderer package extends this interface.

import type { BaseActorProviderProps } from "@xmachines/play-actor";
import type { DefineRegistryResult } from "@xmachines/json-render-react";

interface ActorProviderProps extends BaseActorProviderProps<DefineRegistryResult> {
	fallback?: React.ReactNode;
	children: React.ReactNode;
}

BaseViewContextValue<TRegistry>

The framework-agnostic base of the ViewContextValue type in each framework. It holds the spec, handlers, registry, and store fields. These fields are identical in React, Vue, Solid, and Svelte.

Testing

Run the test suite for this package in isolation:

# From the package directory
pnpm test

# From the monorepo root (workspace-scoped)
pnpm --filter @xmachines/play-actor test

# Watch mode
pnpm --filter @xmachines/play-actor run test:watch

Requirements

  • Node.js >=22.0.0
  • TypeScript >=5.7 (strict mode)
  • ESM only"type": "module"