@xmachines/play-solid 1.0.0-beta.6 → 1.0.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +251 -62
  2. package/package.json +7 -7
package/README.md CHANGED
@@ -1,11 +1,27 @@
1
1
  # @xmachines/play-solid
2
2
 
3
- SolidJS renderer for XMachines Play architecture. Enables catalog-driven view rendering with actor-owned business state.
3
+ **SolidJS renderer consuming signals and UI schema with provider pattern**
4
+
5
+ Signal-driven SolidJS rendering layer observing actor state with zero SolidJS state for business logic.
6
+
7
+ ## Overview
8
+
9
+ `@xmachines/play-solid` provides `PlayRenderer` for building SolidJS UIs that passively observe actor signals. This package enables framework-swappable architecture where SolidJS is just a rendering target subscribing to signal changes — business logic lives entirely in the actor.
10
+
11
+ Per [RFC Play v1](https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md), this package implements:
12
+
13
+ - **Signal-Only Reactivity (INV-05):** No createSignal/createStore for business logic, TC39 signals only
14
+ - **Passive Infrastructure (INV-04):** Components observe signals, send events to actor
15
+
16
+ **Key Principle:** SolidJS state is never used for business logic. Signals are the source of truth.
17
+
18
+ Renderer receives actor via props (provider pattern), not children.
4
19
 
5
20
  ## Installation
6
21
 
7
22
  ```bash
8
- npm install @xmachines/play-solid solid-js
23
+ npm install solid-js@^1.8.0
24
+ npm install @xmachines/play-solid
9
25
  ```
10
26
 
11
27
  ## Current Exports
@@ -13,98 +29,271 @@ npm install @xmachines/play-solid solid-js
13
29
  - `PlayRenderer`
14
30
  - `PlayRendererProps` (type)
15
31
 
16
- ## Usage
32
+ **Peer dependencies:**
17
33
 
18
- ```typescript
19
- import { PlayRenderer } from '@xmachines/play-solid';
20
- import { definePlayer } from '@xmachines/play-xstate';
21
- import { defineCatalog } from '@xmachines/play-catalog';
34
+ - `solid-js` ^1.8.0 - SolidJS runtime
35
+
36
+ ## Quick Start
22
37
 
23
- // Define catalog
38
+ ```tsx
39
+ import { render } from "solid-js/web";
40
+ import { definePlayer } from "@xmachines/play-xstate";
41
+ import { defineCatalog } from "@xmachines/play-catalog";
42
+ import { PlayRenderer } from "@xmachines/play-solid";
43
+ import { z } from "zod";
44
+
45
+ // 1. Define catalog (business logic layer)
24
46
  const catalog = defineCatalog({
25
- Home: { component: 'Home', props: {} },
26
- Login: { component: 'Login', props: { error: { type: 'string' } } }
47
+ LoginForm: z.object({ error: z.string().optional() }),
48
+ Dashboard: z.object({
49
+ userId: z.string(),
50
+ username: z.string(),
51
+ }),
27
52
  });
28
53
 
29
- // Create player
30
- const createPlayer = definePlayer({
31
- machine: authMachine,
32
- catalog
33
- });
54
+ // 2. Create SolidJS components (view layer)
55
+ const components = {
56
+ LoginForm: (props) => (
57
+ <form
58
+ onSubmit={(e) => {
59
+ e.preventDefault();
60
+ const data = new FormData(e.currentTarget);
61
+ props.send({
62
+ type: "auth.login",
63
+ username: data.get("username"),
64
+ });
65
+ }}
66
+ >
67
+ {props.error && <p style={{ color: "red" }}>{props.error}</p>}
68
+ <input name="username" required placeholder="Username" />
69
+ <button type="submit">Log In</button>
70
+ </form>
71
+ ),
72
+ Dashboard: (props) => (
73
+ <div>
74
+ <h1>Welcome, {props.username}!</h1>
75
+ <p>User ID: {props.userId}</p>
76
+ <button onClick={() => props.send({ type: "auth.logout" })}>Log Out</button>
77
+ </div>
78
+ ),
79
+ };
34
80
 
81
+ // 3. Create player actor (business logic runtime)
82
+ const createPlayer = definePlayer({ machine: authMachine, catalog });
35
83
  const actor = createPlayer();
36
84
  actor.start();
37
85
 
38
- // Define components
39
- const components = {
40
- Home: (props) => <div>Home</div>,
41
- Login: (props) => (
42
- <form onSubmit={(e) => {
43
- e.preventDefault();
44
- props.send({ type: 'auth.login', payload: {...} });
45
- }}>
46
- {props.error && <p>{props.error}</p>}
47
- <input type="text" name="username" />
48
- <button type="submit">Login</button>
49
- </form>
50
- )
51
- };
52
-
53
- // Render
54
- <PlayRenderer actor={actor} components={components} />
86
+ // 4. Render UI (actor via props)
87
+ render(
88
+ () => <PlayRenderer actor={actor} components={components} />,
89
+ document.getElementById("app")!,
90
+ );
55
91
  ```
56
92
 
57
- ## API
93
+ ## API Reference
58
94
 
59
95
  ### PlayRenderer
60
96
 
61
- Component that observes `actor.currentView` signal and renders the appropriate component from the catalog.
97
+ Main renderer component subscribing to actor signals and dynamically rendering catalog components:
98
+
99
+ ```typescript
100
+ interface PlayRendererProps {
101
+ actor: AbstractActor<any>;
102
+ components: Record<string, Component<any>>;
103
+ fallback?: JSX.Element;
104
+ }
105
+ ```
62
106
 
63
107
  **Props:**
64
108
 
65
- - `actor: AbstractActor & Viewable` - Actor instance with currentView signal
66
- - `components: Record<string, Component<any>>` - Map of component names to SolidJS components
67
- - `fallback?: JSX.Element` - Optional fallback to show when currentView is null
109
+ - `actor` - Actor instance with `currentView` signal
110
+ - `components` - Map of component names to SolidJS components
111
+ - `fallback` - Component shown when `currentView` is null
68
112
 
69
- **Features:**
113
+ **Behavior:**
70
114
 
71
- - Automatically bridges TC39 Signals to SolidJS reactivity
72
- - Passes `send` function to components for event forwarding
73
- - Handles missing components gracefully with error logging
74
- - Uses one-shot watcher re-watch pattern for proper signal observation
115
+ 1. Subscribes to `actor.currentView` signal using a `Signal.subtle.Watcher` inside the component
116
+ 2. Looks up component from `components` map using `view.component` string
117
+ 3. Renders component with props from `view.props` + `send` function using Solid's `<Dynamic />`
75
118
 
76
- ## Canonical Watcher Lifecycle
119
+ **Example:**
77
120
 
78
- Use the same watcher flow as React/Vue/router packages:
121
+ ```tsx
122
+ <PlayRenderer
123
+ actor={actor}
124
+ components={{
125
+ HomePage: (props) => <div>Home</div>,
126
+ AboutPage: (props) => <div>About</div>,
127
+ }}
128
+ fallback={<div>Loading...</div>}
129
+ />
130
+ ```
79
131
 
80
- 1. `notify`
81
- 2. `queueMicrotask`
82
- 3. `getPending()`
83
- 4. read actor signals and update framework-local render trigger
84
- 5. re-arm with `watch(...)` or `watch()`
132
+ ## Examples
85
133
 
86
- Watcher notifications are one-shot, so re-arm is mandatory.
134
+ ### Component Receiving Props from Catalog
87
135
 
88
- ## Cleanup Contract
136
+ ```tsx
137
+ import { PlayRenderer } from "@xmachines/play-solid";
138
+ import { defineCatalog } from "@xmachines/play-catalog";
139
+ import { z } from "zod";
89
140
 
90
- Solid integrations must perform explicit teardown:
141
+ // Define schema in catalog
142
+ const catalog = defineCatalog({
143
+ UserProfile: z.object({
144
+ userId: z.string(),
145
+ name: z.string(),
146
+ avatar: z.string().url().optional(),
147
+ stats: z.object({
148
+ posts: z.number(),
149
+ followers: z.number(),
150
+ }),
151
+ }),
152
+ });
91
153
 
92
- - Use `onCleanup` for lifecycle teardown.
93
- - Call `unwatch(...)` on teardown, not only reference nulling.
94
- - Keep adapters/renderers passive; state validity remains actor-owned.
154
+ // Component receives type-safe props + send
155
+ const components = {
156
+ UserProfile: (props) => (
157
+ <div>
158
+ {props.avatar && <img src={props.avatar} alt={props.name} />}
159
+ <h1>{props.name}</h1>
160
+ <p>ID: {props.userId}</p>
161
+ <div>
162
+ <span>{props.stats.posts} posts</span>
163
+ <span>{props.stats.followers} followers</span>
164
+ </div>
165
+ <button
166
+ onClick={() =>
167
+ props.send({
168
+ type: "profile.edit",
169
+ userId: props.userId,
170
+ })
171
+ }
172
+ >
173
+ Edit Profile
174
+ </button>
175
+ </div>
176
+ ),
177
+ };
178
+
179
+ <PlayRenderer actor={actor} components={components} />;
180
+ ```
181
+
182
+ ### Provider Pattern
183
+
184
+ ```tsx
185
+ import { PlayTanStackRouterProvider } from "@xmachines/play-tanstack-solid-router";
186
+ import { PlayRenderer } from "@xmachines/play-solid";
187
+ import { createSignal, onCleanup } from "solid-js";
188
+
189
+ // Renderer receives actor via props (not children)
190
+ function App() {
191
+ return (
192
+ <PlayTanStackRouterProvider
193
+ actor={actor}
194
+ router={router}
195
+ routeMap={routeMap}
196
+ renderer={(currentActor, currentRouter) => {
197
+ return (
198
+ <div>
199
+ <Header actor={currentActor} />
200
+ <PlayRenderer actor={currentActor} components={components} />
201
+ <Footer />
202
+ </div>
203
+ );
204
+ }}
205
+ />
206
+ );
207
+ }
208
+
209
+ // Header component also receives actor
210
+ function Header(props) {
211
+ const [route, setRoute] = createSignal<string | null>(null);
212
+
213
+ // Manual watcher setup for custom component reading signals
214
+ let watcher: Signal.subtle.Watcher;
215
+
216
+ // ... setup watcher to update route signal ...
217
+
218
+ return (
219
+ <header>
220
+ <nav>Current: {route()}</nav>
221
+ </header>
222
+ );
223
+ }
224
+ ```
95
225
 
96
226
  ## Architecture
97
227
 
98
- PlayRenderer follows the XMachines Play architecture:
228
+ This package implements **Signal-Only Reactivity (INV-05)** and **Passive Infrastructure (INV-04)**:
229
+
230
+ 1. **No Business Logic in SolidJS:**
231
+ - No createSignal/createStore for business state
232
+ - No createEffect for business side effects
233
+ - SolidJS only triggers renders, doesn't control state
234
+
235
+ 2. **Signals as Source of Truth:**
236
+ - `actor.currentView.get()` provides UI structure
237
+ - `actor.currentRoute.get()` provides navigation state
238
+ - Components observe signals via explicit watcher patterns
239
+
240
+ 3. **Event Forwarding:**
241
+ - Components receive `send` function via props
242
+ - User actions send events to actor (e.g., `{ type: "auth.login" }`)
243
+ - Actor guards validate and process events
244
+
245
+ 4. **Microtask Batching:**
246
+ - `Signal.subtle.Watcher` coalesces rapid signal changes
247
+ - Prevents SolidJS thrashing from multiple signal updates
248
+ - Single SolidJS render per microtask batch
249
+
250
+ 5. **Explicit Disposal Contract:**
251
+ - Component teardown calls watcher `unwatch` in `onCleanup`
252
+ - Do not rely on GC-only cleanup
99
253
 
100
- - **Actor Authority**: Actor controls all state transitions via guards
101
- - **Passive Infrastructure**: Renderer observes signals, sends events
102
- - **Signal-Only Reactivity**: Business logic state lives in actor signals
254
+ **Pattern:**
103
255
 
104
- The renderer bridges TC39 Signals (used by XMachines actors) to SolidJS's reactivity system using `Signal.subtle.Watcher` with a one-shot re-watch pattern.
256
+ - Renderer receives actor via props (provider pattern)
257
+ - Enables composition with navigation, headers, footers
258
+ - Supports multiple renderers in same app
105
259
 
106
- Signals remain observation plumbing, not an alternate mutation channel.
260
+ **Architectural Invariants:**
261
+
262
+ - **Signal-Only Reactivity (INV-05):** No SolidJS state for business logic
263
+ - **Passive Infrastructure (INV-04):** Components reflect, never decide
264
+
265
+ ## Canonical Watcher Lifecycle
266
+
267
+ If you write your own custom integration, use the same watcher flow as `PlayRenderer`:
268
+
269
+ 1. `notify` callback runs
270
+ 2. Schedule work with `queueMicrotask`
271
+ 3. Drain `watcher.getPending()`
272
+ 4. Read actor signals and update SolidJS-local signal state (`setSignal()`)
273
+ 5. Re-arm with `watch(...)` or `watch()`
274
+
275
+ Watcher notify is one-shot. Re-arm is required for continuous observation.
276
+
277
+ ## Benefits
278
+
279
+ - **Framework Swappable:** Business logic has zero SolidJS imports
280
+ - **Type Safety:** Props validated against catalog schemas
281
+ - **Simple Testing:** Test actors without SolidJS renderer
282
+ - **Performance:** Microtask batching reduces unnecessary renders
283
+ - **Composability:** Renderer prop enables complex layouts
284
+
285
+ ## Related Packages
286
+
287
+ - **[@xmachines/play-xstate](../play-xstate)** - XState adapter providing actors
288
+ - **[@xmachines/play-catalog](../play-catalog)** - UI schema validation
289
+ - **[@xmachines/play-solid-router](../play-solid-router)** - Solid Router integration
290
+ - **[@xmachines/play-tanstack-solid-router](../play-tanstack-solid-router)** - TanStack Solid Router integration
291
+ - **[@xmachines/play-actor](../play-actor)** - Actor base
292
+ - **[@xmachines/play-signals](../play-signals)** - TC39 Signals primitives
107
293
 
108
294
  ## License
109
295
 
110
- MIT
296
+ Copyright (c) 2016 [Mikael Karon](mailto:mikael@karon.se). All rights reserved.
297
+
298
+ This work is licensed under the terms of the MIT license.
299
+ For a copy, see <https://opensource.org/licenses/MIT>.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmachines/play-solid",
3
- "version": "1.0.0-beta.6",
3
+ "version": "1.0.0-beta.8",
4
4
  "description": "SolidJS renderer for XMachines Play architecture",
5
5
  "keywords": [
6
6
  "catalog",
@@ -29,22 +29,22 @@
29
29
  },
30
30
  "scripts": {
31
31
  "build": "vite build && tsc --build",
32
- "clean": "rm -rf dist tsconfig.tsbuildinfo",
32
+ "clean": "rm -rf dist tsconfig.tsbuildinfo node_modules/.vite node_modules/.vite-temp",
33
33
  "typecheck": "tsc --noEmit",
34
- "test": "vitest run",
34
+ "test": "vitest",
35
35
  "test:watch": "vitest",
36
36
  "test:ui": "vitest --ui",
37
37
  "prepublishOnly": "npm run build"
38
38
  },
39
39
  "dependencies": {
40
- "@xmachines/play-actor": "1.0.0-beta.6",
41
- "@xmachines/play-catalog": "1.0.0-beta.6",
42
- "@xmachines/play-signals": "1.0.0-beta.6"
40
+ "@xmachines/play-actor": "1.0.0-beta.8",
41
+ "@xmachines/play-catalog": "1.0.0-beta.8",
42
+ "@xmachines/play-signals": "1.0.0-beta.8"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@solidjs/testing-library": "^0.8.10",
46
46
  "@types/node": "^25.5.0",
47
- "@xmachines/shared": "1.0.0-beta.6",
47
+ "@xmachines/shared": "1.0.0-beta.8",
48
48
  "solid-js": "^1.9.11",
49
49
  "typescript": "^5.9.3",
50
50
  "vite": "^8.0.0",