@xmachines/play-actor 1.0.0-beta.46 → 1.0.0-beta.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +130 -155
  2. package/package.json +9 -9
package/README.md CHANGED
@@ -1,215 +1,190 @@
1
- # @xmachines/play-actor
2
-
3
- **Abstract Actor base class with signal protocol for XMachines Play Architecture**
4
-
5
- Foundation for all actor implementations, enforcing XState compatibility and reactive signal contracts.
6
-
7
- ## Overview
1
+ <!-- generated-by: gsd-doc-writer -->
8
2
 
9
- `@xmachines/play-actor` provides `AbstractActor`, a base class that extends XState's `Actor` while enforcing the Play Architecture's signal protocol. It maintains XState ecosystem compatibility (inspection tools, devtools) while exposing reactive signals for infrastructure layer communication.
10
-
11
- Per [Play RFC](../docs/rfc/play.md), this package implements:
3
+ # @xmachines/play-actor
12
4
 
13
- - **Actor Authority (INV-01):** Actor is sole source of truth for state transitions
14
- - **Signal-Only Reactivity (INV-05):** Infrastructure observes via TC39 Signals, never directly queries
15
- - **Passive Infrastructure (INV-04):** Infrastructure reflects, never decides
5
+ Abstract Actor base class for XMachines Play Architecture.
16
6
 
17
- **Note:** This is an abstract base class. Concrete implementations are provided by adapters (see [@xmachines/play-xstate](../play-xstate/README.md)).
7
+ Part of the [xmachines-js monorepo](../../README.md).
18
8
 
19
9
  ## Installation
20
10
 
21
11
  ```bash
22
- npm install xstate@^5.0.0
23
12
  npm install @xmachines/play-actor
24
13
  ```
25
14
 
26
- ## Current Exports
15
+ **Peer dependencies** — install alongside the package:
27
16
 
28
- - `AbstractActor`
29
- - `Routable` (type)
30
- - `Viewable` (type)
31
- - `PlaySpec` (type)
32
- - `typedSpec`
33
- - `BaseActorProviderProps` (type)
34
- - `BaseViewContextValue` (type)
17
+ ```bash
18
+ npm install xstate @xmachines/play @xmachines/play-signals
19
+ ```
35
20
 
36
- **Peer dependencies:**
21
+ ## Overview
37
22
 
38
- - `xstate` ^5.0.0 State machine runtime (XState compatibility)
39
- - `@xmachines/play-signals` - TC39 Signals primitives
40
- - `@xmachines/play` - Protocol types (PlayEvent, etc.)
23
+ `@xmachines/play-actor` provides `AbstractActor`, a minimal base class that extends the XState `Actor` class while enforcing the Play Architecture's **signal protocol** (RFC section 5.3). It exposes reactive TC39 Signals for infrastructure-layer communication while preserving full XState ecosystem compatibility (devtools, inspection).
41
24
 
42
- ## Quick Start
25
+ The core protocol is deliberately minimal:
43
26
 
44
- **Usage:** This is an abstract base class — use concrete implementations:
27
+ | Property | Type | Description |
28
+ | -------- | ------------------------- | ---------------------------------------- |
29
+ | `state` | `Signal.State<unknown>` | Reactive snapshot of current actor state |
30
+ | `send` | `(event: TEvent) => void` | Event dispatch method |
45
31
 
46
- ```typescript
47
- import { definePlayer } from "@xmachines/play-xstate";
32
+ Optional capabilities are declared as separate interfaces — a concrete actor opts in only to what it needs:
48
33
 
49
- // definePlayer returns PlayerActor (extends AbstractActor)
50
- const createPlayer = definePlayer({ machine });
51
- const actor = createPlayer();
52
- actor.start();
34
+ | Interface | Property | Description |
35
+ | ---------- | ----------------------------------------------- | ------------------------------------- |
36
+ | `Routable` | `currentRoute: Signal.Computed<string \| null>` | Current route path derived from state |
37
+ | `Routable` | `initialRoute: string \| null` | Route the actor starts on |
38
+ | `Viewable` | `currentView: Signal.State<PlaySpec \| null>` | Current JSON-render view spec |
53
39
 
54
- // Signal protocol properties (from AbstractActor)
55
- console.log(actor.state.get()); // Current snapshot
56
- console.log(actor.currentRoute.get()); // Derived route
57
- console.log(actor.currentView.get()); // Derived view structure
58
- ```
40
+ Concrete implementations are created by adapters such as [`@xmachines/play-xstate`](../play-xstate/README.md).
59
41
 
60
- ## API Reference
42
+ ## API Summary
61
43
 
62
- ### AbstractActor<TLogic>
44
+ ### `AbstractActor<TLogic, TEvent>`
63
45
 
64
- Abstract base class defining signal protocol:
46
+ Abstract base class extending XState `Actor<TLogic>`.
65
47
 
66
- **Abstract Properties (must implement):**
48
+ ```ts
49
+ import { AbstractActor } from "@xmachines/play-actor";
50
+ import { Signal } from "@xmachines/play-signals";
51
+ import type { AnyActorLogic } from "xstate";
67
52
 
68
- - `state: Signal.State<unknown>` - Reactive snapshot of current state
53
+ class MyActor extends AbstractActor<AnyActorLogic> {
54
+ // Required: reactive state signal
55
+ state = new Signal.State({});
69
56
 
70
- **Optional capability interfaces:**
57
+ // Required: typed event dispatch
58
+ send = (event: { type: string }) => {
59
+ /* dispatch to XState */
60
+ };
61
+ }
62
+ ```
71
63
 
72
- Implement `Routable` to add routing support:
64
+ With a typed event union:
73
65
 
74
- - `currentRoute: Signal.Computed<string | null>` - Derived navigation path
66
+ ```ts
67
+ type AuthEvent = { type: "auth.login"; username: string } | { type: "auth.logout" };
75
68
 
76
- Implement `Viewable` to add view rendering support:
69
+ class AuthActor extends AbstractActor<AnyActorLogic, AuthEvent> {
70
+ state = new Signal.State({ isAuthenticated: false, username: null });
77
71
 
78
- - `currentView: Signal.State<PlaySpec | null>` - Current view spec (updated on every state transition). `PlaySpec` is a `@json-render/core` spec object (`{ root, elements }`) that drives the renderer directly.
72
+ send = (event: AuthEvent) => {
73
+ /* dispatch */
74
+ };
75
+ }
76
+ ```
79
77
 
80
- **Inherited from XState Actor:**
78
+ ### `typedSpec<TContext>(spec)`
81
79
 
82
- - `send(event): void` - Send event to actor
83
- - `start(): void` - Start the actor
84
- - `stop(): void` - Stop the actor
85
- - `getSnapshot()` - Get current XState snapshot (typed as `SnapshotFrom<TLogic>`)
80
+ Identity helper that constrains a `PlaySpec` object's `contextProps` to keys of a specific machine context type. This enables compile-time validation and IDE autocomplete without any runtime cost.
86
81
 
87
- **Example implementation pattern:**
82
+ ```ts
83
+ import { typedSpec } from "@xmachines/play-actor";
88
84
 
89
- ```typescript
90
- import { AbstractActor, type Routable, type Viewable, type PlaySpec } from "@xmachines/play-actor";
91
- import { Signal } from "@xmachines/play-signals";
92
- import type { AnyActorLogic, AnyMachineSnapshot } from "xstate";
93
-
94
- class PlayerActor<TLogic extends AnyActorLogic>
95
- extends AbstractActor<TLogic>
96
- implements Routable, Viewable
97
- {
98
- // Required: reactive state snapshot
99
- state = new Signal.State<AnyMachineSnapshot>(this.getSnapshot() as AnyMachineSnapshot);
100
-
101
- // Routable: derived navigation path
102
- currentRoute = new Signal.Computed(() => {
103
- return deriveRoute(this.state.get());
104
- });
105
-
106
- // Viewable: current view spec — Signal.State, updated on every state transition
107
- currentView = new Signal.State<PlaySpec | null>(null);
108
-
109
- constructor(logic: TLogic) {
110
- super(logic);
111
-
112
- // Subscribe to XState transitions and update signals
113
- this.subscribe((snapshot) => {
114
- this.state.set(snapshot as AnyMachineSnapshot);
115
- // Derive currentView from snapshot meta and update the signal...
116
- });
117
- }
85
+ interface DashboardCtx {
86
+ username: string;
87
+ params: Record<string, string>;
88
+ query: Record<string, string>;
89
+ }
90
+
91
+ // In an XState machine meta block:
92
+ meta: {
93
+ view: typedSpec<DashboardCtx>({
94
+ root: "root",
95
+ contextProps: ["username"], // key of DashboardCtx
96
+ // contextProps: ["usernaem"], // ✗ compile error
97
+ elements: {
98
+ root: { type: "Dashboard", props: {}, children: [] },
99
+ },
100
+ }),
118
101
  }
119
102
  ```
120
103
 
121
- ## Examples
104
+ ### `PlaySpec`
122
105
 
123
- ### Infrastructure Observing Signals
106
+ Extends `@json-render/core`'s `Spec` with an optional `contextProps` field — an explicit allowlist of machine context fields that are merged into element props at view derivation time.
124
107
 
125
- ```typescript
126
- import { AbstractActor } from "@xmachines/play-actor";
127
- import { Signal } from "@xmachines/play-signals";
108
+ ```ts
109
+ import type { PlaySpec } from "@xmachines/play-actor";
128
110
 
129
- function syncUrlToActor(actor: AbstractActor<any>) {
130
- // Infrastructure passively observes actor's route signal
131
- const watcher = new Signal.subtle.Watcher(() => {
132
- queueMicrotask(() => {
133
- const pending = watcher.getPending();
134
- if (pending.length > 0) {
135
- const route = actor.currentRoute.get();
136
- if (route !== null) {
137
- // Update browser URL (Passive Infrastructure)
138
- window.history.replaceState(null, "", route);
139
- }
140
- watcher.watch(...pending); // Re-watch
141
- }
142
- });
143
- });
144
-
145
- watcher.watch(actor.currentRoute);
146
- actor.currentRoute.get(); // Initial read
147
-
148
- return () => watcher.unwatch(actor.currentRoute);
149
- }
111
+ const spec: PlaySpec = {
112
+ root: "root",
113
+ contextProps: ["username"], // only these keys are exposed to components
114
+ elements: {
115
+ root: { type: "Profile", props: { username: undefined }, children: [] },
116
+ },
117
+ };
150
118
  ```
151
119
 
152
- ### Browser Navigation Sending Events
120
+ ### `Routable`
153
121
 
154
- ```typescript
155
- import { AbstractActor } from "@xmachines/play-actor";
156
-
157
- function connectBrowserNavigation(actor: AbstractActor<any>) {
158
- const handlePopstate = () => {
159
- const path = window.location.pathname;
122
+ Interface for actors that support routing.
160
123
 
161
- // Browser event sent to actor (Actor Authority)
162
- // Actor guards decide if navigation is valid
163
- actor.send({ type: "play.route", to: path });
164
- };
165
-
166
- window.addEventListener("popstate", handlePopstate);
124
+ ```ts
125
+ import type { Routable } from "@xmachines/play-actor";
126
+ import { Signal } from "@xmachines/play-signals";
167
127
 
168
- return () => {
169
- window.removeEventListener("popstate", handlePopstate);
128
+ // Implement in a concrete actor:
129
+ class RoutableActor extends AbstractActor<AnyActorLogic> implements Routable {
130
+ state = new Signal.State({});
131
+ currentRoute = new Signal.Computed(() => this.state.get().path ?? null);
132
+ initialRoute = "/";
133
+ send = (event) => {
134
+ /* dispatch */
170
135
  };
171
136
  }
172
137
  ```
173
138
 
174
- ## Architecture
139
+ ### `Viewable`
175
140
 
176
- This base class enforces three architectural invariants:
141
+ Interface for actors that expose a renderable view signal.
177
142
 
178
- 1. **Actor Authority (INV-01):**
179
- - Actor decides all state transitions via guards
180
- - Infrastructure sends events, actor validates and processes
181
- - Actor's decision is final — no override by infrastructure
143
+ ```ts
144
+ import type { Viewable } from "@xmachines/play-actor";
145
+ import type { PlaySpec } from "@xmachines/play-actor";
146
+ import { Signal } from "@xmachines/play-signals";
147
+
148
+ // currentView carries PlaySpec | null
149
+ const signal = new Signal.State<PlaySpec | null>(null);
150
+ const viewable: Viewable = { currentView: signal };
151
+ ```
182
152
 
183
- 2. **Signal-Only Reactivity (INV-05):**
184
- - All reactive state exposed via TC39 Signals
185
- - Infrastructure uses `Signal.subtle.Watcher` to observe
186
- - No direct queries (`getSnapshot()` for internal use only)
153
+ ### `BaseActorProviderProps<TRegistry>`
187
154
 
188
- 3. **Passive Infrastructure (INV-04):**
189
- - Infrastructure reflects actor state (via signals)
190
- - Infrastructure never decides transitions
191
- - Browser/router events sent as commands to actor
155
+ Framework-agnostic base props shared by every `ActorProvider` implementation (React, Vue, Solid, Svelte). Framework renderer packages extend this interface.
192
156
 
193
- ## XState Compatibility
157
+ ```ts
158
+ import type { BaseActorProviderProps } from "@xmachines/play-actor";
159
+ import type { DefineRegistryResult } from "@json-render/react";
194
160
 
195
- `AbstractActor` extends XState's `Actor<TLogic>` to maintain:
161
+ interface ActorProviderProps extends BaseActorProviderProps<DefineRegistryResult> {
162
+ fallback?: React.ReactNode;
163
+ children: React.ReactNode;
164
+ }
165
+ ```
196
166
 
197
- - **Type Safety:** Generic `TLogic extends AnyActorLogic` parameter
198
- - **Inspection API:** XState Inspector can attach to actors
199
- - **DevTools Integration:** Standard XState devtools work
200
- - **Ecosystem Tools:** Works with XState visualization, testing libraries
167
+ ### `BaseViewContextValue<TRegistry>`
201
168
 
202
- **Snapshot Format:** Standard XState snapshots (state + context) remain unchanged signals are accessible via actor properties, not snapshots.
169
+ Framework-agnostic base for every framework's `ViewContextValue`. Holds `spec`, `handlers`, `registry`, and `store` fields that are identical across React, Vue, Solid, and Svelte.
203
170
 
204
- ## Related Packages
171
+ ## Testing
205
172
 
206
- - **[@xmachines/play-xstate](../play-xstate/README.md)** - Concrete PlayerActor implementation
207
- - **[@xmachines/play-signals](../play-signals/README.md)** - TC39 Signals primitives
208
- - **[@xmachines/play](../play/README.md)** - Protocol types (PlayEvent, RouterBridge)
173
+ Run the test suite for this package in isolation:
209
174
 
210
- ## License
175
+ ```bash
176
+ # From the package directory
177
+ npm test
178
+
179
+ # From the monorepo root (workspace-scoped)
180
+ npm test -w packages/play-actor
181
+
182
+ # Watch mode
183
+ npm run test:watch -w packages/play-actor
184
+ ```
211
185
 
212
- Copyright (c) 2016 [Mikael Karon](mailto:mikael@karon.se). All rights reserved.
186
+ ## Requirements
213
187
 
214
- This work is licensed under the terms of the MIT license.
215
- For a copy, see <https://opensource.org/licenses/MIT>.
188
+ - **Node.js** `>=22.0.0`
189
+ - **TypeScript** `>=5.7` (strict mode)
190
+ - **ESM only** — `"type": "module"`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmachines/play-actor",
3
- "version": "1.0.0-beta.46",
3
+ "version": "1.0.0-beta.48",
4
4
  "private": false,
5
5
  "description": "Abstract Actor base class for XMachines Play Architecture",
6
6
  "keywords": [
@@ -43,16 +43,16 @@
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^25.6.0",
46
- "@xmachines/shared": "1.0.0-beta.46",
47
- "oxfmt": "^0.45.0",
48
- "oxlint": "^1.60.0",
49
- "vitest": "^4.1.4",
50
- "xstate": "^5.30.0"
46
+ "@xmachines/shared": "1.0.0-beta.48",
47
+ "oxfmt": "^0.47.0",
48
+ "oxlint": "^1.62.0",
49
+ "vitest": "^4.1.5",
50
+ "xstate": "^5.31.0"
51
51
  },
52
52
  "peerDependencies": {
53
- "@xmachines/play": "1.0.0-beta.46",
54
- "@xmachines/play-signals": "1.0.0-beta.46",
55
- "xstate": "^5.30.0"
53
+ "@xmachines/play": "1.0.0-beta.48",
54
+ "@xmachines/play-signals": "1.0.0-beta.48",
55
+ "xstate": "^5.31.0"
56
56
  },
57
57
  "engines": {
58
58
  "node": ">=22.0.0"