@xmachines/play-actor 1.0.0-beta.46 → 1.0.0-beta.47
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.
- package/README.md +130 -155
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,215 +1,190 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
Per [Play RFC](../docs/rfc/play.md), this package implements:
|
|
3
|
+
# @xmachines/play-actor
|
|
12
4
|
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15
|
+
**Peer dependencies** — install alongside the package:
|
|
27
16
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
21
|
+
## Overview
|
|
37
22
|
|
|
38
|
-
- `
|
|
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
|
-
|
|
25
|
+
The core protocol is deliberately minimal:
|
|
43
26
|
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
actor
|
|
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
|
-
|
|
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
|
|
42
|
+
## API Summary
|
|
61
43
|
|
|
62
|
-
### AbstractActor<TLogic
|
|
44
|
+
### `AbstractActor<TLogic, TEvent>`
|
|
63
45
|
|
|
64
|
-
Abstract base class
|
|
46
|
+
Abstract base class extending XState `Actor<TLogic>`.
|
|
65
47
|
|
|
66
|
-
|
|
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
|
-
|
|
53
|
+
class MyActor extends AbstractActor<AnyActorLogic> {
|
|
54
|
+
// Required: reactive state signal
|
|
55
|
+
state = new Signal.State({});
|
|
69
56
|
|
|
70
|
-
|
|
57
|
+
// Required: typed event dispatch
|
|
58
|
+
send = (event: { type: string }) => {
|
|
59
|
+
/* dispatch to XState */
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
```
|
|
71
63
|
|
|
72
|
-
|
|
64
|
+
With a typed event union:
|
|
73
65
|
|
|
74
|
-
|
|
66
|
+
```ts
|
|
67
|
+
type AuthEvent = { type: "auth.login"; username: string } | { type: "auth.logout" };
|
|
75
68
|
|
|
76
|
-
|
|
69
|
+
class AuthActor extends AbstractActor<AnyActorLogic, AuthEvent> {
|
|
70
|
+
state = new Signal.State({ isAuthenticated: false, username: null });
|
|
77
71
|
|
|
78
|
-
|
|
72
|
+
send = (event: AuthEvent) => {
|
|
73
|
+
/* dispatch */
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
```
|
|
79
77
|
|
|
80
|
-
|
|
78
|
+
### `typedSpec<TContext>(spec)`
|
|
81
79
|
|
|
82
|
-
|
|
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
|
-
|
|
82
|
+
```ts
|
|
83
|
+
import { typedSpec } from "@xmachines/play-actor";
|
|
88
84
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
{
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
104
|
+
### `PlaySpec`
|
|
122
105
|
|
|
123
|
-
|
|
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
|
-
```
|
|
126
|
-
import {
|
|
127
|
-
import { Signal } from "@xmachines/play-signals";
|
|
108
|
+
```ts
|
|
109
|
+
import type { PlaySpec } from "@xmachines/play-actor";
|
|
128
110
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
###
|
|
120
|
+
### `Routable`
|
|
153
121
|
|
|
154
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
139
|
+
### `Viewable`
|
|
175
140
|
|
|
176
|
-
|
|
141
|
+
Interface for actors that expose a renderable view signal.
|
|
177
142
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
+
```ts
|
|
158
|
+
import type { BaseActorProviderProps } from "@xmachines/play-actor";
|
|
159
|
+
import type { DefineRegistryResult } from "@json-render/react";
|
|
194
160
|
|
|
195
|
-
|
|
161
|
+
interface ActorProviderProps extends BaseActorProviderProps<DefineRegistryResult> {
|
|
162
|
+
fallback?: React.ReactNode;
|
|
163
|
+
children: React.ReactNode;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
196
166
|
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
171
|
+
## Testing
|
|
205
172
|
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
186
|
+
## Requirements
|
|
213
187
|
|
|
214
|
-
|
|
215
|
-
|
|
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.
|
|
3
|
+
"version": "1.0.0-beta.47",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Abstract Actor base class for XMachines Play Architecture",
|
|
6
6
|
"keywords": [
|
|
@@ -43,15 +43,15 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^25.6.0",
|
|
46
|
-
"@xmachines/shared": "1.0.0-beta.
|
|
46
|
+
"@xmachines/shared": "1.0.0-beta.47",
|
|
47
47
|
"oxfmt": "^0.45.0",
|
|
48
48
|
"oxlint": "^1.60.0",
|
|
49
49
|
"vitest": "^4.1.4",
|
|
50
50
|
"xstate": "^5.30.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@xmachines/play": "1.0.0-beta.
|
|
54
|
-
"@xmachines/play-signals": "1.0.0-beta.
|
|
53
|
+
"@xmachines/play": "1.0.0-beta.47",
|
|
54
|
+
"@xmachines/play-signals": "1.0.0-beta.47",
|
|
55
55
|
"xstate": "^5.30.0"
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|