@xmachines/play-actor 1.0.0-beta.9 → 1.1.0

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,219 +1,202 @@
1
1
  # @xmachines/play-actor
2
2
 
3
- **Abstract Actor base class with signal protocol for XMachines Play Architecture**
3
+ Abstract Actor base class for XMachines Play Architecture.
4
4
 
5
- Foundation for all actor implementations, enforcing XState compatibility and reactive signal contracts.
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Version](https://img.shields.io/badge/version-1.1.0-blue)](https://www.npmjs.com/package/@xmachines/play-actor)
6
7
 
7
- ## Overview
8
-
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:
12
-
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
16
-
17
- **Note:** This is an abstract base class. Concrete implementations are provided by adapters (see [@xmachines/play-xstate](../play-xstate)).
8
+ Part of the [xmachines-js monorepo](../../README.md).
18
9
 
19
10
  ## Installation
20
11
 
21
12
  ```bash
22
- npm install xstate@^5.0.0
23
- npm install @xmachines/play-actor
13
+ pnpm add @xmachines/play-actor
24
14
  ```
25
15
 
26
- ## Current Exports
16
+ **Peer dependencies** — install alongside the package:
27
17
 
28
- - `AbstractActor`
29
- - `Routable` (type)
30
- - `Viewable` (type)
31
-
32
- **Peer dependencies:**
18
+ ```bash
19
+ pnpm add xstate @xmachines/play @xmachines/play-signals
20
+ ```
33
21
 
34
- - `xstate` ^5.0.0 — State machine runtime (XState compatibility)
35
- - `@xmachines/play-signals` - TC39 Signals primitives
36
- - `@xmachines/play` - Protocol types (PlayEvent, etc.)
22
+ ## Overview
37
23
 
38
- ## Quick Start
24
+ `@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).
39
25
 
40
- **Usage:** This is an abstract base class — use concrete implementations:
26
+ The core protocol is deliberately minimal:
41
27
 
42
- ```typescript
43
- import { definePlayer } from "@xmachines/play-xstate";
28
+ | Property | Type | Description |
29
+ | -------- | ------------------------- | ---------------------------------------- |
30
+ | `state` | `Signal.State<unknown>` | Reactive snapshot of current actor state |
31
+ | `send` | `(event: TEvent) => void` | Event dispatch method |
44
32
 
45
- // definePlayer returns PlayerActor (extends AbstractActor)
46
- const createPlayer = definePlayer({ machine, catalog });
47
- const actor = createPlayer();
48
- actor.start();
33
+ Optional capabilities are declared as separate interfaces — a concrete actor opts in only to what it needs:
49
34
 
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
- ```
35
+ | Interface | Property | Description |
36
+ | ---------- | ----------------------------------------------- | ------------------------------------- |
37
+ | `Routable` | `currentRoute: Signal.Computed<string \| null>` | Current route path derived from state |
38
+ | `Routable` | `initialRoute: string \| null` | Route the actor starts on |
39
+ | `Viewable` | `currentView: Signal.State<PlaySpec \| null>` | Current JSON-render view spec |
55
40
 
56
- ## API Reference
41
+ Concrete implementations are created by adapters such as [`@xmachines/play-xstate`](../play-xstate/README.md).
57
42
 
58
- ### AbstractActor<TLogic>
43
+ ## API Summary
59
44
 
60
- Abstract base class defining signal protocol:
45
+ ### `AbstractActor<TLogic, TEvent>`
61
46
 
62
- **Abstract Properties (must implement):**
47
+ Abstract base class extending XState `Actor<TLogic>`.
63
48
 
64
- - `state: Signal.State<unknown>` - Reactive snapshot of current state
49
+ A subclass **is** the actor: hand the logic and its options to `super()` so one
50
+ instance holds the running machine, and reach XState's own `send` through the
51
+ prototype — `send` is declared abstract here only to narrow the event type, and
52
+ TypeScript forbids `super` calls to an abstract member.
65
53
 
66
- **Optional capability interfaces:**
54
+ ```ts
55
+ import { AbstractActor } from "@xmachines/play-actor";
56
+ import { Signal } from "@xmachines/play-signals";
57
+ import { Actor, type ActorOptions, type AnyActorLogic } from "xstate";
58
+
59
+ class MyActor extends AbstractActor<AnyActorLogic> {
60
+ // Required: reactive state signal
61
+ state: Signal.State<unknown>;
62
+
63
+ constructor(logic: AnyActorLogic, options?: ActorOptions<AnyActorLogic>) {
64
+ super(logic, options);
65
+ this.state = new Signal.State(this.getSnapshot());
66
+ super.subscribe((snapshot) => this.state.set(snapshot));
67
+ }
68
+
69
+ // Required: typed event dispatch
70
+ override send(event: { type: string }): void {
71
+ Actor.prototype.send.call(this, event);
72
+ }
73
+ }
74
+ ```
67
75
 
68
- Implement `Routable` to add routing support:
76
+ With a typed event union:
69
77
 
70
- - `currentRoute: Signal.Computed<string | null>` - Derived navigation path
78
+ ```ts
79
+ type AuthEvent = { type: "auth.login"; username: string } | { type: "auth.logout" };
71
80
 
72
- Implement `Viewable` to add view rendering support:
81
+ class AuthActor extends AbstractActor<AnyActorLogic, AuthEvent> {
82
+ state = new Signal.State({ isAuthenticated: false, username: null });
73
83
 
74
- - `currentView: Signal.State<ViewMetadata | null>` - Current UI structure (updated at state entry)
75
- - `catalog: Record<string, unknown>` - Component catalog
84
+ override send(event: AuthEvent): void {
85
+ Actor.prototype.send.call(this, event);
86
+ }
87
+ }
88
+ ```
76
89
 
77
- **Inherited from XState Actor:**
90
+ ### `typedSpec<TContext>(spec)`
78
91
 
79
- - `send(event): void` - Send event to actor
80
- - `start(): void` - Start the actor
81
- - `stop(): void` - Stop the actor
82
- - `getSnapshot()` - Get current XState snapshot (typed as `SnapshotFrom<TLogic>`)
92
+ 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.
83
93
 
84
- **Example implementation pattern:**
94
+ ```ts
95
+ import { typedSpec } from "@xmachines/play-actor";
85
96
 
86
- ```typescript
87
- import { AbstractActor, type Routable, type Viewable, type ViewMetadata } from "@xmachines/play-actor";
88
- import { Signal } from "@xmachines/play-signals";
89
- import type { AnyActorLogic, AnyMachineSnapshot } from "xstate";
90
-
91
- class PlayerActor<TLogic extends AnyActorLogic>
92
- extends AbstractActor<TLogic>
93
- implements Routable, Viewable
94
- {
95
- // Required: reactive state snapshot
96
- state = new Signal.State<AnyMachineSnapshot>(this.getSnapshot() as AnyMachineSnapshot);
97
-
98
- // Routable: derived navigation path
99
- currentRoute = new Signal.Computed(() => {
100
- return deriveRoute(this.state.get());
101
- });
102
-
103
- // Viewable: current UI structure — Signal.State, updated at state entry (not computed)
104
- currentView = new Signal.State<ViewMetadata | null>(null);
105
-
106
- // Viewable: component catalog
107
- catalog: Record<string, unknown>;
108
-
109
- constructor(logic: TLogic, catalog: Record<string, unknown>) {
110
- super(logic);
111
- this.catalog = catalog;
112
-
113
- // Subscribe to XState transitions and update signals
114
- this.subscribe((snapshot) => {
115
- this.state.set(snapshot as AnyMachineSnapshot);
116
- // Update currentView based on snapshot meta...
117
- });
118
- }
97
+ interface DashboardCtx {
98
+ username: string;
99
+ params: Record<string, string>;
100
+ query: Record<string, string>;
119
101
  }
102
+
103
+ // In an XState machine meta block:
104
+ meta: {
105
+ view: typedSpec<DashboardCtx>({
106
+ root: "root",
107
+ contextProps: ["username"], // ✓ key of DashboardCtx
108
+ // contextProps: ["usernaem"], // ✗ compile error
109
+ elements: {
110
+ root: { type: "Dashboard", props: {}, children: [] },
111
+ },
112
+ }),
120
113
  }
121
114
  ```
122
115
 
123
- **Complete API:** See [API Documentation](../../docs/api/@xmachines/play-actor)
116
+ ### `PlaySpec`
124
117
 
125
- ## Examples
118
+ Extends `@xmachines/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.
126
119
 
127
- ### Infrastructure Observing Signals
120
+ ```ts
121
+ import type { PlaySpec } from "@xmachines/play-actor";
128
122
 
129
- ```typescript
130
- import { AbstractActor } from "@xmachines/play-actor";
131
- import { Signal } from "@xmachines/play-signals";
132
-
133
- function syncUrlToActor(actor: AbstractActor<any>) {
134
- // Infrastructure passively observes actor's route signal
135
- const watcher = new Signal.subtle.Watcher(() => {
136
- queueMicrotask(() => {
137
- const pending = watcher.getPending();
138
- if (pending.length > 0) {
139
- const route = actor.currentRoute.get();
140
- if (route !== null) {
141
- // Update browser URL (Passive Infrastructure)
142
- window.history.replaceState(null, "", route);
143
- }
144
- watcher.watch(...pending); // Re-watch
145
- }
146
- });
147
- });
148
-
149
- watcher.watch(actor.currentRoute);
150
- actor.currentRoute.get(); // Initial read
151
-
152
- return () => watcher.unwatch(actor.currentRoute);
153
- }
123
+ const spec: PlaySpec = {
124
+ root: "root",
125
+ contextProps: ["username"], // only these keys are exposed to components
126
+ elements: {
127
+ root: { type: "Profile", props: { username: undefined }, children: [] },
128
+ },
129
+ };
154
130
  ```
155
131
 
156
- ### Browser Navigation Sending Events
132
+ ### `Routable`
157
133
 
158
- ```typescript
159
- import { AbstractActor } from "@xmachines/play-actor";
134
+ Interface for actors that support routing.
160
135
 
161
- function connectBrowserNavigation(actor: AbstractActor<any>) {
162
- const handlePopstate = () => {
163
- const path = window.location.pathname;
164
-
165
- // Browser event sent to actor (Actor Authority)
166
- // Actor guards decide if navigation is valid
167
- actor.send({ type: "play.route", to: path });
168
- };
169
-
170
- window.addEventListener("popstate", handlePopstate);
136
+ ```ts
137
+ import type { Routable } from "@xmachines/play-actor";
138
+ import { Signal } from "@xmachines/play-signals";
171
139
 
172
- return () => {
173
- window.removeEventListener("popstate", handlePopstate);
140
+ // Implement in a concrete actor (note: RoutableActor interface is exported from @xmachines/play-router):
141
+ class MyRoutableActor extends AbstractActor<AnyActorLogic> implements Routable {
142
+ state = new Signal.State({});
143
+ currentRoute = new Signal.Computed(() => this.state.get().path ?? null);
144
+ initialRoute = "/";
145
+ send = (event) => {
146
+ /* dispatch */
174
147
  };
175
148
  }
176
149
  ```
177
150
 
178
- ## Architecture
151
+ ### `Viewable`
179
152
 
180
- This base class enforces three architectural invariants:
153
+ Interface for actors that expose a renderable view signal.
181
154
 
182
- 1. **Actor Authority (INV-01):**
183
- - Actor decides all state transitions via guards
184
- - Infrastructure sends events, actor validates and processes
185
- - Actor's decision is final — no override by infrastructure
155
+ ```ts
156
+ import type { Viewable } from "@xmachines/play-actor";
157
+ import type { PlaySpec } from "@xmachines/play-actor";
158
+ import { Signal } from "@xmachines/play-signals";
159
+
160
+ // currentView carries PlaySpec | null
161
+ const signal = new Signal.State<PlaySpec | null>(null);
162
+ const viewable: Viewable = { currentView: signal };
163
+ ```
186
164
 
187
- 2. **Signal-Only Reactivity (INV-05):**
188
- - All reactive state exposed via TC39 Signals
189
- - Infrastructure uses `Signal.subtle.Watcher` to observe
190
- - No direct queries (`getSnapshot()` for internal use only)
165
+ ### `BaseActorProviderProps<TRegistry>`
191
166
 
192
- 3. **Passive Infrastructure (INV-04):**
193
- - Infrastructure reflects actor state (via signals)
194
- - Infrastructure never decides transitions
195
- - Browser/router events sent as commands to actor
167
+ Framework-agnostic base props shared by every `ActorProvider` implementation (React, Vue, Solid, Svelte). Framework renderer packages extend this interface.
196
168
 
197
- ## XState Compatibility
169
+ ```ts
170
+ import type { BaseActorProviderProps } from "@xmachines/play-actor";
171
+ import type { DefineRegistryResult } from "@xmachines/json-render-react";
198
172
 
199
- `AbstractActor` extends XState's `Actor<TLogic>` to maintain:
173
+ interface ActorProviderProps extends BaseActorProviderProps<DefineRegistryResult> {
174
+ fallback?: React.ReactNode;
175
+ children: React.ReactNode;
176
+ }
177
+ ```
200
178
 
201
- - **Type Safety:** Generic `TLogic extends AnyActorLogic` parameter
202
- - **Inspection API:** XState Inspector can attach to actors
203
- - **DevTools Integration:** Standard XState devtools work
204
- - **Ecosystem Tools:** Works with XState visualization, testing libraries
179
+ ### `BaseViewContextValue<TRegistry>`
205
180
 
206
- **Snapshot Format:** Standard XState snapshots (state + context) remain unchanged signals are accessible via actor properties, not snapshots.
181
+ Framework-agnostic base for every framework's `ViewContextValue`. Holds `spec`, `handlers`, `registry`, and `store` fields that are identical across React, Vue, Solid, and Svelte.
207
182
 
208
- ## Related Packages
183
+ ## Testing
209
184
 
210
- - **[@xmachines/play-xstate](../play-xstate)** - Concrete PlayerActor implementation
211
- - **[@xmachines/play-signals](../play-signals)** - TC39 Signals primitives
212
- - **[@xmachines/play](../play)** - Protocol types (PlayEvent, RouterBridge)
185
+ Run the test suite for this package in isolation:
213
186
 
214
- ## License
187
+ ```bash
188
+ # From the package directory
189
+ pnpm test
190
+
191
+ # From the monorepo root (workspace-scoped)
192
+ pnpm --filter @xmachines/play-actor test
193
+
194
+ # Watch mode
195
+ pnpm --filter @xmachines/play-actor run test:watch
196
+ ```
215
197
 
216
- Copyright (c) 2016 [Mikael Karon](mailto:mikael@karon.se). All rights reserved.
198
+ ## Requirements
217
199
 
218
- This work is licensed under the terms of the MIT license.
219
- For a copy, see <https://opensource.org/licenses/MIT>.
200
+ - **Node.js** `>=22.0.0`
201
+ - **TypeScript** `>=5.7` (strict mode)
202
+ - **ESM only** — `"type": "module"`
@@ -16,205 +16,185 @@
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 "@xmachines/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 `@xmachines/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;
78
82
  /**
79
- * View metadata for rendering
83
+ * Actor capability for exposing renderable view state.
80
84
  *
81
- * Describes the component to be rendered and the props to pass to it.
82
- * Used by PlayRenderer to dynamically render UI based on actor state.
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.
83
89
  */
84
- export interface ViewMetadata {
85
- /** Name of the component to render from the catalog */
86
- component: string;
87
- /** Props to pass to the component */
88
- props: Record<string, unknown>;
89
- }
90
90
  export interface Viewable {
91
91
  /**
92
- * Current view signal
93
- *
94
- * State signal containing UI structure schema from meta.view. Infrastructure renders view.
95
- *
96
- * Invariant: Logic-Driven UI - View structure is defined by business logic, not JSX.
97
- *
98
- * @example
99
- * ```typescript
100
- * const watcher = new Signal.subtle.Watcher(() => {
101
- * const view = actor.currentView.get();
102
- * console.log('View changed:', view);
103
- * });
104
- * watcher.watch(actor.currentView);
105
- * ```
106
- */
107
- readonly currentView: Signal.State<ViewMetadata | null>;
108
- /**
109
- * 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.
110
94
  *
111
- * Maps component names to actual component implementations.
112
- * Used by renderers to resolve view.component to actual UI components.
95
+ * Infrastructure renders view Logic-Driven UI invariant.
113
96
  */
114
- readonly catalog: Record<string, unknown>;
97
+ readonly currentView: Signal.State<PlaySpec | null>;
115
98
  }
116
99
  /**
117
- * Abstract base class for Play Architecture actors.
100
+ * Framework-agnostic base for every framework's `ViewContextValue`.
118
101
  *
119
- * Extends XState Actor to maintain ecosystem compatibility (inspection, devtools)
120
- * while enforcing minimal signal protocol for Actor Infrastructure communication.
121
- *
122
- * The core protocol contains only:
123
- * - state: Reactive state snapshot
124
- * - send: Event dispatch method
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`.
125
105
  *
126
- * Optional capabilities (routing, view rendering) are provided via interfaces:
127
- * - Implement Routable for routing support
128
- * - Implement Viewable for view rendering support
129
- *
130
- * Concrete implementations created by @xmachines/play-xstate adapter.
106
+ * @typeParam TRegistry - The framework's component registry type (e.g. `ComponentRegistry` from `@xmachines/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
+ * `@xmachines/json-render-core` so no second generic is needed.
131
123
  *
132
- * @typeParam TLogic - XState actor logic type (maintains type safety)
124
+ * Framework packages extend this with their `fallback`, `onError`, and `children` fields.
133
125
  *
134
- * Invariant: Actor Authority - Actor is the sole source of truth for state transitions.
135
- * Invariant: Signal-Only Reactivity - Infrastructure observes via TC39 Signals.
136
- * Invariant: Passive Infrastructure - Infrastructure reflects, never decides.
126
+ * @typeParam TRegistry - The framework's `DefineRegistryResult` type.
137
127
  *
138
128
  * @example
139
- * Simple actor (no routing, no view)
140
- * ```typescript
141
- * class SimpleActor extends AbstractActor<AnyActorLogic> {
142
- * state = new Signal.State({...});
143
- * send(event) { ... }
144
- * }
145
- * ```
129
+ * ```ts
130
+ * import type { BaseActorProviderProps } from "@xmachines/play-actor";
131
+ * import type { DefineRegistryResult } from "@xmachines/json-render-react";
146
132
  *
147
- * @example
148
- * Routable actor
149
- * ```typescript
150
- * class RoutableActor extends AbstractActor<AnyActorLogic> implements Routable {
151
- * state = new Signal.State({...});
152
- * currentRoute = new Signal.Computed(() => deriveRoute(this.state.get()));
153
- * send(event) { ... }
133
+ * interface ActorProviderProps extends BaseActorProviderProps<DefineRegistryResult> {
134
+ * fallback?: React.ReactNode;
135
+ * children: React.ReactNode;
154
136
  * }
155
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.
156
161
  *
157
- * @example
158
- * Full-featured actor (routing + view)
159
- * ```typescript
160
- * class PlayerActor extends AbstractActor<AnyActorLogic> implements Routable, Viewable {
161
- * state = new Signal.State({...});
162
- * currentRoute = new Signal.Computed(() => deriveRoute(this.state.get()));
163
- * currentView = new Signal.State(null);
164
- * catalog = {};
165
- * send(event) { ... }
166
- * }
167
- * ```
162
+ * Provides signal-driven state observation that integrates with XState ecosystem
163
+ * tooling (devtools, inspection) while exposing reactive signals for
164
+ * Infrastructure layer communication.
168
165
  *
169
- * @see {@link https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md#53-actor-protocol | RFC Play v1 Section 5.3}
170
- * @see {@link Routable} for routing capability
171
- * @see {@link Viewable} for view rendering capability
166
+ * **A subclass IS the actor.** Forward the logic *and* its options to
167
+ * `super(logic, options)`, then observe `this`. Holding a separately
168
+ * constructed actor alongside leaves this instance running as an empty second
169
+ * actor, and every inherited member — `system`, `sessionId`, `clock`, the
170
+ * internal `_send` that receives `sendTo()` traffic, and anything a future
171
+ * XState version adds — answers from that empty one until it is individually
172
+ * forwarded.
173
+ *
174
+ * @typeParam TLogic - XState actor logic type
175
+ * @typeParam TEvent - Event type constraint (defaults to EventObject)
172
176
  */
173
- export declare abstract class AbstractActor<TLogic extends AnyActorLogic> extends Actor<TLogic> {
177
+ export declare abstract class AbstractActor<TLogic extends AnyActorLogic, TEvent extends EventObject = EventObject> extends Actor<TLogic> {
174
178
  /**
175
179
  * Reactive snapshot of current actor state.
176
180
  *
177
- * Typed as `Signal.State<unknown>` at the abstract level; concrete implementations
178
- * narrow this to the actual snapshot type (e.g., `Signal.State<AnyMachineSnapshot>`
179
- * in `@xmachines/play-xstate`'s `PlayerActor`).
180
- *
181
181
  * Infrastructure observes this signal to react to state changes without
182
- * directly coupling to the Actor's internal state machine implementation.
183
- *
184
- * @example
185
- * ```typescript
186
- * // Infrastructure observes state signal
187
- * const watcher = new Signal.subtle.Watcher(() => {
188
- * console.log('Actor state changed:', actor.state.get());
189
- * });
190
- * watcher.watch(actor.state);
191
- * ```
182
+ * directly coupling to the actor's internal state machine implementation.
192
183
  */
193
184
  abstract state: Signal.State<unknown>;
194
185
  /**
195
- * Send event to Actor
196
- *
197
- * Infrastructure forwards user intents (navigation, domain events, custom events)
198
- * as events to the Actor. The Actor's state machine guards determine whether
199
- * each event is valid from the current state.
200
- *
201
- * @param event - Event object with type property (e.g., PlayEvent, PlayRouteEvent)
202
- *
203
- * Invariant: Actor Authority - Only Actor decides whether an event is valid.
186
+ * Send event to Actor.
204
187
  *
205
- * @example
206
- * ```typescript
207
- * // Infrastructure forwards user intent
208
- * actor.send({ type: 'auth.login', userId: '123' });
209
- * // Actor's guards determine if event is allowed
210
- * ```
188
+ * Constrained to TEvent for type safety in concrete implementations.
211
189
  *
212
- * @remarks
213
- * Accepts any event object with a type property. Core events (PlayEvent) are in
214
- * @xmachines/play, routing events (PlayRouteEvent) are in @xmachines/play-router.
190
+ * Note for implementations that wrap `send` (validating the event, or
191
+ * notifying hooks around it): this declaration is abstract purely to narrow
192
+ * the event type, and TypeScript forbids `super` calls to an abstract
193
+ * member. Reach XState's own implementation with
194
+ * `Actor.prototype.send.call(this, event)` instead. Making this concrete
195
+ * would allow `super.send()` but would force every existing subclass to add
196
+ * an `override` modifier — a breaking change for adapters outside this repo.
215
197
  */
216
- abstract send(event: {
217
- readonly type: string;
218
- } & Record<string, unknown>): void;
198
+ abstract send(event: TEvent): void;
219
199
  }
220
200
  //# 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;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC5B,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,QAAQ;IACxB;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAExD;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,8BAAsB,aAAa,CAAC,MAAM,SAAS,aAAa,CAAE,SAAQ,KAAK,CAAC,MAAM,CAAC;IACtF;;;;;;;;;;;;;;;;;;OAkBG;IACH,SAAgB,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAE7C;;;;;;;;;;;;;;;;;;;;;OAqBG;aACsB,IAAI,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;CAC/F"}
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,EACX,IAAI,EACJ,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,MAAM,6BAA6B,CAAC;AAErC;;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;;;;;;;;;;;;;;;;;GAiBG;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;;;;;;;;;;;;OAYG;aACsB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;CAClD"}
@@ -18,61 +18,54 @@
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
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.
29
24
  *
30
- * Optional capabilities (routing, view rendering) are provided via interfaces:
31
- * - Implement Routable for routing support
32
- * - Implement Viewable for view rendering support
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.
33
28
  *
34
- * Concrete implementations created by @xmachines/play-xstate adapter.
35
- *
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<AnyActorLogic> {
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<AnyActorLogic> 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<AnyActorLogic> 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.
58
+ *
59
+ * **A subclass IS the actor.** Forward the logic *and* its options to
60
+ * `super(logic, options)`, then observe `this`. Holding a separately
61
+ * constructed actor alongside leaves this instance running as an empty second
62
+ * actor, and every inherited member — `system`, `sessionId`, `clock`, the
63
+ * internal `_send` that receives `sendTo()` traffic, and anything a future
64
+ * XState version adds — answers from that empty one until it is individually
65
+ * forwarded.
72
66
  *
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
67
+ * @typeParam TLogic - XState actor logic type
68
+ * @typeParam TEvent - Event type constraint (defaults to EventObject)
76
69
  */
77
70
  export class AbstractActor extends Actor {
78
71
  }
@@ -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;AAsGnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,MAAM,OAAgB,aAA4C,SAAQ,KAAa;CA6CtF"}
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;AAuCrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,SAAS,CACxB,IAEC;IAED,OAAO,IAAgB,CAAC;AACzB,CAAC;AAiFD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAgB,aAGpB,SAAQ,KAAa;CAuBtB"}
package/dist/index.d.ts CHANGED
@@ -13,7 +13,8 @@
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, type ViewMetadata, } from "./abstract-actor.js";
18
+ export { AbstractActor, typedSpec, type Routable, type Viewable, type PlaySpec, type BaseActorProviderProps, type BaseViewContextValue, } from "./abstract-actor.js";
19
+ export { toAtomState, attachRenderErrorHandler } from "./provider-guards.js";
19
20
  //# 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,EACN,aAAa,EACb,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,YAAY,GACjB,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;AAE7B,OAAO,EAAE,WAAW,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -13,7 +13,8 @@
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
+ export { toAtomState, attachRenderErrorHandler } from "./provider-guards.js";
19
20
  //# 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,EACN,aAAa,GAIb,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;AAE7B,OAAO,EAAE,WAAW,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Shared provider guards for framework ActorProvider implementations.
3
+ *
4
+ * Every framework integration (React, Vue, Solid, Svelte, DOM) performs the same
5
+ * two rituals when bridging an actor's view to a json-render tree:
6
+ *
7
+ * 1. Sanitize `spec.state` before seeding a fresh `@xstate/store` atom
8
+ * ({@link toAtomState}).
9
+ * 2. Inject a component-level `onRenderError` handler into the component
10
+ * registry without mutating the caller's registry
11
+ * ({@link attachRenderErrorHandler}).
12
+ *
13
+ * Centralising them here keeps the guard semantics identical across frameworks.
14
+ *
15
+ * @packageDocumentation
16
+ */
17
+ import type { RenderErrorHandler } from "@xmachines/json-render-core";
18
+ /**
19
+ * Safely coerce a spec's `state` field to a plain object for `createAtom`.
20
+ *
21
+ * `spec.state` is typed as `unknown` in `PlaySpec`. At runtime it can be
22
+ * `null`, `undefined`, a primitive, or a plain object depending on what the
23
+ * machine author put in the view spec. `createAtom` requires a plain object as
24
+ * its initial value — anything else produces a broken store at runtime.
25
+ *
26
+ * Only plain objects (prototype is `Object.prototype` or `null`) are accepted
27
+ * and returned as-is. Everything else — `null`, `undefined`, primitives,
28
+ * arrays, class instances, and built-in objects (Date, Map, Set, etc.) — falls
29
+ * back to a fresh `{}`, preventing silent broken-store bugs at runtime.
30
+ *
31
+ * @param state - The raw `spec.state` value from a `PlaySpec`.
32
+ * @returns `state` itself when it is a plain object, otherwise a new empty object.
33
+ */
34
+ export declare function toAtomState(state: unknown): Record<string, unknown>;
35
+ /**
36
+ * Clone a component registry and inject an `onRenderError` handler.
37
+ *
38
+ * The handler is defined as a non-enumerable, configurable own property on the
39
+ * clone (per D-19 — one injection convention for all framework renderers), so
40
+ * it overrides any handler set at `defineRegistry` level without showing up
41
+ * when the registry's component entries are enumerated.
42
+ *
43
+ * The caller's registry is never mutated — a shallow clone is returned. Callers
44
+ * that need per-instance handlers (e.g. an `onRenderError` prop on
45
+ * `ActorProvider`) can therefore share one `defineRegistry` result across
46
+ * providers safely.
47
+ *
48
+ * @typeParam TRegistry - The framework-specific component registry type.
49
+ * @param registry - The component registry from `defineRegistry().registry`.
50
+ * @param handler - Called with `(error, componentName)` when a catalog component throws during render.
51
+ * @returns A shallow clone of `registry` with `onRenderError` attached.
52
+ */
53
+ export declare function attachRenderErrorHandler<TRegistry extends object>(registry: TRegistry, handler: RenderErrorHandler): TRegistry;
54
+ //# sourceMappingURL=provider-guards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-guards.d.ts","sourceRoot":"","sources":["../src/provider-guards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEtE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQnE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,SAAS,MAAM,EAChE,QAAQ,EAAE,SAAS,EACnB,OAAO,EAAE,kBAAkB,GACzB,SAAS,CAQX"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Shared provider guards for framework ActorProvider implementations.
3
+ *
4
+ * Every framework integration (React, Vue, Solid, Svelte, DOM) performs the same
5
+ * two rituals when bridging an actor's view to a json-render tree:
6
+ *
7
+ * 1. Sanitize `spec.state` before seeding a fresh `@xstate/store` atom
8
+ * ({@link toAtomState}).
9
+ * 2. Inject a component-level `onRenderError` handler into the component
10
+ * registry without mutating the caller's registry
11
+ * ({@link attachRenderErrorHandler}).
12
+ *
13
+ * Centralising them here keeps the guard semantics identical across frameworks.
14
+ *
15
+ * @packageDocumentation
16
+ */
17
+ /**
18
+ * Safely coerce a spec's `state` field to a plain object for `createAtom`.
19
+ *
20
+ * `spec.state` is typed as `unknown` in `PlaySpec`. At runtime it can be
21
+ * `null`, `undefined`, a primitive, or a plain object depending on what the
22
+ * machine author put in the view spec. `createAtom` requires a plain object as
23
+ * its initial value — anything else produces a broken store at runtime.
24
+ *
25
+ * Only plain objects (prototype is `Object.prototype` or `null`) are accepted
26
+ * and returned as-is. Everything else — `null`, `undefined`, primitives,
27
+ * arrays, class instances, and built-in objects (Date, Map, Set, etc.) — falls
28
+ * back to a fresh `{}`, preventing silent broken-store bugs at runtime.
29
+ *
30
+ * @param state - The raw `spec.state` value from a `PlaySpec`.
31
+ * @returns `state` itself when it is a plain object, otherwise a new empty object.
32
+ */
33
+ export function toAtomState(state) {
34
+ if (state !== null && typeof state === "object" && !Array.isArray(state)) {
35
+ const proto = Object.getPrototypeOf(state);
36
+ if (proto === Object.prototype || proto === null) {
37
+ return state;
38
+ }
39
+ }
40
+ return {};
41
+ }
42
+ /**
43
+ * Clone a component registry and inject an `onRenderError` handler.
44
+ *
45
+ * The handler is defined as a non-enumerable, configurable own property on the
46
+ * clone (per D-19 — one injection convention for all framework renderers), so
47
+ * it overrides any handler set at `defineRegistry` level without showing up
48
+ * when the registry's component entries are enumerated.
49
+ *
50
+ * The caller's registry is never mutated — a shallow clone is returned. Callers
51
+ * that need per-instance handlers (e.g. an `onRenderError` prop on
52
+ * `ActorProvider`) can therefore share one `defineRegistry` result across
53
+ * providers safely.
54
+ *
55
+ * @typeParam TRegistry - The framework-specific component registry type.
56
+ * @param registry - The component registry from `defineRegistry().registry`.
57
+ * @param handler - Called with `(error, componentName)` when a catalog component throws during render.
58
+ * @returns A shallow clone of `registry` with `onRenderError` attached.
59
+ */
60
+ export function attachRenderErrorHandler(registry, handler) {
61
+ const clone = { ...registry };
62
+ Object.defineProperty(clone, "onRenderError", {
63
+ value: handler,
64
+ enumerable: false,
65
+ configurable: true,
66
+ });
67
+ return clone;
68
+ }
69
+ //# sourceMappingURL=provider-guards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-guards.js","sourceRoot":"","sources":["../src/provider-guards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,WAAW,CAAC,KAAc;IACzC,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1E,MAAM,KAAK,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,CAAY,CAAC;QACtD,IAAI,KAAK,KAAK,MAAM,CAAC,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YAClD,OAAO,KAAgC,CAAC;QACzC,CAAC;IACF,CAAC;IACD,OAAO,EAAE,CAAC;AACX,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,wBAAwB,CACvC,QAAmB,EACnB,OAA2B;IAE3B,MAAM,KAAK,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;IAC9B,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,eAAe,EAAE;QAC7C,KAAK,EAAE,OAAO;QACd,UAAU,EAAE,KAAK;QACjB,YAAY,EAAE,IAAI;KAClB,CAAC,CAAC;IACH,OAAO,KAAK,CAAC;AACd,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmachines/play-actor",
3
- "version": "1.0.0-beta.9",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "description": "Abstract Actor base class for XMachines Play Architecture",
6
6
  "keywords": [
@@ -12,12 +12,18 @@
12
12
  ],
13
13
  "license": "MIT",
14
14
  "author": "XMachines Contributors",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+ssh://git@gitlab.com/xmachin-es/xmachines-js.git",
18
+ "directory": "packages/play-actor"
19
+ },
15
20
  "files": [
16
21
  "dist",
17
22
  "README.md",
18
23
  "LICENSE"
19
24
  ],
20
25
  "type": "module",
26
+ "sideEffects": false,
21
27
  "exports": {
22
28
  ".": {
23
29
  "types": "./dist/index.d.ts",
@@ -29,28 +35,31 @@
29
35
  },
30
36
  "scripts": {
31
37
  "build": "tsc --build",
32
- "clean": "rm -rf dist *.tsbuildinfo node_modules/.vite node_modules/.vite-temp",
33
- "typecheck": "tsc --noEmit",
38
+ "clean": "rm -rf dist *.tsbuildinfo coverage node_modules/.svelte2tsx-* node_modules/.vite*",
34
39
  "test": "vitest",
35
40
  "lint": "oxlint .",
36
41
  "lint:fix": "oxlint --fix .",
37
42
  "format": "oxfmt .",
38
- "format:check": "oxfmt --check .",
39
- "prepublishOnly": "npm run build"
43
+ "format:check": "oxfmt --check ."
44
+ },
45
+ "dependencies": {
46
+ "@xmachines/json-render-core": "^0.19.0-xm.2"
40
47
  },
41
48
  "devDependencies": {
42
- "@types/node": "^25.5.0",
43
- "@xmachines/shared": "1.0.0-beta.9",
44
- "vitest": "^4.1.0",
45
- "xstate": "^5.0.0"
49
+ "@testing-library/jest-dom": "^6.9.1",
50
+ "@types/node": "^26.2.0",
51
+ "oxfmt": "^0.64.0",
52
+ "oxlint": "^1.79.0",
53
+ "vitest": "^4.1.11",
54
+ "xstate": "^5.31.0"
46
55
  },
47
56
  "peerDependencies": {
48
- "@xmachines/play": "1.0.0-beta.9",
49
- "@xmachines/play-signals": "1.0.0-beta.9",
50
- "xstate": "^5.0.0"
57
+ "@xmachines/play": "1.1.0",
58
+ "@xmachines/play-signals": "1.1.0",
59
+ "xstate": "^5.31.0"
51
60
  },
52
61
  "engines": {
53
62
  "node": ">=22.0.0"
54
63
  },
55
- "_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.28.0 to prevent drift."
64
+ "_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."
56
65
  }