@xmachines/play-actor 1.0.0-beta.5 → 1.0.0-beta.50

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mikael Karon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,218 +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 [RFC Play v1](https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.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)).
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)
17
+ ```bash
18
+ npm install xstate @xmachines/play @xmachines/play-signals
19
+ ```
31
20
 
32
- **Peer dependencies:**
21
+ ## Overview
33
22
 
34
- - `xstate` ^5.0.0 - State machine runtime (XState compatibility)
35
- - `@xmachines/play-signals` - TC39 Signals primitives
36
- - `@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).
37
24
 
38
- ## Quick Start
25
+ The core protocol is deliberately minimal:
39
26
 
40
- **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 |
41
31
 
42
- ```typescript
43
- import { definePlayer } from "@xmachines/play-xstate";
32
+ Optional capabilities are declared as separate interfaces — a concrete actor opts in only to what it needs:
44
33
 
45
- // definePlayer returns PlayerActor (extends AbstractActor)
46
- const createPlayer = definePlayer({ machine, catalog });
47
- const actor = createPlayer();
48
- 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 |
49
39
 
50
- // Signal protocol properties (from AbstractActor)
51
- console.log(actor.state.get()); // Current snapshot
52
- console.log(actor.currentRoute.get()); // Derived route
53
- console.log(actor.currentView.get()); // Derived view structure
54
- ```
40
+ Concrete implementations are created by adapters such as [`@xmachines/play-xstate`](../play-xstate/README.md).
55
41
 
56
- ## API Reference
42
+ ## API Summary
57
43
 
58
- ### AbstractActor<TLogic>
44
+ ### `AbstractActor<TLogic, TEvent>`
59
45
 
60
- Abstract base class defining signal protocol:
46
+ Abstract base class extending XState `Actor<TLogic>`.
61
47
 
62
- **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";
63
52
 
64
- - `state: Signal.State<any>` - Reactive snapshot of current state
65
- - `currentRoute: Signal.Computed<string | null>` - Derived navigation path
66
- - `currentView: Signal.Computed<Record<string, any> | null>` - Derived UI structure
67
- - `catalog: any` - Component catalog
53
+ class MyActor extends AbstractActor<AnyActorLogic> {
54
+ // Required: reactive state signal
55
+ state = new Signal.State({});
68
56
 
69
- **Inherited from XState Actor:**
57
+ // Required: typed event dispatch
58
+ send = (event: { type: string }) => {
59
+ /* dispatch to XState */
60
+ };
61
+ }
62
+ ```
70
63
 
71
- - `send(event: PlayEvent): void` - Send event to actor
72
- - `start(): void` - Start the actor
73
- - `stop(): void` - Stop the actor
74
- - `getSnapshot(): Snapshot` - Get current XState snapshot
64
+ With a typed event union:
75
65
 
76
- **Example implementation pattern:**
66
+ ```ts
67
+ type AuthEvent = { type: "auth.login"; username: string } | { type: "auth.logout" };
77
68
 
78
- ```typescript
79
- import { AbstractActor } from "@xmachines/play-actor";
80
- import { Signal } from "@xmachines/play-signals";
69
+ class AuthActor extends AbstractActor<AnyActorLogic, AuthEvent> {
70
+ state = new Signal.State({ isAuthenticated: false, username: null });
81
71
 
82
- class PlayerActor<TLogic extends AnyActorLogic> extends AbstractActor<TLogic> {
83
- // Implement required signal properties
84
- state = new Signal.State(this.internalActor.getSnapshot());
72
+ send = (event: AuthEvent) => {
73
+ /* dispatch */
74
+ };
75
+ }
76
+ ```
85
77
 
86
- currentRoute = new Signal.Computed(() => {
87
- const snapshot = this.state.get();
88
- return deriveRoute(snapshot);
89
- });
78
+ ### `typedSpec<TContext>(spec)`
90
79
 
91
- currentView = new Signal.Computed(() => {
92
- const snapshot = this.state.get();
93
- return snapshot.meta?.view ?? null;
94
- });
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.
95
81
 
96
- catalog = this.config.catalog;
82
+ ```ts
83
+ import { typedSpec } from "@xmachines/play-actor";
97
84
 
98
- // Internal XState actor
99
- private internalActor: Actor<TLogic>;
85
+ interface DashboardCtx {
86
+ username: string;
87
+ params: Record<string, string>;
88
+ query: Record<string, string>;
89
+ }
100
90
 
101
- constructor(logic: TLogic, catalog: any) {
102
- super(logic);
103
- this.internalActor = createActor(logic);
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
+ }),
101
+ }
102
+ ```
104
103
 
105
- // Subscribe to XState transitions
106
- this.internalActor.subscribe((snapshot) => {
107
- this.state.set(snapshot); // Update signal
108
- });
109
- }
104
+ ### `PlaySpec`
110
105
 
111
- override start(): void {
112
- this.internalActor.start();
113
- }
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.
114
107
 
115
- override stop(): void {
116
- this.internalActor.stop();
117
- }
108
+ ```ts
109
+ import type { PlaySpec } from "@xmachines/play-actor";
118
110
 
119
- override send(event: PlayEvent): void {
120
- this.internalActor.send(event as any);
121
- }
122
- }
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
+ };
123
118
  ```
124
119
 
125
- **Complete API:** See [API Documentation](../../docs/api/@xmachines/play-actor)
120
+ ### `Routable`
126
121
 
127
- ## Examples
122
+ Interface for actors that support routing.
128
123
 
129
- ### Infrastructure Observing Signals
130
-
131
- ```typescript
132
- import { AbstractActor } from "@xmachines/play-actor";
124
+ ```ts
125
+ import type { Routable } from "@xmachines/play-actor";
133
126
  import { Signal } from "@xmachines/play-signals";
134
127
 
135
- function syncUrlToActor(actor: AbstractActor<any>) {
136
- // Infrastructure passively observes actor's route signal
137
- const watcher = new Signal.subtle.Watcher(() => {
138
- queueMicrotask(() => {
139
- const pending = watcher.getPending();
140
- if (pending.length > 0) {
141
- const route = actor.currentRoute.get();
142
- if (route !== null) {
143
- // Update browser URL (Passive Infrastructure)
144
- window.history.replaceState(null, "", route);
145
- }
146
- watcher.watch(...pending); // Re-watch
147
- }
148
- });
149
- });
150
-
151
- watcher.watch(actor.currentRoute);
152
- actor.currentRoute.get(); // Initial read
153
-
154
- return () => watcher.unwatch(actor.currentRoute);
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 */
135
+ };
155
136
  }
156
137
  ```
157
138
 
158
- ### Browser Navigation Sending Events
159
-
160
- ```typescript
161
- import { AbstractActor } from "@xmachines/play-actor";
139
+ ### `Viewable`
162
140
 
163
- function connectBrowserNavigation(actor: AbstractActor<any>) {
164
- const handlePopstate = () => {
165
- const path = window.location.pathname;
141
+ Interface for actors that expose a renderable view signal.
166
142
 
167
- // Browser event sent to actor (Actor Authority)
168
- // Actor guards decide if navigation is valid
169
- actor.send({ type: "play.route", to: path });
170
- };
171
-
172
- window.addEventListener("popstate", handlePopstate);
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";
173
147
 
174
- return () => {
175
- window.removeEventListener("popstate", handlePopstate);
176
- };
177
- }
148
+ // currentView carries PlaySpec | null
149
+ const signal = new Signal.State<PlaySpec | null>(null);
150
+ const viewable: Viewable = { currentView: signal };
178
151
  ```
179
152
 
180
- ## Architecture
153
+ ### `BaseActorProviderProps<TRegistry>`
181
154
 
182
- This base class enforces three architectural invariants:
155
+ Framework-agnostic base props shared by every `ActorProvider` implementation (React, Vue, Solid, Svelte). Framework renderer packages extend this interface.
183
156
 
184
- 1. **Actor Authority (INV-01):**
185
- - Actor decides all state transitions via guards
186
- - Infrastructure sends events, actor validates and processes
187
- - Actor's decision is final — no override by infrastructure
157
+ ```ts
158
+ import type { BaseActorProviderProps } from "@xmachines/play-actor";
159
+ import type { DefineRegistryResult } from "@json-render/react";
188
160
 
189
- 2. **Signal-Only Reactivity (INV-05):**
190
- - All reactive state exposed via TC39 Signals
191
- - Infrastructure uses `Signal.subtle.Watcher` to observe
192
- - No direct queries (`getSnapshot()` for internal use only)
161
+ interface ActorProviderProps extends BaseActorProviderProps<DefineRegistryResult> {
162
+ fallback?: React.ReactNode;
163
+ children: React.ReactNode;
164
+ }
165
+ ```
193
166
 
194
- 3. **Passive Infrastructure (INV-04):**
195
- - Infrastructure reflects actor state (via signals)
196
- - Infrastructure never decides transitions
197
- - Browser/router events sent as commands to actor
167
+ ### `BaseViewContextValue<TRegistry>`
198
168
 
199
- ## XState Compatibility
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.
200
170
 
201
- `AbstractActor` extends XState's `Actor<TLogic>` to maintain:
171
+ ## Testing
202
172
 
203
- - **Type Safety:** Generic `TLogic extends AnyActorLogic` parameter
204
- - **Inspection API:** XState Inspector can attach to actors
205
- - **DevTools Integration:** Standard XState devtools work
206
- - **Ecosystem Tools:** Works with XState visualization, testing libraries
173
+ Run the test suite for this package in isolation:
207
174
 
208
- **Snapshot Format:** Standard XState snapshots (state + context) remain unchanged — signals are accessible via actor properties, not snapshots.
175
+ ```bash
176
+ # From the package directory
177
+ npm test
209
178
 
210
- ## Related Packages
179
+ # From the monorepo root (workspace-scoped)
180
+ npm test -w packages/play-actor
211
181
 
212
- - **[@xmachines/play-xstate](../play-xstate)** - Concrete PlayerActor implementation
213
- - **[@xmachines/play-signals](../play-signals)** - TC39 Signals primitives
214
- - **[@xmachines/play](../play)** - Protocol types (PlayEvent, RouterBridge)
182
+ # Watch mode
183
+ npm run test:watch -w packages/play-actor
184
+ ```
215
185
 
216
- ## License
186
+ ## Requirements
217
187
 
218
- MIT
188
+ - **Node.js** `>=22.0.0`
189
+ - **TypeScript** `>=5.7` (strict mode)
190
+ - **ESM only** — `"type": "module"`
@@ -16,189 +16,169 @@
16
16
  *
17
17
  * @packageDocumentation
18
18
  */
19
- import { Actor, type AnyActorLogic } from "xstate";
19
+ import { Actor, type AnyActorLogic, type EventObject } from "xstate";
20
20
  import type { Signal } from "@xmachines/play-signals";
21
+ import type { Spec, StateStore, RenderErrorHandler, ActionHandler } from "@json-render/core";
21
22
  /**
22
23
  * Optional capability: Routing support
24
+ */
25
+ export interface Routable {
26
+ readonly currentRoute: Signal.Computed<string | null>;
27
+ readonly initialRoute: string | null;
28
+ }
29
+ /**
30
+ * XMachines extension of `@json-render/core` `Spec`.
23
31
  *
24
- * Actors implementing this interface can derive a route from their state.
25
- * Router adapters observe the currentRoute signal to sync browser URLs.
32
+ * Adds `contextProps` an explicit allowlist of machine context fields that
33
+ * `deriveCurrentView` merges into element props as low-priority slots. Only
34
+ * fields named here are ever exposed to components; nothing leaks from context
35
+ * without an opt-in declaration.
26
36
  *
27
- * @example
28
- * ```typescript
29
- * class MyActor extends AbstractActor implements Routable {
30
- * currentRoute = new Signal.Computed(() => deriveRoute(this.state.get()));
31
- * }
32
- *
33
- * // Router requires Routable
34
- * function connectRouter<T extends AbstractActor & Routable>(actor: T) {
35
- * watcher.watch(actor.currentRoute);
36
- * }
37
- * ```
37
+ * Use `typedSpec<TContext>(...)` at the definition site to validate `contextProps`
38
+ * entries against your machine's context type at compile time.
38
39
  */
39
- export interface Routable {
40
+ export interface PlaySpec extends Spec {
40
41
  /**
41
- * Current route signal
42
- *
43
- * Computed signal derived from state machine. Infrastructure observes to sync browser URL.
44
- *
45
- * Invariant: Passive Infrastructure - Infrastructure reflects route, never decides.
42
+ * Explicit allowlist of machine context field names to expose as prop slots.
43
+ * Each named field is merged into every spec element's `props` at view derivation
44
+ * time, filling any slot whose current value is `undefined`.
46
45
  *
47
- * @example
48
- * ```typescript
49
- * const watcher = new Signal.subtle.Watcher(() => {
50
- * const route = actor.currentRoute.get();
51
- * console.log('Route changed:', route);
52
- * });
53
- * watcher.watch(actor.currentRoute);
54
- * ```
46
+ * Use `typedSpec<TContext>(...)` to constrain entries to `keyof TContext & string`.
55
47
  */
56
- readonly currentRoute: Signal.Computed<string | null>;
48
+ readonly contextProps?: readonly string[];
57
49
  }
58
50
  /**
59
- * Optional capability: View rendering support
51
+ * Identity helper that constrains a `PlaySpec` object's `contextProps` to keys
52
+ * of a specific machine context type, giving compile-time validation and IDE
53
+ * autocomplete at the definition site.
54
+ *
55
+ * XState's `meta` field is typed as `Record<string, unknown>`, so TypeScript
56
+ * cannot infer the constraint from context. `typedSpec<MyCtx>(...)` is the
57
+ * opt-in mechanism that activates enforcement where the spec is written.
60
58
  *
61
- * Actors implementing this interface can derive view structures from their state.
62
- * Renderers observe the currentView signal to update the UI.
59
+ * At runtime this is a no-op the spec object is returned unchanged.
63
60
  *
64
61
  * @example
65
- * ```typescript
66
- * class MyActor extends AbstractActor implements Viewable {
67
- * currentView = new Signal.State(null);
68
- * catalog = { HomePage: HomeComponent };
62
+ * ```ts
63
+ * interface DashboardCtx {
64
+ * username: string;
65
+ * params: Record<string, string>;
66
+ * query: Record<string, string>;
69
67
  * }
70
68
  *
71
- * // Renderer requires Viewable
72
- * function renderView<T extends AbstractActor & Viewable>(actor: T) {
73
- * const view = actor.currentView.get();
74
- * return catalog[view.component];
69
+ * meta: {
70
+ * view: typedSpec<DashboardCtx>({
71
+ * root: "root",
72
+ * contextProps: ["username"], // ✓ key of DashboardCtx
73
+ * // contextProps: ["usernaem"], // ✗ compile error
74
+ * elements: { root: { type: "Dashboard", props: {}, children: [] } },
75
+ * }),
75
76
  * }
76
77
  * ```
77
78
  */
79
+ export declare function typedSpec<TContext extends object>(spec: Omit<PlaySpec, "contextProps"> & {
80
+ readonly contextProps?: readonly (keyof TContext & string)[];
81
+ }): PlaySpec;
82
+ /**
83
+ * Actor capability for exposing renderable view state.
84
+ *
85
+ * `Viewable` marks actors that publish a `currentView` signal.
86
+ * Renderers such as `PlayRenderer` consume this contract to resolve the
87
+ * current view description into concrete UI without embedding view logic inside the
88
+ * framework adapter.
89
+ */
78
90
  export interface Viewable {
79
91
  /**
80
- * Current view signal
81
- *
82
- * State signal containing UI structure schema from meta.view. Infrastructure renders view.
83
- *
84
- * Invariant: Logic-Driven UI - View structure is defined by business logic, not JSX.
85
- *
86
- * @example
87
- * ```typescript
88
- * const watcher = new Signal.subtle.Watcher(() => {
89
- * const view = actor.currentView.get();
90
- * console.log('View changed:', view);
91
- * });
92
- * watcher.watch(actor.currentView);
93
- * ```
94
- */
95
- readonly currentView: Signal.State<any>;
96
- /**
97
- * Component catalog for view resolution
92
+ * Current view signal. Contains the json-render PlaySpec for the current machine
93
+ * state, or null when no view is active.
98
94
  *
99
- * Maps component names to actual component implementations.
100
- * Used by renderers to resolve view.component to actual UI components.
95
+ * Infrastructure renders view Logic-Driven UI invariant.
101
96
  */
102
- readonly catalog: any;
97
+ readonly currentView: Signal.State<PlaySpec | null>;
103
98
  }
104
99
  /**
105
- * Abstract base class for Play Architecture actors.
106
- *
107
- * Extends XState Actor to maintain ecosystem compatibility (inspection, devtools)
108
- * while enforcing minimal signal protocol for Actor ↔ Infrastructure communication.
109
- *
110
- * The core protocol contains only:
111
- * - state: Reactive state snapshot
112
- * - send: Event dispatch method
100
+ * Framework-agnostic base for every framework's `ViewContextValue`.
113
101
  *
114
- * Optional capabilities (routing, view rendering) are provided via interfaces:
115
- * - Implement Routable for routing support
116
- * - Implement Viewable for view rendering support
102
+ * Holds the three fields that are identical across React, Vue, Solid, and Svelte.
103
+ * `registry` is framework-specific (each framework has its own `ComponentRegistry`
104
+ * type) so it is typed via `TRegistry` — the same generic used in `BaseActorProviderProps`.
117
105
  *
118
- * Concrete implementations created by @xmachines/play-xstate adapter.
106
+ * @typeParam TRegistry - The framework's component registry type (e.g. `ComponentRegistry` from `@json-render/react`).
107
+ */
108
+ export interface BaseViewContextValue<TRegistry extends object> {
109
+ /** The current PlaySpec to render. */
110
+ spec: PlaySpec;
111
+ /** Action handlers resolved against the live StateStore. */
112
+ handlers: Record<string, ActionHandler>;
113
+ /** Component registry from registryResult.registry. */
114
+ registry: TRegistry;
115
+ /** The active StateStore — pass to JSONUIProvider/JsonUIProvider as `store` to share state across providers. */
116
+ store: StateStore;
117
+ }
118
+ /**
119
+ * Framework-agnostic base props shared by every `ActorProvider` implementation
120
+ * (React, Vue, Solid, Svelte). `TRegistry` captures the framework-specific
121
+ * `DefineRegistryResult` type; `RenderErrorHandler` is sourced from
122
+ * `@json-render/core` so no second generic is needed.
119
123
  *
120
- * @typeParam TLogic - XState actor logic type (maintains type safety)
124
+ * Framework packages extend this with their `fallback`, `onError`, and `children` fields.
121
125
  *
122
- * Invariant: Actor Authority - Actor is the sole source of truth for state transitions.
123
- * Invariant: Signal-Only Reactivity - Infrastructure observes via TC39 Signals.
124
- * Invariant: Passive Infrastructure - Infrastructure reflects, never decides.
126
+ * @typeParam TRegistry - The framework's `DefineRegistryResult` type.
125
127
  *
126
128
  * @example
127
- * Simple actor (no routing, no view)
128
- * ```typescript
129
- * class SimpleActor extends AbstractActor<any> {
130
- * state = new Signal.State({...});
131
- * send(event) { ... }
132
- * }
133
- * ```
129
+ * ```ts
130
+ * import type { BaseActorProviderProps } from "@xmachines/play-actor";
131
+ * import type { DefineRegistryResult } from "@json-render/react";
134
132
  *
135
- * @example
136
- * Routable actor
137
- * ```typescript
138
- * class RoutableActor extends AbstractActor<any> implements Routable {
139
- * state = new Signal.State({...});
140
- * currentRoute = new Signal.Computed(() => deriveRoute(this.state.get()));
141
- * send(event) { ... }
133
+ * interface ActorProviderProps extends BaseActorProviderProps<DefineRegistryResult> {
134
+ * fallback?: React.ReactNode;
135
+ * children: React.ReactNode;
142
136
  * }
143
137
  * ```
138
+ */
139
+ export interface BaseActorProviderProps<TRegistry extends {
140
+ registry: object;
141
+ handlers: (...args: never[]) => unknown;
142
+ }> {
143
+ /** Actor instance with currentView signal (requires Viewable capability). */
144
+ actor: AbstractActor<AnyActorLogic> & Viewable;
145
+ /** Full result from defineRegistry() — contains the component registry and action handlers factory. */
146
+ registryResult: TRegistry;
147
+ /**
148
+ * Optional external StateStore (controlled mode).
149
+ * When provided, spec.state is ignored and this store is the single source of truth.
150
+ * When omitted, a fresh @xstate/store atom is created per view transition from spec.state.
151
+ */
152
+ store?: StateStore;
153
+ /**
154
+ * Called when an individual catalog component throws during render.
155
+ * Takes precedence over any onRenderError set via defineRegistry.
156
+ */
157
+ onRenderError?: RenderErrorHandler;
158
+ }
159
+ /**
160
+ * Abstract base class for Play Architecture actors.
144
161
  *
145
- * @example
146
- * Full-featured actor (routing + view)
147
- * ```typescript
148
- * class PlayerActor extends AbstractActor<any> implements Routable, Viewable {
149
- * state = new Signal.State({...});
150
- * currentRoute = new Signal.Computed(() => deriveRoute(this.state.get()));
151
- * currentView = new Signal.State(null);
152
- * catalog = {};
153
- * send(event) { ... }
154
- * }
155
- * ```
162
+ * Provides signal-driven state observation that integrates with XState ecosystem
163
+ * tooling (devtools, inspection) while exposing reactive signals for
164
+ * Infrastructure layer communication.
156
165
  *
157
- * @see {@link https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md#53-actor-protocol | RFC Play v1 Section 5.3}
158
- * @see {@link Routable} for routing capability
159
- * @see {@link Viewable} for view rendering capability
166
+ * @typeParam TLogic - XState actor logic type
167
+ * @typeParam TEvent - Event type constraint (defaults to EventObject)
160
168
  */
161
- export declare abstract class AbstractActor<TLogic extends AnyActorLogic> extends Actor<TLogic> {
169
+ export declare abstract class AbstractActor<TLogic extends AnyActorLogic, TEvent extends EventObject = EventObject> extends Actor<TLogic> {
162
170
  /**
163
171
  * Reactive snapshot of current actor state.
164
172
  *
165
173
  * Infrastructure observes this signal to react to state changes without
166
- * directly coupling to the Actor's internal state machine implementation.
167
- *
168
- * @example
169
- * ```typescript
170
- * // Infrastructure observes state signal
171
- * const watcher = new Signal.subtle.Watcher(() => {
172
- * console.log('Actor state changed:', actor.state.get());
173
- * });
174
- * watcher.watch(actor.state);
175
- * ```
174
+ * directly coupling to the actor's internal state machine implementation.
176
175
  */
177
- abstract state: Signal.State<any>;
176
+ abstract state: Signal.State<unknown>;
178
177
  /**
179
- * Send event to Actor
180
- *
181
- * Infrastructure forwards user intents (navigation, domain events, custom events)
182
- * as events to the Actor. The Actor's state machine guards determine whether
183
- * each event is valid from the current state.
184
- *
185
- * @param event - Event object with type property (e.g., PlayEvent, PlayRouteEvent)
186
- *
187
- * Invariant: Actor Authority - Only Actor decides whether an event is valid.
188
- *
189
- * @example
190
- * ```typescript
191
- * // Infrastructure forwards user intent
192
- * actor.send({ type: 'auth.login', userId: '123' });
193
- * // Actor's guards determine if event is allowed
194
- * ```
178
+ * Send event to Actor.
195
179
  *
196
- * @remarks
197
- * Accepts any event object with a type property. Core events (PlayEvent) are in
198
- * @xmachines/play, routing events (PlayRouteEvent) are in @xmachines/play-router.
180
+ * Constrained to TEvent for type safety in concrete implementations.
199
181
  */
200
- abstract send(event: {
201
- readonly type: string;
202
- } & Record<string, any>): void;
182
+ abstract send(event: TEvent): void;
203
183
  }
204
184
  //# sourceMappingURL=abstract-actor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"abstract-actor.d.ts","sourceRoot":"","sources":["../src/abstract-actor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,KAAK,EAAE,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AACnD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,QAAQ;IACxB;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACtD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,QAAQ;IACxB;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAExC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,8BAAsB,aAAa,CAAC,MAAM,SAAS,aAAa,CAAE,SAAQ,KAAK,CAAC,MAAM,CAAC;IACtF;;;;;;;;;;;;;;OAcG;IACH,SAAgB,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAEzC;;;;;;;;;;;;;;;;;;;;;OAqBG;aACsB,IAAI,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;CAC3F"}
1
+ {"version":3,"file":"abstract-actor.d.ts","sourceRoot":"","sources":["../src/abstract-actor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,KAAK,EAAE,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAE7F;;GAEG;AACH,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACtD,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,QAAS,SAAQ,IAAI;IACrC;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,SAAS,CAAC,QAAQ,SAAS,MAAM,EAChD,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,cAAc,CAAC,GAAG;IACtC,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,EAAE,CAAC;CAC7D,GACC,QAAQ,CAEV;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;CACpD;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,oBAAoB,CAAC,SAAS,SAAS,MAAM;IAC7D,sCAAsC;IACtC,IAAI,EAAE,QAAQ,CAAC;IACf,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACxC,uDAAuD;IACvD,QAAQ,EAAE,SAAS,CAAC;IACpB,gHAAgH;IAChH,KAAK,EAAE,UAAU,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,sBAAsB,CACtC,SAAS,SAAS;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAA;CAAE;IAE/E,6EAA6E;IAC7E,KAAK,EAAE,aAAa,CAAC,aAAa,CAAC,GAAG,QAAQ,CAAC;IAC/C,uGAAuG;IACvG,cAAc,EAAE,SAAS,CAAC;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB;;;OAGG;IACH,aAAa,CAAC,EAAE,kBAAkB,CAAC;CACnC;AAED;;;;;;;;;GASG;AACH,8BAAsB,aAAa,CAClC,MAAM,SAAS,aAAa,EAC5B,MAAM,SAAS,WAAW,GAAG,WAAW,CACvC,SAAQ,KAAK,CAAC,MAAM,CAAC;IACtB;;;;;OAKG;IACH,SAAgB,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAE7C;;;;OAIG;aACsB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;CAClD"}
@@ -18,61 +18,46 @@
18
18
  */
19
19
  import { Actor } from "xstate";
20
20
  /**
21
- * Abstract base class for Play Architecture actors.
22
- *
23
- * Extends XState Actor to maintain ecosystem compatibility (inspection, devtools)
24
- * while enforcing minimal signal protocol for Actor ↔ Infrastructure communication.
25
- *
26
- * The core protocol contains only:
27
- * - state: Reactive state snapshot
28
- * - send: Event dispatch method
29
- *
30
- * Optional capabilities (routing, view rendering) are provided via interfaces:
31
- * - Implement Routable for routing support
32
- * - Implement Viewable for view rendering support
21
+ * Identity helper that constrains a `PlaySpec` object's `contextProps` to keys
22
+ * of a specific machine context type, giving compile-time validation and IDE
23
+ * autocomplete at the definition site.
33
24
  *
34
- * Concrete implementations created by @xmachines/play-xstate adapter.
25
+ * XState's `meta` field is typed as `Record<string, unknown>`, so TypeScript
26
+ * cannot infer the constraint from context. `typedSpec<MyCtx>(...)` is the
27
+ * opt-in mechanism that activates enforcement where the spec is written.
35
28
  *
36
- * @typeParam TLogic - XState actor logic type (maintains type safety)
37
- *
38
- * Invariant: Actor Authority - Actor is the sole source of truth for state transitions.
39
- * Invariant: Signal-Only Reactivity - Infrastructure observes via TC39 Signals.
40
- * Invariant: Passive Infrastructure - Infrastructure reflects, never decides.
29
+ * At runtime this is a no-op the spec object is returned unchanged.
41
30
  *
42
31
  * @example
43
- * Simple actor (no routing, no view)
44
- * ```typescript
45
- * class SimpleActor extends AbstractActor<any> {
46
- * state = new Signal.State({...});
47
- * send(event) { ... }
32
+ * ```ts
33
+ * interface DashboardCtx {
34
+ * username: string;
35
+ * params: Record<string, string>;
36
+ * query: Record<string, string>;
48
37
  * }
49
- * ```
50
38
  *
51
- * @example
52
- * Routable actor
53
- * ```typescript
54
- * class RoutableActor extends AbstractActor<any> implements Routable {
55
- * state = new Signal.State({...});
56
- * currentRoute = new Signal.Computed(() => deriveRoute(this.state.get()));
57
- * send(event) { ... }
39
+ * meta: {
40
+ * view: typedSpec<DashboardCtx>({
41
+ * root: "root",
42
+ * contextProps: ["username"], // key of DashboardCtx
43
+ * // contextProps: ["usernaem"], // ✗ compile error
44
+ * elements: { root: { type: "Dashboard", props: {}, children: [] } },
45
+ * }),
58
46
  * }
59
47
  * ```
48
+ */
49
+ export function typedSpec(spec) {
50
+ return spec;
51
+ }
52
+ /**
53
+ * Abstract base class for Play Architecture actors.
60
54
  *
61
- * @example
62
- * Full-featured actor (routing + view)
63
- * ```typescript
64
- * class PlayerActor extends AbstractActor<any> implements Routable, Viewable {
65
- * state = new Signal.State({...});
66
- * currentRoute = new Signal.Computed(() => deriveRoute(this.state.get()));
67
- * currentView = new Signal.State(null);
68
- * catalog = {};
69
- * send(event) { ... }
70
- * }
71
- * ```
55
+ * Provides signal-driven state observation that integrates with XState ecosystem
56
+ * tooling (devtools, inspection) while exposing reactive signals for
57
+ * Infrastructure layer communication.
72
58
  *
73
- * @see {@link https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md#53-actor-protocol | RFC Play v1 Section 5.3}
74
- * @see {@link Routable} for routing capability
75
- * @see {@link Viewable} for view rendering capability
59
+ * @typeParam TLogic - XState actor logic type
60
+ * @typeParam TEvent - Event type constraint (defaults to EventObject)
76
61
  */
77
62
  export class AbstractActor extends Actor {
78
63
  }
@@ -1 +1 @@
1
- {"version":3,"file":"abstract-actor.js","sourceRoot":"","sources":["../src/abstract-actor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,KAAK,EAAsB,MAAM,QAAQ,CAAC;AAyFnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,MAAM,OAAgB,aAA4C,SAAQ,KAAa;CAyCtF"}
1
+ {"version":3,"file":"abstract-actor.js","sourceRoot":"","sources":["../src/abstract-actor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,KAAK,EAAwC,MAAM,QAAQ,CAAC;AAkCrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,SAAS,CACxB,IAEC;IAED,OAAO,IAAgB,CAAC;AACzB,CAAC;AAiFD;;;;;;;;;GASG;AACH,MAAM,OAAgB,aAGpB,SAAQ,KAAa;CAetB"}
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * reactive signals for Infrastructure layer communication.
14
14
  *
15
15
  * @packageDocumentation
16
- * @see {@link https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md#53-actor-protocol | RFC Play v1 Section 5.3}
16
+ * @see [Play RFC](../../docs/rfc/play.md)
17
17
  */
18
- export { AbstractActor, type Routable, type Viewable } from "./abstract-actor.js";
18
+ export { AbstractActor, typedSpec, type Routable, type Viewable, type PlaySpec, type BaseActorProviderProps, type BaseViewContextValue, } from "./abstract-actor.js";
19
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EACN,aAAa,EACb,SAAS,EACT,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,GACzB,MAAM,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * reactive signals for Infrastructure layer communication.
14
14
  *
15
15
  * @packageDocumentation
16
- * @see {@link https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md#53-actor-protocol | RFC Play v1 Section 5.3}
16
+ * @see [Play RFC](../../docs/rfc/play.md)
17
17
  */
18
- export { AbstractActor } from "./abstract-actor.js";
18
+ export { AbstractActor, typedSpec, } from "./abstract-actor.js";
19
19
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,aAAa,EAAgC,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EACN,aAAa,EACb,SAAS,GAMT,MAAM,qBAAqB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmachines/play-actor",
3
- "version": "1.0.0-beta.5",
3
+ "version": "1.0.0-beta.50",
4
4
  "private": false,
5
5
  "description": "Abstract Actor base class for XMachines Play Architecture",
6
6
  "keywords": [
@@ -20,6 +20,7 @@
20
20
  "type": "module",
21
21
  "exports": {
22
22
  ".": {
23
+ "source": "./src/index.ts",
23
24
  "types": "./dist/index.d.ts",
24
25
  "import": "./dist/index.js"
25
26
  }
@@ -29,26 +30,32 @@
29
30
  },
30
31
  "scripts": {
31
32
  "build": "tsc --build",
32
- "clean": "rm -rf dist *.tsbuildinfo",
33
- "typecheck": "tsc --noEmit",
34
- "test": "vitest run",
33
+ "clean": "rm -rf dist *.tsbuildinfo coverage node_modules/.svelte2tsx-*",
34
+ "test": "vitest",
35
35
  "lint": "oxlint .",
36
36
  "lint:fix": "oxlint --fix .",
37
37
  "format": "oxfmt .",
38
38
  "format:check": "oxfmt --check .",
39
39
  "prepublishOnly": "npm run build"
40
40
  },
41
+ "dependencies": {
42
+ "@json-render/core": "^0.18.0"
43
+ },
41
44
  "devDependencies": {
42
- "@types/node": "^25.5.0",
43
- "@xmachines/shared": "1.0.0-beta.5",
44
- "xstate": "^5.28.0"
45
+ "@types/node": "^25.6.0",
46
+ "@xmachines/shared": "1.0.0-beta.50",
47
+ "oxfmt": "^0.47.0",
48
+ "oxlint": "^1.62.0",
49
+ "vitest": "^4.1.5",
50
+ "xstate": "^5.31.0"
45
51
  },
46
52
  "peerDependencies": {
47
- "@xmachines/play": "1.0.0-beta.5",
48
- "@xmachines/play-signals": "1.0.0-beta.5",
49
- "xstate": "^5.28.0"
53
+ "@xmachines/play": "1.0.0-beta.50",
54
+ "@xmachines/play-signals": "1.0.0-beta.50",
55
+ "xstate": "^5.31.0"
50
56
  },
51
57
  "engines": {
52
58
  "node": ">=22.0.0"
53
- }
59
+ },
60
+ "_devDependencies_note": "xstate appears in both peerDependencies and devDependencies intentionally. devDependencies provides workspace resolution for local builds and tests. peerDependencies declares the consumer version constraint. Both are pinned to ^5.30.0 to prevent drift."
54
61
  }