@xmachines/play-solid-router 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/README.md CHANGED
@@ -1,641 +1,225 @@
1
1
  # @xmachines/play-solid-router
2
2
 
3
- **SolidJS Router adapter for XMachines Universal Player Architecture**
3
+ 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`.
4
4
 
5
- SolidJS Router adapter using `RouterBridgeBase` for consistent actor↔router sync.
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-solid-router)
6
7
 
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:**
18
-
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
24
-
25
- **Framework Compatibility:**
26
-
27
- - SolidJS 1.8.4+ (signals-native architecture)
28
- - @solidjs/router 0.13.0+ (modern routing primitives)
29
- - TC39 Signals polyfill integration
8
+ Part of the [xmachines-js monorepo](../../README.md).
30
9
 
31
10
  ## Installation
32
11
 
33
12
  ```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
- }
13
+ pnpm add @xmachines/play-solid-router
96
14
  ```
97
15
 
98
- ## API Reference
16
+ **Peer dependencies** (must be installed separately):
99
17
 
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
- }
18
+ ```bash
19
+ pnpm add solid-js @solidjs/router xstate
117
20
  ```
118
21
 
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
22
+ - `solid-js` `^1.8.0`
23
+ - `@solidjs/router` `^0.16.1`
24
+ - `xstate` `^5.31.0`
126
25
 
127
- **Methods:**
26
+ ## Quick Start
128
27
 
129
- - `connect()` - Start bidirectional synchronization.
130
- - `disconnect()` - Stop synchronization and cleanup bridge resources.
131
- - `dispose()` - Alias of `disconnect()`.
28
+ ```tsx
29
+ import { Router, Route, useNavigate, useLocation, useParams } from "@solidjs/router";
30
+ import { onCleanup, type ParentComponent } from "solid-js";
31
+ import { PlayRouterProvider, createRouteMap } from "@xmachines/play-solid-router";
32
+ import { definePlayer } from "@xmachines/play-xstate";
33
+ import { myMachine } from "./machine.js";
132
34
 
133
- **Internal Behavior:**
35
+ const actor = definePlayer({ machine: myMachine })();
36
+ actor.start();
134
37
 
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
38
+ const routeMap = createRouteMap(myMachine);
140
39
 
141
- ### `RouteMap`
40
+ const Layout: ParentComponent = () => {
41
+ const navigate = useNavigate();
42
+ const location = useLocation();
43
+ const params = useParams();
142
44
 
143
- Bidirectional mapping between XMachines state IDs and SolidJS Router paths with pattern matching support.
45
+ onCleanup(() => actor.stop());
144
46
 
145
- `RouteMap` extends `BaseRouteMap` from `@xmachines/play-router`, inheriting bucket-indexed
146
- bidirectional route matching. No routing logic lives in the adapter itself.
47
+ return (
48
+ <PlayRouterProvider
49
+ actor={actor}
50
+ routeMap={routeMap}
51
+ router={{ navigate, location, params }}
52
+ renderer={(a, router) => <MyApp actor={a} />}
53
+ />
54
+ );
55
+ };
147
56
 
148
- ```typescript
149
- interface RouteMapping {
150
- readonly stateId: string;
151
- readonly path: string;
57
+ export default function App() {
58
+ return <Router root={Layout}>{/* one <Route> per routable state */}</Router>;
152
59
  }
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
60
  ```
161
61
 
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:
62
+ ## API Summary
178
63
 
179
- ```typescript
180
- const routeMap = new RouteMap([{ stateId: "#settings", path: "/settings/:section?" }]);
64
+ ### `PlayRouterProvider`
181
65
 
182
- routeMap.getStateIdByPath("/settings"); // '#settings'
183
- routeMap.getStateIdByPath("/settings/account"); // '#settings'
184
- routeMap.getStateIdByPath("/settings/privacy"); // '#settings'
185
- routeMap.getStateIdByPath("/other"); // null
186
- ```
66
+ 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.
187
67
 
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
- );
68
+ ```tsx
69
+ interface PlayRouterProviderProps<TActor extends PlayActor> {
70
+ /** The actor to sync with Solid Router. */
71
+ actor: TActor;
72
+ /** Bidirectional route map for state ID ↔ URL path lookups. */
73
+ routeMap: RouteMap;
74
+ /**
75
+ * The three Solid Router hook results that drive bidirectional sync.
76
+ * Must be obtained via useNavigate(), useLocation(), and useParams()
77
+ * inside a router context.
78
+ */
79
+ router: SolidRouterHooks;
80
+ /** Render callback — receives the concrete actor type and router hooks. */
81
+ renderer: (actor: TActor, router: SolidRouterHooks) => JSX.Element;
246
82
  }
247
83
  ```
248
84
 
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";
85
+ ### `SolidRouterBridge`
255
86
 
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
- };
87
+ Low-level class for manual integration. Extends `RouterBridgeBase` from `@xmachines/play-router` and uses Solid's `createEffect` for reactive router→actor sync.
274
88
 
275
- const appMachine = setup({
276
- types: {
277
- context: {} as { userId?: string; section?: string },
278
- events: {} as PlayRouteEvent
279
- }
280
- }).createMachine(formatPlayRouteTransitions(machineConfig));
89
+ > **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
90
 
282
- const catalog = defineCatalog({
283
- Profile,
284
- Settings,
285
- });
91
+ ```tsx
92
+ import { useNavigate, useLocation, useParams, onCleanup } from "@solidjs/router";
93
+ import { SolidRouterBridge, RouteMap } from "@xmachines/play-solid-router";
286
94
 
287
- // Router with dynamic routes
288
95
  function App() {
289
- const navigate = useNavigate();
290
- const location = useLocation();
291
- const params = useParams();
292
-
293
- const routeMap = createRouteMap(appMachine);
96
+ const navigate = useNavigate();
97
+ const location = useLocation();
98
+ const params = useParams();
294
99
 
295
- const createPlayer = definePlayer({ machine: appMachine, catalog });
296
- const actor = createPlayer();
297
- actor.start();
298
- const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
100
+ const routeMap = new RouteMap([
101
+ { stateId: "#home", path: "/" },
102
+ { stateId: "#profile", path: "/profile/:userId" },
103
+ ]);
299
104
 
300
- onCleanup(() => bridge.dispose());
105
+ const bridge = new SolidRouterBridge(navigate, location, params, actor, routeMap);
106
+ bridge.connect();
107
+ onCleanup(() => bridge.disconnect());
301
108
 
302
- return (
303
- <Router>
304
- <Route path="/profile/:userId" component={Profile} />
305
- <Route path="/settings/:section?" component={Settings} />
306
- </Router>
307
- );
109
+ return <div>...</div>;
308
110
  }
309
111
  ```
310
112
 
311
- **Usage in component:**
113
+ ### `createRouteMap(machine)`
312
114
 
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
- }
115
+ Factory that builds a `RouteMap` directly from an XState machine definition. Re-exported from `@xmachines/play-router`.
116
+
117
+ ```ts
118
+ import { createRouteMap } from "@xmachines/play-solid-router";
119
+
120
+ const routeMap = createRouteMap(myMachine);
329
121
  ```
330
122
 
331
- ### Query Parameters: Search/Filters via Query Strings
123
+ ### `RouteMap` / `RouteMapping`
332
124
 
333
- ```typescript
334
- // State machine with query param handling
335
- import { formatPlayRouteTransitions } from "@xmachines/play-xstate";
336
- import { defineCatalog } from "@xmachines/play-catalog";
125
+ Bidirectional state ID ↔ URL path mapping. Re-exported from `@xmachines/play-router`.
337
126
 
338
- const machineConfig = {
339
- context: { query: '', filters: {} },
340
- states: {
341
- search: {
342
- meta: {
343
- route: '/search',
344
- view: { component: 'Search' },
345
- },
346
- }
347
- }
348
- };
127
+ ```ts
128
+ import { RouteMap } from "@xmachines/play-solid-router";
349
129
 
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
- }
130
+ const routeMap = new RouteMap([
131
+ { stateId: "#home", path: "/" },
132
+ { stateId: "#profile", path: "/profile/:userId" },
133
+ { stateId: "#settings", path: "/settings/:section?" },
134
+ ]);
385
135
  ```
386
136
 
387
- **SolidJS Router automatically reflects query params in URL:**
137
+ ### Types
388
138
 
389
- - `/search?q=xmachines&tag=typescript`
139
+ | Export | Description |
140
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
141
+ | `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. |
142
+ | `RoutableActor` | Deprecated alias for `PlayActor`. Use `PlayActor` from `@xmachines/play-router` in new code. |
143
+ | `AbstractActor` | Re-exported from `@xmachines/play-actor` for convenience when typing renderer callbacks. |
144
+ | `SolidRouterHooks` | Shape of the `router` prop: `{ navigate, location, params }` |
145
+ | `PlayRouterProviderProps` | Full props interface for `PlayRouterProvider` |
146
+ | `PlayRouteEvent` | Event type sent to the actor on URL change (`play.route`) |
147
+ | `RouterBridge` | Interface implemented by `SolidRouterBridge` |
148
+ | `RouteMapOptions` | Options bag for `RouteMap` construction. Re-exported from `@xmachines/play-router`. |
390
149
 
391
- ### Protected Routes: Authentication Guards
150
+ ## Usage Patterns
392
151
 
393
- ```typescript
394
- // State machine with auth guards
395
- import { defineCatalog } from "@xmachines/play-catalog";
152
+ ### Protected Routes and Guards
396
153
 
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",
154
+ Auth guards live entirely inside the state machine, preventing flashes of unauthorized content:
155
+
156
+ ```ts
157
+ const machineConfig = {
405
158
  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
159
  dashboard: {
419
- meta: { route: "/dashboard", view: { component: "Dashboard" } },
160
+ meta: { route: "/dashboard" },
420
161
  always: {
421
162
  guard: ({ context }) => !context.isAuthenticated,
422
163
  target: "login",
423
164
  },
424
165
  },
425
166
  },
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
- }
167
+ };
468
168
  ```
469
169
 
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)
170
+ When a user navigates to `/dashboard` while unauthenticated:
480
171
 
481
- **Actor Router (Signal-driven via createEffect):**
172
+ 1. Solid Router updates the URL.
173
+ 2. Bridge intercepts and sends `play.route` to the actor.
174
+ 3. Actor evaluates the guard — denies transition, moves to `login` instead.
175
+ 4. Bridge observes new actor route (`/login`) via TC39 Signal.
176
+ 5. Bridge calls `navigate("/login")`.
482
177
 
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
178
+ ### Dynamic Routes with Parameters
490
179
 
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:**
180
+ ```ts
181
+ const routeMap = new RouteMap([
182
+ { stateId: "#post", path: "/users/:userId/posts/:postId" },
183
+ { stateId: "#settings", path: "/settings/:section?" },
184
+ ]);
512
185
 
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
- );
186
+ // Params are extracted from Solid's useParams() and forwarded in the play.route event:
187
+ // { type: "play.route", to: "#post", params: { userId: "123", postId: "456" }, query: {} }
544
188
  ```
545
189
 
546
- ### Relationship to Other Packages
190
+ Path parameters are extracted from Solid's reactive `useParams()` proxy — no URLPattern polyfill is needed for parameterized routes.
547
191
 
548
- **Package Dependencies:**
192
+ ## Testing
549
193
 
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()`
194
+ Run tests for this package in isolation:
555
195
 
556
- **Architecture Layers:**
196
+ ```bash
197
+ # From the monorepo root
198
+ pnpm --filter @xmachines/play-solid-router test
557
199
 
200
+ # Or from this package directory
201
+ pnpm test
558
202
  ```
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
203
 
589
- SolidJS hooks (`useNavigate`, `useLocation`, `useParams`) **MUST** be called inside component tree:
204
+ **Browser tests** (`test/browser/**/*.browser.test.ts`) run against real Chromium via Playwright, covering async sequencing that jsdom cannot faithfully reproduce:
590
205
 
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
- }
206
+ ```bash
207
+ pnpm exec vitest --config vitest.browser.config.ts --project play-solid-router-browser
605
208
  ```
606
209
 
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
210
+ Coverage thresholds: **80%** lines, functions, branches, and statements.
610
211
 
611
- **Bucket-indexed matching via `BaseRouteMap`:**
212
+ ## Related Packages
612
213
 
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.
214
+ - [@xmachines/play-router](../play-router/README.md) core router primitives and `RouterBridgeBase`
215
+ - [@xmachines/play-tanstack-solid-router](../play-tanstack-solid-router/README.md) TanStack Solid Router adapter
216
+ - [@xmachines/play-solid](../play-solid/README.md) SolidJS view renderer
217
+ - [@xmachines/play-xstate](../play-xstate/README.md) — XState v5 player factory
616
218
 
617
- **Supported syntax:**
219
+ ## Learn More
618
220
 
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
- ```
221
+ - [Demo](examples/demo/README.md)
635
222
 
636
223
  ## License
637
224
 
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>.
225
+ MIT see [LICENSE](LICENSE).