@xmachines/play-solid-router 1.0.0-beta.46 → 1.0.0-beta.47

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