@xmachines/play-solid-router 1.0.0-beta.9 → 2.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,641 +1,224 @@
1
- # @xmachines/play-solid-router
2
-
3
- **SolidJS Router adapter for XMachines Universal Player Architecture**
4
-
5
- SolidJS Router adapter using `RouterBridgeBase` for consistent actor↔router sync.
6
-
7
- ## Overview
8
-
9
- `@xmachines/play-solid-router` provides seamless integration between SolidJS Router and XMachines state machines. Built on Solid's reactive primitives, it enables zero-adaptation signals synchronization.
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):** State machine controls navigation, router reflects decisions
14
- - **Passive Infrastructure (INV-04):** Router observes `actor.currentRoute` signal
15
- - **Signal-Only Reactivity (INV-05):** TC39 watcher lifecycle + Solid reactive owner integration
16
-
17
- **Key Benefits:**
1
+ <!-- generated-by: gsd-doc-writer -->
18
2
 
19
- - **Bridge-first:** Extends shared `RouterBridgeBase` policy used by other adapters
20
- - **Automatic tracking:** Uses Solid reactivity for router→actor while base class handles actor→router watcher lifecycle
21
- - **Fine-grained reactivity:** Updates only affected components
22
- - **Logic-driven navigation:** Business logic in state machines, not components
23
- - **Type-safe parameters:** Route params flow through state machine context
3
+ # @xmachines/play-solid-router
24
4
 
25
- **Framework Compatibility:**
5
+ SolidJS Router adapter for the XMachines Universal Player Architecture. Provides bidirectional synchronisation between a `PlayerActor`'s state machine routes and the browser URL via `@solidjs/router`.
26
6
 
27
- - SolidJS 1.8.4+ (signals-native architecture)
28
- - @solidjs/router 0.13.0+ (modern routing primitives)
29
- - TC39 Signals polyfill integration
7
+ Part of the [xmachines-js monorepo](../../README.md).
30
8
 
31
9
  ## Installation
32
10
 
33
11
  ```bash
34
- npm install @solidjs/router@^0.13.0 solid-js@^1.8.0 @xmachines/play-solid-router @xmachines/play-solid
35
- ```
36
-
37
- **Peer dependencies:**
38
-
39
- - `@solidjs/router` ^0.13.0 — SolidJS Router library
40
- - `solid-js` ^1.8.0 — SolidJS runtime
41
- - `@xmachines/play-solid` — Solid renderer (`PlayRenderer`)
42
- - `@xmachines/play-actor` — Actor base
43
- - `@xmachines/play-router` — Route extraction
44
- - `@xmachines/play-signals` — TC39 Signals primitives
45
-
46
- ## Quick Start
47
-
48
- ```typescript
49
- import { Router, Route, useNavigate, useLocation, useParams } from '@solidjs/router';
50
- import { onCleanup } from 'solid-js';
51
- import { SolidRouterBridge, createRouteMap } from '@xmachines/play-solid-router';
52
- import { definePlayer } from '@xmachines/play-xstate';
53
-
54
- function App() {
55
- // 1. Get SolidJS Router hooks (MUST be inside component)
56
- const navigate = useNavigate();
57
- const location = useLocation();
58
- const params = useParams();
59
-
60
- // 2. Create route mapping from machine routes
61
- const routeMap = createRouteMap(authMachine);
62
-
63
- // 3. Create player with state machine
64
- const createPlayer = definePlayer({
65
- machine: authMachine,
66
- catalog: componentCatalog
67
- });
68
- const actor = createPlayer();
69
- actor.start();
70
-
71
- // 4. Create bridge to sync actor and router
72
- const bridge = new SolidRouterBridge(
73
- navigate,
74
- location,
75
- params,
76
- actor,
77
- routeMap
78
- );
79
-
80
- // 5. Start synchronization
81
- bridge.connect();
82
-
83
- // 6. Cleanup on component disposal
84
- onCleanup(() => {
85
- bridge.disconnect();
86
- });
87
-
88
- return (
89
- <Router>
90
- <Route path="/" component={HomeView} />
91
- <Route path="/profile/:userId" component={ProfileView} />
92
- <Route path="/settings/:section?" component={SettingsView} />
93
- </Router>
94
- );
95
- }
96
- ```
97
-
98
- ## API Reference
99
-
100
- ### `SolidRouterBridge`
101
-
102
- Router adapter implementing the `RouterBridge` protocol for SolidJS Router.
103
-
104
- **Type Signature:**
105
-
106
- ```typescript
107
- class SolidRouterBridge {
108
- constructor(
109
- navigate: ReturnType<typeof useNavigate>,
110
- location: ReturnType<typeof useLocation>,
111
- params: ReturnType<typeof useParams>,
112
- actor: AbstractActor<any>,
113
- routeMap: RouteMap,
114
- );
115
- dispose(): void;
116
- }
12
+ pnpm add @xmachines/play-solid-router
117
13
  ```
118
14
 
119
- **Constructor Parameters:**
120
-
121
- - `navigate` - Function from `useNavigate()` hook (signals-aware navigation)
122
- - `location` - Object from `useLocation()` hook (reactive pathname, search, hash)
123
- - `params` - Object from `useParams()` hook (reactive route parameters)
124
- - `actor` - XMachines actor instance (from `definePlayer().actor`)
125
- - `routeMap` - Bidirectional state ID ↔ path mapping
126
-
127
- **Methods:**
128
-
129
- - `connect()` - Start bidirectional synchronization.
130
- - `disconnect()` - Stop synchronization and cleanup bridge resources.
131
- - `dispose()` - Alias of `disconnect()`.
132
-
133
- **Internal Behavior:**
134
-
135
- - Uses `RouterBridgeBase` TC39 watcher lifecycle for actor→router synchronization
136
- - Updates SolidJS Router via `navigate(path)` when actor state changes
137
- - Uses `createEffect(on(...))` to watch `location.pathname` signal
138
- - Sends `play.route` events to actor when user navigates
139
- - Prevents circular updates with path tracking and processing flags
15
+ **Peer dependencies** (must be installed separately):
140
16
 
141
- ### `RouteMap`
142
-
143
- Bidirectional mapping between XMachines state IDs and SolidJS Router paths with pattern matching support.
144
-
145
- `RouteMap` extends `BaseRouteMap` from `@xmachines/play-router`, inheriting bucket-indexed
146
- bidirectional route matching. No routing logic lives in the adapter itself.
147
-
148
- ```typescript
149
- interface RouteMapping {
150
- readonly stateId: string;
151
- readonly path: string;
152
- }
153
-
154
- // RouteMap is a thin subclass of BaseRouteMap — no extra methods
155
- class RouteMap extends BaseRouteMap {}
156
-
157
- // Inherited API:
158
- routeMap.getStateIdByPath(path: string): string | null
159
- routeMap.getPathByStateId(stateId: string): string | null
160
- ```
161
-
162
- `getStateIdByPath` and `getPathByStateId` both return `null` (not `undefined`) for misses.
163
-
164
- **Constructor Parameters:**
165
-
166
- - `mappings` - Array of `{ stateId, path }` entries:
167
- - `stateId` — State machine state ID (e.g., `'#profile'`)
168
- - `path` — SolidJS Router path pattern (e.g., `'/profile/:userId'`)
169
-
170
- **Methods:**
171
-
172
- - `getPathByStateId(stateId)` — Find path pattern from state ID
173
- - `getStateIdByPath(path)` — Find state ID from path with pattern matching (supports `:param` and `:param?` syntax)
174
-
175
- **Pattern Matching:**
176
-
177
- Uses bucket-indexed RegExp matching for dynamic routes:
178
-
179
- ```typescript
180
- const routeMap = new RouteMap([{ stateId: "#settings", path: "/settings/:section?" }]);
181
-
182
- routeMap.getStateIdByPath("/settings"); // '#settings'
183
- routeMap.getStateIdByPath("/settings/account"); // '#settings'
184
- routeMap.getStateIdByPath("/settings/privacy"); // '#settings'
185
- routeMap.getStateIdByPath("/other"); // null
186
- ```
187
-
188
- ## Examples
189
-
190
- ### Basic Usage: Simple 2-3 Route Setup
191
-
192
- ```typescript
193
- import { Router, Route } from '@solidjs/router';
194
- import { createSignal } from 'solid-js';
195
- import { defineCatalog } from '@xmachines/play-catalog';
196
-
197
- // State machine with 3 states
198
- const appMachine = setup({
199
- types: {
200
- events: {} as PlayRouteEvent
201
- }
202
- }).createMachine({
203
- id: 'app',
204
- initial: 'home',
205
- states: {
206
- home: {
207
- meta: { route: '/', view: { component: 'Home' } }
208
- },
209
- about: {
210
- meta: { route: '/about', view: { component: 'About' } }
211
- },
212
- contact: {
213
- meta: { route: '/contact', view: { component: 'Contact' } }
214
- }
215
- }
216
- });
217
-
218
- const catalog = defineCatalog({
219
- Home,
220
- About,
221
- Contact,
222
- });
223
-
224
- // Component setup
225
- function App() {
226
- const navigate = useNavigate();
227
- const location = useLocation();
228
- const params = useParams();
229
-
230
- const routeMap = createRouteMap(appMachine);
231
-
232
- const createPlayer = definePlayer({ machine: appMachine, catalog });
233
- const actor = createPlayer();
234
- actor.start();
235
- const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
236
-
237
- onCleanup(() => bridge.dispose());
238
-
239
- return (
240
- <Router>
241
- <Route path="/" component={Home} />
242
- <Route path="/about" component={About} />
243
- <Route path="/contact" component={Contact} />
244
- </Router>
245
- );
246
- }
17
+ ```bash
18
+ pnpm add solid-js @solidjs/router xstate
247
19
  ```
248
20
 
249
- ### Parameter Handling: Dynamic Routes with `:param` Syntax
250
-
251
- ```typescript
252
- // State machine with parameter routes
253
- import { formatPlayRouteTransitions } from "@xmachines/play-xstate";
254
- import { defineCatalog } from "@xmachines/play-catalog";
21
+ - `solid-js` `^1.8.0`
22
+ - `@solidjs/router` `^0.16.1`
23
+ - `xstate` `^6.0.0-alpha.19`
255
24
 
256
- const machineConfig = {
257
- id: 'app',
258
- context: {},
259
- states: {
260
- profile: {
261
- meta: {
262
- route: '/profile/:userId',
263
- view: { component: 'Profile' },
264
- },
265
- },
266
- settings: {
267
- meta: {
268
- route: '/settings/:section?',
269
- view: { component: 'Settings' },
270
- },
271
- }
272
- }
273
- };
274
-
275
- const appMachine = setup({
276
- types: {
277
- context: {} as { userId?: string; section?: string },
278
- events: {} as PlayRouteEvent
279
- }
280
- }).createMachine(formatPlayRouteTransitions(machineConfig));
281
-
282
- const catalog = defineCatalog({
283
- Profile,
284
- Settings,
285
- });
286
-
287
- // Router with dynamic routes
288
- function App() {
289
- const navigate = useNavigate();
290
- const location = useLocation();
291
- const params = useParams();
25
+ ## Quick Start
292
26
 
293
- const routeMap = createRouteMap(appMachine);
27
+ ```tsx
28
+ import { Router, Route, useNavigate, useLocation, useParams } from "@solidjs/router";
29
+ import { onCleanup, type ParentComponent } from "solid-js";
30
+ import { PlayRouterProvider, createRouteMap } from "@xmachines/play-solid-router";
31
+ import { definePlayer } from "@xmachines/play-xstate";
32
+ import { myMachine } from "./machine.js";
294
33
 
295
- const createPlayer = definePlayer({ machine: appMachine, catalog });
296
- const actor = createPlayer();
297
- actor.start();
298
- const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
34
+ const actor = definePlayer({ machine: myMachine })();
35
+ actor.start();
299
36
 
300
- onCleanup(() => bridge.dispose());
37
+ const routeMap = createRouteMap(myMachine);
301
38
 
302
- return (
303
- <Router>
304
- <Route path="/profile/:userId" component={Profile} />
305
- <Route path="/settings/:section?" component={Settings} />
306
- </Router>
307
- );
308
- }
309
- ```
39
+ const Layout: ParentComponent = () => {
40
+ const navigate = useNavigate();
41
+ const location = useLocation();
42
+ const params = useParams();
310
43
 
311
- **Usage in component:**
44
+ onCleanup(() => actor.stop());
312
45
 
313
- ```tsx
314
- function ProfileButton(props: { userId: string }) {
315
46
  return (
316
- <button
317
- onClick={() =>
318
- props.actor.send({
319
- type: "play.route",
320
- to: "#profile",
321
- params: { userId: props.userId },
322
- })
323
- }
324
- >
325
- View Profile
326
- </button>
47
+ <PlayRouterProvider
48
+ actor={actor}
49
+ routeMap={routeMap}
50
+ router={{ navigate, location, params }}
51
+ renderer={(a, router) => <MyApp actor={a} />}
52
+ />
327
53
  );
328
- }
329
- ```
330
-
331
- ### Query Parameters: Search/Filters via Query Strings
332
-
333
- ```typescript
334
- // State machine with query param handling
335
- import { formatPlayRouteTransitions } from "@xmachines/play-xstate";
336
- import { defineCatalog } from "@xmachines/play-catalog";
337
-
338
- const machineConfig = {
339
- context: { query: '', filters: {} },
340
- states: {
341
- search: {
342
- meta: {
343
- route: '/search',
344
- view: { component: 'Search' },
345
- },
346
- }
347
- }
348
54
  };
349
55
 
350
- const searchMachine = setup({
351
- types: {
352
- context: {} as { query: string; filters: Record<string, string> },
353
- events: {} as PlayRouteEvent
354
- }
355
- }).createMachine(formatPlayRouteTransitions(machineConfig));
356
-
357
- const catalog = defineCatalog({
358
- Search,
359
- });
360
-
361
- const player = definePlayer({ machine: searchMachine, catalog });
362
-
363
- // Component sends query params
364
- function SearchBar(props) {
365
- const [searchTerm, setSearchTerm] = createSignal('');
366
-
367
- function handleSearch() {
368
- props.actor.send({
369
- type: 'play.route',
370
- to: '#search',
371
- query: { q: searchTerm(), tag: 'typescript' }
372
- });
373
- }
374
-
375
- return (
376
- <div>
377
- <input
378
- value={searchTerm()}
379
- onInput={(e) => setSearchTerm(e.target.value)}
380
- />
381
- <button onClick={handleSearch}>Search</button>
382
- </div>
383
- );
56
+ export default function App() {
57
+ return <Router root={Layout}>{/* one <Route> per routable state */}</Router>;
384
58
  }
385
59
  ```
386
60
 
387
- **SolidJS Router automatically reflects query params in URL:**
388
-
389
- - `/search?q=xmachines&tag=typescript`
61
+ ## API Summary
390
62
 
391
- ### Protected Routes: Authentication Guards
63
+ ### `PlayRouterProvider`
392
64
 
393
- ```typescript
394
- // State machine with auth guards
395
- import { defineCatalog } from "@xmachines/play-catalog";
65
+ A SolidJS component that wires a `PlayerActor` to Solid Router. It creates and connects a `SolidRouterBridge` on mount and disconnects it via `onCleanup` on unmount.
396
66
 
397
- const authMachine = setup({
398
- types: {
399
- context: {} as { isAuthenticated: boolean },
400
- events: {} as PlayRouteEvent | { type: "login" } | { type: "logout" },
401
- },
402
- }).createMachine({
403
- context: { isAuthenticated: false },
404
- initial: "home",
405
- states: {
406
- home: {
407
- meta: { route: "/", view: { component: "Home" } },
408
- },
409
- login: {
410
- meta: { route: "/login", view: { component: "Login" } },
411
- on: {
412
- login: {
413
- target: "dashboard",
414
- actions: assign({ isAuthenticated: true }),
415
- },
416
- },
417
- },
418
- dashboard: {
419
- meta: { route: "/dashboard", view: { component: "Dashboard" } },
420
- always: {
421
- guard: ({ context }) => !context.isAuthenticated,
422
- target: "login",
423
- },
424
- },
425
- },
426
- });
427
-
428
- const catalog = defineCatalog({
429
- Home,
430
- Login,
431
- Dashboard,
432
- });
433
-
434
- const player = definePlayer({ machine: authMachine, catalog });
67
+ ```tsx
68
+ interface PlayRouterProviderProps<TActor extends PlayActor> {
69
+ /** The actor to sync with Solid Router. */
70
+ actor: TActor;
71
+ /** Bidirectional route map for state ID ↔ URL path lookups. */
72
+ routeMap: RouteMap;
73
+ /**
74
+ * The three Solid Router hook results that drive bidirectional sync.
75
+ * Must be obtained via useNavigate(), useLocation(), and useParams()
76
+ * inside a router context.
77
+ */
78
+ router: SolidRouterHooks;
79
+ /** Render callback — receives the concrete actor type and router hooks. */
80
+ renderer: (actor: TActor, router: SolidRouterHooks) => JSX.Element;
81
+ }
435
82
  ```
436
83
 
437
- **Guard behavior:**
84
+ ### `SolidRouterBridge`
438
85
 
439
- - User navigates to `/dashboard`
440
- - Bridge sends `play.route` event to actor
441
- - Actor's `always` guard checks `isAuthenticated`
442
- - If `false`, actor transitions to `login` state
443
- - Bridge detects state change via `createEffect`, redirects to `/login`
444
- - Actor Authority principle enforced
86
+ Low-level class for manual integration. Extends `RouterBridgeBase` from `@xmachines/play-router` and uses Solid's `createEffect` for reactive router→actor sync.
445
87
 
446
- ### Cleanup: Proper Disposal on Component Unmount
88
+ > **Important:** `connect()` must be called inside a Solid reactive owner (component or `createRoot`). Cleanup is not automatic call `disconnect()` (or `dispose()`) explicitly, typically in `onCleanup()`.
447
89
 
448
90
  ```tsx
449
- import { onCleanup } from "solid-js";
450
- import { SolidRouterBridge } from "@xmachines/play-solid-router";
91
+ import { useNavigate, useLocation, useParams, onCleanup } from "@solidjs/router";
92
+ import { SolidRouterBridge, RouteMap } from "@xmachines/play-solid-router";
451
93
 
452
94
  function App() {
453
95
  const navigate = useNavigate();
454
96
  const location = useLocation();
455
97
  const params = useParams();
456
- const actor = useContext(ActorContext);
457
- const routeMap = useContext(RouteMapContext);
458
98
 
459
- const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
99
+ const routeMap = new RouteMap([
100
+ { stateId: "#home", path: "/" },
101
+ { stateId: "#profile", path: "/profile/:userId" },
102
+ ]);
460
103
 
461
- // CRITICAL: Cleanup effects
462
- onCleanup(() => {
463
- bridge.dispose();
464
- });
104
+ const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
105
+ bridge.connect();
106
+ onCleanup(() => bridge.disconnect());
465
107
 
466
- return <Router>...</Router>;
108
+ return <div>...</div>;
467
109
  }
468
110
  ```
469
111
 
470
- **Why cleanup matters:**
112
+ ### `createRouteMap(machine)`
471
113
 
472
- - `createEffect` subscriptions continue after disposal (memory leak)
473
- - Multiple bridge instances send duplicate events
474
- - Tests fail with "Cannot send to stopped actor" errors
475
- - Solid's fine-grained reactivity tracks disposed components
114
+ Factory that builds a `RouteMap` directly from an XState machine definition. Re-exported from `@xmachines/play-router`.
476
115
 
477
- ## Architecture
116
+ ```ts
117
+ import { createRouteMap } from "@xmachines/play-solid-router";
478
118
 
479
- ### Bidirectional Sync (Actor ↔ Router)
119
+ const routeMap = createRouteMap(myMachine);
120
+ ```
121
+
122
+ ### `RouteMap` / `RouteMapping`
480
123
 
481
- **Actor Router (Signal-driven via createEffect):**
124
+ Bidirectional state ID ↔ URL path mapping. Re-exported from `@xmachines/play-router`.
482
125
 
483
- 1. Actor transitions to new state with `meta.route`
484
- 2. `actor.currentRoute` signal updates
485
- 3. `createEffect(on(...))` fires with new route value
486
- 4. Bridge extracts state ID from signal
487
- 5. Bridge looks up path via `routeMap.getPathByStateId(stateId)`
488
- 6. Bridge calls `navigate(path)`
489
- 7. SolidJS Router updates URL and renders component
126
+ ```ts
127
+ import { RouteMap } from "@xmachines/play-solid-router";
490
128
 
491
- **Router Actor (Location tracking via createEffect):**
129
+ const routeMap = new RouteMap([
130
+ { stateId: "#home", path: "/" },
131
+ { stateId: "#profile", path: "/profile/:userId" },
132
+ { stateId: "#settings", path: "/settings/:section?" },
133
+ ]);
134
+ ```
492
135
 
493
- 1. User clicks link or browser back button
494
- 2. `location.pathname` signal updates
495
- 3. `createEffect(on(...))` fires with new pathname
496
- 4. Bridge looks up state ID via `routeMap.getStateIdByPath(pathname)`
497
- 5. Bridge extracts params from `useParams()` reactive object
498
- 6. Bridge sends `play.route` event to actor
499
- 7. Actor validates navigation (guards, transitions)
500
- 8. If accepted: Actor transitions, signal updates, URL stays
501
- 9. If rejected: Actor redirects, bridge corrects URL via `navigate()`
136
+ ### Types
502
137
 
503
- ### Circular Update Prevention
138
+ | Export | Description |
139
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
140
+ | `PlayActor` | `AbstractActor & Routable & Viewable` — canonical actor shape from `@xmachines/play-router`. Required by `PlayRouterProvider`, which renders the current view spec in addition to synchronizing routes. |
141
+ | `RoutableActor` | Deprecated alias for `PlayActor`. Use `PlayActor` from `@xmachines/play-router` in new code. |
142
+ | `AbstractActor` | Re-exported from `@xmachines/play-actor` for convenience when typing renderer callbacks. |
143
+ | `SolidRouterHooks` | Shape of the `router` prop: `{ navigate, location, params }` |
144
+ | `PlayRouterProviderProps` | Full props interface for `PlayRouterProvider` |
145
+ | `PlayRouteEvent` | Event type sent to the actor on URL change (`play.route`) |
146
+ | `RouterBridge` | Interface implemented by `SolidRouterBridge` |
147
+ | `RouteMapOptions` | Options bag for `RouteMap` construction. Re-exported from `@xmachines/play-router`. |
504
148
 
505
- **Multi-layer guards prevent infinite loops:**
149
+ ## Usage Patterns
506
150
 
507
- 1. **`lastSyncedPath` tracking:** Stores last synchronized path, skips if unchanged
508
- 2. **`isProcessingNavigation` flag:** Set during navigation processing, prevents concurrent syncs
509
- 3. **Effect timing:** Solid's batched updates and `defer: true` option prevent rapid cycles
151
+ ### Protected Routes and Guards
510
152
 
511
- **Signals-native pattern:**
153
+ Auth guards live entirely inside the state machine, preventing flashes of unauthorized content:
512
154
 
513
- ```typescript
514
- // Actor Router
515
- createEffect(
516
- on(
517
- () => this.actor.currentRoute.get(),
518
- (route) => {
519
- if (!route || route === this.lastSyncedPath || this.isProcessingNavigation) {
520
- return;
521
- }
522
- this.lastSyncedPath = route;
523
- this.navigate(route);
524
- },
525
- { defer: true },
526
- ),
527
- );
528
-
529
- // Router → Actor
530
- createEffect(
531
- on(
532
- () => this.location.pathname,
533
- (pathname) => {
534
- if (pathname === this.lastSyncedPath || this.isProcessingNavigation) {
535
- return;
536
- }
537
- this.isProcessingNavigation = true;
538
- this.actor.send({ type: "play.route", to: stateId, params });
539
- this.isProcessingNavigation = false;
155
+ ```ts
156
+ const machineConfig = {
157
+ states: {
158
+ dashboard: {
159
+ meta: { route: "/dashboard" },
160
+ always: ({ context }) => {
161
+ if (context.isAuthenticated) return;
162
+ return { target: "login" };
163
+ },
540
164
  },
541
- { defer: true },
542
- ),
543
- );
165
+ },
166
+ };
544
167
  ```
545
168
 
546
- ### Relationship to Other Packages
169
+ When a user navigates to `/dashboard` while unauthenticated:
547
170
 
548
- **Package Dependencies:**
171
+ 1. Solid Router updates the URL.
172
+ 2. Bridge intercepts and sends `play.route` to the actor.
173
+ 3. Actor evaluates the guard — denies transition, moves to `login` instead.
174
+ 4. Bridge observes new actor route (`/login`) via TC39 Signal.
175
+ 5. Bridge calls `navigate("/login")`.
549
176
 
550
- - `@xmachines/play` - Protocol interfaces (`PlayRouteEvent`, `RouterBridge`)
551
- - `@xmachines/play-actor` - Actor base class with signal protocol
552
- - `@xmachines/play-router` - Route extraction and pattern matching
553
- - `@xmachines/play-signals` - TC39 Signals polyfill for reactivity
554
- - `@xmachines/play-xstate` - XState integration via `definePlayer()`
177
+ ### Dynamic Routes with Parameters
555
178
 
556
- **Architecture Layers:**
179
+ ```ts
180
+ const routeMap = new RouteMap([
181
+ { stateId: "#post", path: "/users/:userId/posts/:postId" },
182
+ { stateId: "#settings", path: "/settings/:section?" },
183
+ ]);
557
184
 
185
+ // Params are extracted from Solid's useParams() and forwarded in the play.route event:
186
+ // { type: "play.route", to: "#post", params: { userId: "123", postId: "456" }, query: {} }
558
187
  ```
559
- ┌─────────────────────────────────────┐
560
- │ Solid Components (View Layer) │
561
- │ - Props include actor reference │
562
- │ - Sends play.route events │
563
- └─────────────────────────────────────┘
564
-
565
- ┌─────────────────────────────────────┐
566
- │ SolidRouterBridge (Adapter) │
567
- │ - createEffect(actor.currentRoute) │
568
- │ - createEffect(location.pathname) │
569
- └─────────────────────────────────────┘
570
- ↕ ↕
571
- ┌─────────────┐ ┌──────────────────┐
572
- │ SolidJS │ │ XMachines Actor │
573
- │ Router │ │ (Business Logic) │
574
- │ (Infra) │ │ │
575
- └─────────────┘ └──────────────────┘
576
- ```
577
-
578
- ### Signals Integration (SolidJS-Specific)
579
188
 
580
- **Why signals-native matters:**
189
+ Path parameters are extracted from Solid's reactive `useParams()` proxy — no URLPattern polyfill is needed for parameterized routes.
581
190
 
582
- - **Zero adaptation:** Solid signals and TC39 Signals share reactive primitives
583
- - **Automatic tracking:** `createEffect(on(...))` tracks dependencies without manual Watcher setup
584
- - **Fine-grained updates:** Only affected components re-render (not full tree)
585
- - **Batched updates:** Solid batches multiple signal changes in single render cycle
191
+ ## Testing
586
192
 
587
- **Hook context requirement (Pitfall 2):**
193
+ Run tests for this package in isolation:
588
194
 
589
- SolidJS hooks (`useNavigate`, `useLocation`, `useParams`) **MUST** be called inside component tree:
590
-
591
- ```tsx
592
- // ❌ WRONG: Bridge created outside component
593
- const navigate = useNavigate(); // ERROR: No reactive context
594
- const bridge = new SolidRouterBridge(navigate, ...);
195
+ ```bash
196
+ # From the monorepo root
197
+ pnpm --filter @xmachines/play-solid-router test
595
198
 
596
- // CORRECT: Bridge created inside component
597
- function App() {
598
- const navigate = useNavigate();
599
- const location = useLocation();
600
- const params = useParams();
601
- const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
602
- onCleanup(() => bridge.dispose());
603
- return <Router>...</Router>;
604
- }
199
+ # Or from this package directory
200
+ pnpm test
605
201
  ```
606
202
 
607
- **Why:** Solid's reactivity system requires reactive ownership context. Hooks create tracked scopes that exist only within component lifecycle.
608
-
609
- ### Pattern Matching for Dynamic Routes
203
+ **Browser tests** (`test/browser/**/*.browser.test.ts`) run against real Chromium via Playwright, covering async sequencing that jsdom cannot faithfully reproduce:
610
204
 
611
- **Bucket-indexed matching via `BaseRouteMap`:**
612
-
613
- Routes are grouped by their first path segment into buckets. On each lookup only the
614
- relevant bucket (plus the wildcard `*` bucket for `:param`-first routes) is scanned —
615
- typically far fewer than all registered routes.
205
+ ```bash
206
+ pnpm exec vitest --config vitest.browser.config.ts --project play-solid-router-browser
207
+ ```
616
208
 
617
- **Supported syntax:**
209
+ Coverage thresholds: **80%** lines, functions, branches, and statements.
618
210
 
619
- - `:param` - Required parameter (e.g., `/profile/:userId` matches `/profile/123`)
620
- - `:param?` - Optional parameter (e.g., `/settings/:section?` matches `/settings` and `/settings/account`)
621
- - Wildcards via `*` (future enhancement)
211
+ ## Related Packages
622
212
 
623
- **Example:**
213
+ - [@xmachines/play-router](../play-router/README.md) — core router primitives and `RouterBridgeBase`
214
+ - [@xmachines/play-tanstack-solid-router](../play-tanstack-solid-router/README.md) — TanStack Solid Router adapter
215
+ - [@xmachines/play-solid](../play-solid/README.md) — SolidJS view renderer
216
+ - [@xmachines/play-xstate](../play-xstate/README.md) — XState v6 player factory
624
217
 
625
- ```typescript
626
- const routeMap = new RouteMap([
627
- { stateId: "#profile", path: "/profile/:userId" },
628
- { stateId: "#settings", path: "/settings/:section?" },
629
- ]);
218
+ ## Learn More
630
219
 
631
- routeMap.getStateIdByPath("/profile/123"); // '#profile'
632
- routeMap.getStateIdByPath("/settings"); // '#settings'
633
- routeMap.getStateIdByPath("/settings/privacy"); // '#settings'
634
- ```
220
+ - [Demo](examples/demo/README.md)
635
221
 
636
222
  ## License
637
223
 
638
- Copyright (c) 2016 [Mikael Karon](mailto:mikael@karon.se). All rights reserved.
639
-
640
- This work is licensed under the terms of the MIT license.
641
- For a copy, see <https://opensource.org/licenses/MIT>.
224
+ MIT see [LICENSE](LICENSE).