@xmachines/play-solid-router 1.0.0-beta.9 → 1.0.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/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
- }
12
+ pnpm add @xmachines/play-solid-router
96
13
  ```
97
14
 
98
- ## API Reference
99
-
100
- ### `SolidRouterBridge`
101
-
102
- Router adapter implementing the `RouterBridge` protocol for SolidJS Router.
103
-
104
- **Type Signature:**
15
+ **Peer dependencies** (must be installed separately):
105
16
 
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
- }
17
+ ```bash
18
+ pnpm add solid-js @solidjs/router xstate
117
19
  ```
118
20
 
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
21
+ - `solid-js` `^1.8.0`
22
+ - `@solidjs/router` `^0.16.1`
23
+ - `xstate` `^5.31.0`
126
24
 
127
- **Methods:**
25
+ ## Quick Start
128
26
 
129
- - `connect()` - Start bidirectional synchronization.
130
- - `disconnect()` - Stop synchronization and cleanup bridge resources.
131
- - `dispose()` - Alias of `disconnect()`.
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";
132
33
 
133
- **Internal Behavior:**
34
+ const actor = definePlayer({ machine: myMachine })();
35
+ actor.start();
134
36
 
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
37
+ const routeMap = createRouteMap(myMachine);
140
38
 
141
- ### `RouteMap`
39
+ const Layout: ParentComponent = () => {
40
+ const navigate = useNavigate();
41
+ const location = useLocation();
42
+ const params = useParams();
142
43
 
143
- Bidirectional mapping between XMachines state IDs and SolidJS Router paths with pattern matching support.
44
+ onCleanup(() => actor.stop());
144
45
 
145
- `RouteMap` extends `BaseRouteMap` from `@xmachines/play-router`, inheriting bucket-indexed
146
- bidirectional route matching. No routing logic lives in the adapter itself.
46
+ return (
47
+ <PlayRouterProvider
48
+ actor={actor}
49
+ routeMap={routeMap}
50
+ router={{ navigate, location, params }}
51
+ renderer={(a, router) => <MyApp actor={a} />}
52
+ />
53
+ );
54
+ };
147
55
 
148
- ```typescript
149
- interface RouteMapping {
150
- readonly stateId: string;
151
- readonly path: string;
56
+ export default function App() {
57
+ return <Router root={Layout}>{/* one <Route> per routable state */}</Router>;
152
58
  }
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
59
  ```
161
60
 
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)
61
+ ## API Summary
174
62
 
175
- **Pattern Matching:**
63
+ ### `PlayRouterProvider`
176
64
 
177
- Uses bucket-indexed RegExp matching for dynamic routes:
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.
178
66
 
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
- );
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;
246
81
  }
247
82
  ```
248
83
 
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";
84
+ ### `SolidRouterBridge`
255
85
 
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
- };
86
+ Low-level class for manual integration. Extends `RouterBridgeBase` from `@xmachines/play-router` and uses Solid's `createEffect` for reactive router→actor sync.
274
87
 
275
- const appMachine = setup({
276
- types: {
277
- context: {} as { userId?: string; section?: string },
278
- events: {} as PlayRouteEvent
279
- }
280
- }).createMachine(formatPlayRouteTransitions(machineConfig));
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()`.
281
89
 
282
- const catalog = defineCatalog({
283
- Profile,
284
- Settings,
285
- });
90
+ ```tsx
91
+ import { useNavigate, useLocation, useParams, onCleanup } from "@solidjs/router";
92
+ import { SolidRouterBridge, RouteMap } from "@xmachines/play-solid-router";
286
93
 
287
- // Router with dynamic routes
288
94
  function App() {
289
- const navigate = useNavigate();
290
- const location = useLocation();
291
- const params = useParams();
292
-
293
- const routeMap = createRouteMap(appMachine);
95
+ const navigate = useNavigate();
96
+ const location = useLocation();
97
+ const params = useParams();
294
98
 
295
- const createPlayer = definePlayer({ machine: appMachine, catalog });
296
- const actor = createPlayer();
297
- actor.start();
298
- 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
+ ]);
299
103
 
300
- onCleanup(() => bridge.dispose());
104
+ const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
105
+ bridge.connect();
106
+ onCleanup(() => bridge.disconnect());
301
107
 
302
- return (
303
- <Router>
304
- <Route path="/profile/:userId" component={Profile} />
305
- <Route path="/settings/:section?" component={Settings} />
306
- </Router>
307
- );
108
+ return <div>...</div>;
308
109
  }
309
110
  ```
310
111
 
311
- **Usage in component:**
112
+ ### `createRouteMap(machine)`
312
113
 
313
- ```tsx
314
- function ProfileButton(props: { userId: string }) {
315
- 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>
327
- );
328
- }
114
+ Factory that builds a `RouteMap` directly from an XState machine definition. Re-exported from `@xmachines/play-router`.
115
+
116
+ ```ts
117
+ import { createRouteMap } from "@xmachines/play-solid-router";
118
+
119
+ const routeMap = createRouteMap(myMachine);
329
120
  ```
330
121
 
331
- ### Query Parameters: Search/Filters via Query Strings
122
+ ### `RouteMap` / `RouteMapping`
332
123
 
333
- ```typescript
334
- // State machine with query param handling
335
- import { formatPlayRouteTransitions } from "@xmachines/play-xstate";
336
- import { defineCatalog } from "@xmachines/play-catalog";
124
+ Bidirectional state ID ↔ URL path mapping. Re-exported from `@xmachines/play-router`.
337
125
 
338
- const machineConfig = {
339
- context: { query: '', filters: {} },
340
- states: {
341
- search: {
342
- meta: {
343
- route: '/search',
344
- view: { component: 'Search' },
345
- },
346
- }
347
- }
348
- };
126
+ ```ts
127
+ import { RouteMap } from "@xmachines/play-solid-router";
349
128
 
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
- );
384
- }
129
+ const routeMap = new RouteMap([
130
+ { stateId: "#home", path: "/" },
131
+ { stateId: "#profile", path: "/profile/:userId" },
132
+ { stateId: "#settings", path: "/settings/:section?" },
133
+ ]);
385
134
  ```
386
135
 
387
- **SolidJS Router automatically reflects query params in URL:**
136
+ ### Types
388
137
 
389
- - `/search?q=xmachines&tag=typescript`
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`. |
390
148
 
391
- ### Protected Routes: Authentication Guards
149
+ ## Usage Patterns
392
150
 
393
- ```typescript
394
- // State machine with auth guards
395
- import { defineCatalog } from "@xmachines/play-catalog";
151
+ ### Protected Routes and Guards
396
152
 
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",
153
+ Auth guards live entirely inside the state machine, preventing flashes of unauthorized content:
154
+
155
+ ```ts
156
+ const machineConfig = {
405
157
  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
158
  dashboard: {
419
- meta: { route: "/dashboard", view: { component: "Dashboard" } },
159
+ meta: { route: "/dashboard" },
420
160
  always: {
421
161
  guard: ({ context }) => !context.isAuthenticated,
422
162
  target: "login",
423
163
  },
424
164
  },
425
165
  },
426
- });
427
-
428
- const catalog = defineCatalog({
429
- Home,
430
- Login,
431
- Dashboard,
432
- });
433
-
434
- const player = definePlayer({ machine: authMachine, catalog });
435
- ```
436
-
437
- **Guard behavior:**
438
-
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
445
-
446
- ### Cleanup: Proper Disposal on Component Unmount
447
-
448
- ```tsx
449
- import { onCleanup } from "solid-js";
450
- import { SolidRouterBridge } from "@xmachines/play-solid-router";
451
-
452
- function App() {
453
- const navigate = useNavigate();
454
- const location = useLocation();
455
- const params = useParams();
456
- const actor = useContext(ActorContext);
457
- const routeMap = useContext(RouteMapContext);
458
-
459
- const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
460
-
461
- // CRITICAL: Cleanup effects
462
- onCleanup(() => {
463
- bridge.dispose();
464
- });
465
-
466
- return <Router>...</Router>;
467
- }
166
+ };
468
167
  ```
469
168
 
470
- **Why cleanup matters:**
471
-
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
476
-
477
- ## Architecture
478
-
479
- ### Bidirectional Sync (Actor ↔ Router)
169
+ When a user navigates to `/dashboard` while unauthenticated:
480
170
 
481
- **Actor Router (Signal-driven via createEffect):**
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")`.
482
176
 
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
177
+ ### Dynamic Routes with Parameters
490
178
 
491
- **Router → Actor (Location tracking via createEffect):**
492
-
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()`
502
-
503
- ### Circular Update Prevention
504
-
505
- **Multi-layer guards prevent infinite loops:**
506
-
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
510
-
511
- **Signals-native pattern:**
179
+ ```ts
180
+ const routeMap = new RouteMap([
181
+ { stateId: "#post", path: "/users/:userId/posts/:postId" },
182
+ { stateId: "#settings", path: "/settings/:section?" },
183
+ ]);
512
184
 
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;
540
- },
541
- { defer: true },
542
- ),
543
- );
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: {} }
544
187
  ```
545
188
 
546
- ### Relationship to Other Packages
189
+ Path parameters are extracted from Solid's reactive `useParams()` proxy — no URLPattern polyfill is needed for parameterized routes.
547
190
 
548
- **Package Dependencies:**
191
+ ## Testing
549
192
 
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()`
193
+ Run tests for this package in isolation:
555
194
 
556
- **Architecture Layers:**
195
+ ```bash
196
+ # From the monorepo root
197
+ pnpm --filter @xmachines/play-solid-router test
557
198
 
199
+ # Or from this package directory
200
+ pnpm test
558
201
  ```
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
-
580
- **Why signals-native matters:**
581
-
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
586
-
587
- **Hook context requirement (Pitfall 2):**
588
202
 
589
- SolidJS hooks (`useNavigate`, `useLocation`, `useParams`) **MUST** be called inside component tree:
203
+ **Browser tests** (`test/browser/**/*.browser.test.ts`) run against real Chromium via Playwright, covering async sequencing that jsdom cannot faithfully reproduce:
590
204
 
591
- ```tsx
592
- // WRONG: Bridge created outside component
593
- const navigate = useNavigate(); // ERROR: No reactive context
594
- const bridge = new SolidRouterBridge(navigate, ...);
595
-
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
- }
205
+ ```bash
206
+ pnpm exec vitest --config vitest.browser.config.ts --project play-solid-router-browser
605
207
  ```
606
208
 
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
209
+ Coverage thresholds: **80%** lines, functions, branches, and statements.
610
210
 
611
- **Bucket-indexed matching via `BaseRouteMap`:**
211
+ ## Related Packages
612
212
 
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.
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 v5 player factory
616
217
 
617
- **Supported syntax:**
218
+ ## Learn More
618
219
 
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)
622
-
623
- **Example:**
624
-
625
- ```typescript
626
- const routeMap = new RouteMap([
627
- { stateId: "#profile", path: "/profile/:userId" },
628
- { stateId: "#settings", path: "/settings/:section?" },
629
- ]);
630
-
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).