@xmachines/play-router 1.0.0-beta.4 → 1.0.0-beta.41

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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +220 -57
  3. package/dist/base-route-map.d.ts +123 -0
  4. package/dist/base-route-map.d.ts.map +1 -0
  5. package/dist/base-route-map.js +172 -0
  6. package/dist/base-route-map.js.map +1 -0
  7. package/dist/build-tree.d.ts.map +1 -1
  8. package/dist/build-tree.js +7 -5
  9. package/dist/build-tree.js.map +1 -1
  10. package/dist/create-route-map-from-machine.d.ts +24 -0
  11. package/dist/create-route-map-from-machine.d.ts.map +1 -0
  12. package/dist/create-route-map-from-machine.js +32 -0
  13. package/dist/create-route-map-from-machine.js.map +1 -0
  14. package/dist/create-route-map-from-tree.d.ts +31 -0
  15. package/dist/create-route-map-from-tree.d.ts.map +1 -0
  16. package/dist/create-route-map-from-tree.js +42 -0
  17. package/dist/create-route-map-from-tree.js.map +1 -0
  18. package/dist/create-route-map.d.ts +29 -9
  19. package/dist/create-route-map.d.ts.map +1 -1
  20. package/dist/create-route-map.js +36 -26
  21. package/dist/create-route-map.js.map +1 -1
  22. package/dist/errors.d.ts +120 -0
  23. package/dist/errors.d.ts.map +1 -0
  24. package/dist/errors.js +150 -0
  25. package/dist/errors.js.map +1 -0
  26. package/dist/extract-routes.d.ts +5 -31
  27. package/dist/extract-routes.d.ts.map +1 -1
  28. package/dist/extract-routes.js +70 -49
  29. package/dist/extract-routes.js.map +1 -1
  30. package/dist/find-route.d.ts +44 -0
  31. package/dist/find-route.d.ts.map +1 -0
  32. package/dist/find-route.js +98 -0
  33. package/dist/find-route.js.map +1 -0
  34. package/dist/index.d.ts +12 -51
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +15 -133
  37. package/dist/index.js.map +1 -1
  38. package/dist/machine-to-graph.d.ts +17 -0
  39. package/dist/machine-to-graph.d.ts.map +1 -0
  40. package/dist/machine-to-graph.js +115 -0
  41. package/dist/machine-to-graph.js.map +1 -0
  42. package/dist/query.d.ts +44 -1
  43. package/dist/query.d.ts.map +1 -1
  44. package/dist/query.js +80 -3
  45. package/dist/query.js.map +1 -1
  46. package/dist/router-bridge-base.d.ts +104 -20
  47. package/dist/router-bridge-base.d.ts.map +1 -1
  48. package/dist/router-bridge-base.js +188 -68
  49. package/dist/router-bridge-base.js.map +1 -1
  50. package/dist/router-sync.d.ts +87 -0
  51. package/dist/router-sync.d.ts.map +1 -0
  52. package/dist/router-sync.js +132 -0
  53. package/dist/router-sync.js.map +1 -0
  54. package/dist/types.d.ts +94 -20
  55. package/dist/types.d.ts.map +1 -1
  56. package/dist/url-pattern-utils.d.ts +60 -0
  57. package/dist/url-pattern-utils.d.ts.map +1 -0
  58. package/dist/url-pattern-utils.js +77 -0
  59. package/dist/url-pattern-utils.js.map +1 -0
  60. package/dist/validate-routes.d.ts +10 -10
  61. package/dist/validate-routes.d.ts.map +1 -1
  62. package/dist/validate-routes.js +16 -16
  63. package/dist/validate-routes.js.map +1 -1
  64. package/package.json +39 -14
  65. package/dist/connect-router.d.ts +0 -56
  66. package/dist/connect-router.d.ts.map +0 -1
  67. package/dist/connect-router.js +0 -119
  68. package/dist/connect-router.js.map +0 -1
  69. package/dist/crawl-machine.d.ts +0 -74
  70. package/dist/crawl-machine.d.ts.map +0 -1
  71. package/dist/crawl-machine.js +0 -95
  72. package/dist/crawl-machine.js.map +0 -1
  73. package/dist/create-browser-history.d.ts +0 -68
  74. package/dist/create-browser-history.d.ts.map +0 -1
  75. package/dist/create-browser-history.js +0 -94
  76. package/dist/create-browser-history.js.map +0 -1
  77. package/dist/create-router.d.ts +0 -73
  78. package/dist/create-router.d.ts.map +0 -1
  79. package/dist/create-router.js +0 -63
  80. package/dist/create-router.js.map +0 -1
  81. package/dist/extract-route.d.ts +0 -25
  82. package/dist/extract-route.d.ts.map +0 -1
  83. package/dist/extract-route.js +0 -63
  84. package/dist/extract-route.js.map +0 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mikael Karon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,17 +2,19 @@
2
2
 
3
3
  **Route tree extraction from XState v5 state machines with routing patterns**
4
4
 
5
- BFS graph crawling and bidirectional route lookup enabling Actor Authority over navigation.
5
+ Graph-based route extraction and bidirectional lookup enabling Actor Authority over navigation.
6
6
 
7
7
  ## Overview
8
8
 
9
- `@xmachines/play-router` extracts route trees from XState state machines by crawling the state graph using breadth-first traversal. It extracts `meta.route` paths from state machines and builds hierarchical route trees with bidirectional state ID ↔ path mapping.
9
+ `@xmachines/play-router` extracts route trees from XState state machines using `@statelyai/graph` a typed, JSON-serializable graph library. It converts machines to a directed `Graph` with hierarchy, transition edges, and route metadata, then builds hierarchical `RouteTree` structures with bidirectional state ID ↔ path mapping.
10
10
 
11
11
  It also exports `RouterBridgeBase`, the shared base class used by framework adapters to implement `RouterBridge` with consistent actor↔router synchronization behavior.
12
12
 
13
13
  `RouterBridgeBase` is the policy point; framework adapters are thin ports that implement only framework-specific navigate/subscribe/unsubscribe behavior.
14
14
 
15
- Per [RFC Play v1](https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md), this package implements:
15
+ The low-level `connectRouter()` from `@xmachines/play-dom-router` uses the same router-to-actor event builder and route-map match helper as `RouterBridgeBase`, so pathname sanitization, state-id normalization, param extraction, and query extraction stay aligned across vanilla and framework adapters.
16
+
17
+ Per [Play RFC](../docs/rfc/play.md), this package implements:
16
18
 
17
19
  - **Actor Authority (INV-01):** Routes derive from machine definitions, not external configuration
18
20
 
@@ -27,13 +29,32 @@ npm install @xmachines/play-router
27
29
 
28
30
  **Peer dependencies:**
29
31
 
30
- - `xstate` ^5.0.0 - State machine runtime
32
+ - `xstate` ^5.0.0 State machine runtime
33
+
34
+ **URLPattern polyfill (Node.js < 24 / older browsers):**
35
+
36
+ `@xmachines/play-router` uses the [URLPattern API](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) for dynamic route matching. URLPattern is available natively on Node.js 24+ and modern browsers (Chrome 95+, Firefox 117+, Safari 16.4+).
37
+
38
+ On environments without native support, load a polyfill **before** importing this package:
39
+
40
+ ```typescript
41
+ // Entry point — must run before any @xmachines/play-router import
42
+ import "urlpattern-polyfill";
43
+ ```
44
+
45
+ Install the polyfill:
46
+
47
+ ```bash
48
+ npm install urlpattern-polyfill
49
+ ```
50
+
51
+ `urlpattern-polyfill` is declared as an optional peer dependency. Package managers will not install it automatically — consumers must install and load it when their runtime lacks native URLPattern support (Node.js < 24, older browsers).
31
52
 
32
53
  ## Quick Start
33
54
 
34
55
  ```typescript
35
56
  import { createMachine } from "xstate";
36
- import { extractMachineRoutes } from "@xmachines/play-router";
57
+ import { extractMachineRoutes, findRouteByPath } from "@xmachines/play-router";
37
58
 
38
59
  // Route pattern (recommended)
39
60
  const machine = createMachine({
@@ -65,11 +86,11 @@ const machine = createMachine({
65
86
  const tree = extractMachineRoutes(machine);
66
87
 
67
88
  // Bidirectional lookup
68
- console.log(tree.byPath.get("/dashboard/overview")); // RouteNode
69
- console.log(tree.byId.get("overview")); // RouteNode
89
+ console.log(findRouteByPath(tree, "/overview")); // RouteNode
90
+ console.log(tree.byStateId.get("overview")); // RouteNode
70
91
 
71
92
  // Pattern matching for dynamic routes
72
- const settingsRoute = tree.byPath.get("/settings/profile");
93
+ const settingsRoute = findRouteByPath(tree, "/settings/profile");
73
94
  console.log(settingsRoute?.id); // "settings"
74
95
  ```
75
96
 
@@ -88,9 +109,7 @@ The demo demonstrates:
88
109
  **Run the demo:**
89
110
 
90
111
  ```bash
91
- cd packages/play-router/examples/demo
92
- npm install
93
- npm run dev
112
+ npm run dev -w @xmachines/play-dom-router-demo
94
113
  ```
95
114
 
96
115
  Open http://localhost:5174/ and explore:
@@ -105,7 +124,7 @@ Open http://localhost:5174/ and explore:
105
124
  ```typescript
106
125
  // Extract routes from machine
107
126
  const routeTree = extractMachineRoutes(authMachine);
108
- const routeMap = createRouteMap(routeTree);
127
+ const routeMatcher = createRouteMap(routeTree);
109
128
 
110
129
  // Actor → URL sync
111
130
  const watcher = new Signal.subtle.Watcher(() => {
@@ -122,7 +141,7 @@ const watcher = new Signal.subtle.Watcher(() => {
122
141
  // URL → Actor sync (with pattern matching)
123
142
  window.addEventListener("popstate", () => {
124
143
  const path = window.location.pathname;
125
- const { to, params } = routeMap.resolve(path);
144
+ const { to, params } = routeMatcher.match(path);
126
145
  if (to) {
127
146
  actor.send({ type: "play.route", to, params });
128
147
  }
@@ -131,7 +150,7 @@ window.addEventListener("popstate", () => {
131
150
  // Initial URL handling
132
151
  const initialPath = window.location.pathname;
133
152
  if (initialPath !== "/") {
134
- const { to, params } = routeMap.resolve(initialPath);
153
+ const { to, params } = routeMatcher.match(initialPath);
135
154
  if (to) {
136
155
  actor.send({ type: "play.route", to, params });
137
156
  }
@@ -159,6 +178,34 @@ Bridge teardown must be explicit and deterministic:
159
178
  - `disconnect`/`dispose` must unwatch signal subscriptions and unhook router listeners.
160
179
  - Do not rely on GC-only cleanup guidance.
161
180
  - Infrastructure remains passive: bridges observe and forward intents, actors decide validity.
181
+ - `createBrowserHistory().destroy()` (from `@xmachines/play-dom-router`) is idempotent and restores shared `window.history` patches only after the last wrapper for that window is removed.
182
+ - **Note**: `createBrowserHistory()` mutates global `window.history` methods (`pushState` and `replaceState`) and coordinates wrappers with shared ref-count state. To reduce leakage, create one history wrapper per browser window at the application boundary and always pair it with `destroy()` during teardown.
183
+
184
+ ## Bridge Sync Ordering
185
+
186
+ `RouterBridgeBase.connect()` has an intentional ordering contract used by all framework adapters:
187
+
188
+ - seed `lastSyncedPath` from `actor.currentRoute` in the constructor
189
+ - install the actor route watcher
190
+ - install adapter router subscriptions
191
+ - resolve initial sync using `getInitialRouterPath()` and `actor.initialRoute`
192
+
193
+ That final step distinguishes:
194
+
195
+ - **deep-link:** browser URL differs from the machine's initial route, so router wins and the actor receives `play.route`
196
+ - **restore:** browser URL is still at the machine's initial route while the actor was restored elsewhere, so actor wins and the bridge pushes the actor route back into the router
197
+
198
+ Actor-originated router sync also keeps `isProcessingNavigation` set until the next microtask. That short window intentionally suppresses synchronous router callbacks that echo the same navigation back into the actor.
199
+
200
+ ## Diagnostics
201
+
202
+ Router infrastructure reports runtime failures through `PlayDiagnostics` from `@xmachines/play`.
203
+
204
+ - Default behavior uses `consoleDiagnostics`.
205
+ - Messages follow a stable `[scope:code] message` format.
206
+ - Example: `[RouterBridgeBase:PLAY_ROUTER_SYNC_FAILED] Failed to sync actor state from router location.`
207
+
208
+ This keeps router errors observable while letting applications swap in their own diagnostics implementation upstream.
162
209
 
163
210
  ## API Reference
164
211
 
@@ -176,9 +223,10 @@ const tree = extractMachineRoutes(machine: AnyStateMachine): RouteTree;
176
223
 
177
224
  **Returns:** `RouteTree` with:
178
225
 
179
- - `routes: RouteNode[]` - Array of route nodes
180
- - `byPath: Map<string, RouteNode>` - URL path → route node
181
- - `byId: Map<string, RouteNode>` - State ID → route node
226
+ - `byPath: Map<string, RouteNode>` URL path route node
227
+ - `byStateId: Map<string, RouteNode>` State ID → route node
228
+ - `root: RouteNode` synthetic root node
229
+ - `graph: MachineGraph` — `@statelyai/graph` Graph for hierarchy queries, reachability checks, and transition-aware navigation
182
230
 
183
231
  **Throws:** Error if routes are invalid (malformed paths, missing state IDs, duplicates)
184
232
 
@@ -189,58 +237,112 @@ import { extractMachineRoutes } from "@xmachines/play-router";
189
237
 
190
238
  const tree = extractMachineRoutes(authMachine);
191
239
 
192
- // Query routes
193
- const loginRoute = tree.byId.get("login");
194
- console.log(loginRoute?.path); // "/login"
240
+ // Query routes — use fullPath for URL matching (path is the raw meta.route segment)
241
+ const loginRoute = tree.byStateId.get("login");
242
+ console.log(loginRoute?.fullPath); // "/login"
243
+ console.log(loginRoute?.path); // "/login" (same for absolute routes)
244
+
245
+ // For nested relative routes the distinction matters:
246
+ // meta.route: "overview" under "/dashboard" → path: "overview", fullPath: "/dashboard/overview"
247
+ const overviewRoute = tree.byStateId.get("overview");
248
+ console.log(overviewRoute?.path); // "overview" (raw segment — relative)
249
+ console.log(overviewRoute?.fullPath); // "/dashboard/overview" (resolved — use this)
195
250
 
196
251
  const dashboardRoute = tree.byPath.get("/dashboard");
197
252
  console.log(dashboardRoute?.id); // "dashboard"
198
253
  ```
199
254
 
200
- ### crawlMachine()
255
+ ### machineToGraph()
201
256
 
202
- Low-level BFS traversal of state machine graph:
257
+ Converts an XState v5 machine to a typed `@statelyai/graph` `Graph`:
203
258
 
204
259
  ```typescript
205
- const visits = crawlMachine(machine: AnyStateMachine): StateVisit[];
206
- ```
260
+ import { machineToGraph } from "@xmachines/play-router";
261
+ import { getChildren, getSuccessors, hasPath } from "@statelyai/graph";
207
262
 
208
- **Returns:** Array of state visits in breadth-first order with:
263
+ const graph = machineToGraph(machine);
209
264
 
210
- - `path: string[]` - State path (e.g., ["dashboard", "settings"])
211
- - `parent: StateNode | null` - Parent state node
212
- - `node: StateNode` - Current state node
265
+ // Hierarchy queries
266
+ const children = getChildren(graph, "myMachine");
267
+ const successors = getSuccessors(graph, "myMachine.home"); // transition-reachable states
213
268
 
214
- **Example:**
269
+ // Reachability
270
+ const reachable = hasPath(graph, "myMachine.home", "myMachine.dashboard");
271
+ ```
272
+
273
+ The graph is also available on any `RouteTree` returned by `extractMachineRoutes()`:
215
274
 
216
275
  ```typescript
217
- import { crawlMachine } from "@xmachines/play-router";
276
+ const tree = extractMachineRoutes(machine);
218
277
 
219
- const visits = crawlMachine(machine);
220
- visits.forEach((visit) => {
221
- console.log("State:", visit.path.join("."));
222
- console.log("Parent:", visit.parent?.id ?? "root");
223
- });
278
+ // Use @statelyai/graph algorithms directly on the graph
279
+ const successors = getSuccessors(tree.graph!, "myMachine.home");
280
+ ```
281
+
282
+ **Node data** (`MachineNodeData`):
283
+
284
+ - `stateId: string` — XState state ID (e.g., `"myMachine.dashboard.overview"`)
285
+ - `type` — `"atomic" | "compound" | "parallel" | "final" | "history"`
286
+ - `meta?: Record<string, unknown>` — original state meta object
287
+ - `route?: string` — extracted route path from `meta.route`
288
+
289
+ **Edge data** (`MachineEdgeData`):
290
+
291
+ - `eventType: string` — event type triggering this transition
292
+ - `guardType?: string` — guard name/description (if transition is guarded)
293
+
294
+ ### createRouteMapFromMachine() / createRouteMapFromTree()
295
+
296
+ Both build a `BaseRouteMap` using `node.fullPath` (absolute resolved paths) for browser URL matching.
297
+
298
+ ```typescript
299
+ // Single-call form — preferred for XState machines:
300
+ import { createRouteMapFromMachine } from "@xmachines/play-router";
301
+ const routeMap = createRouteMapFromMachine(machine);
302
+
303
+ // Two-step form — used by framework adapter packages:
304
+ import { extractMachineRoutes, createRouteMapFromTree } from "@xmachines/play-router";
305
+ const routeTree = extractMachineRoutes(machine);
306
+ const routeMap = createRouteMapFromTree(routeTree);
224
307
  ```
225
308
 
309
+ Both forms produce identical results. `createRouteMapFromTree` is exposed for framework adapters (React Router, TanStack Router) that need to hold the `RouteTree` separately for other purposes (e.g. graph queries).
310
+
311
+ **`node.path` vs `node.fullPath`:** Every `RouteNode` carries both fields. `path` is the raw `meta.route` segment as declared in the machine — it may be relative (e.g. `"overview"`). `fullPath` is the resolved absolute path (e.g. `"/dashboard/overview"`). Route maps always use `fullPath`. Only use `path` if you specifically need the declared segment.
312
+
226
313
  ### Query Utilities
227
314
 
228
315
  ```typescript
229
- // Get child routes from state
230
- const children = getNavigableRoutes(tree, "dashboard");
316
+ import {
317
+ getNavigableRoutes,
318
+ getRoutableRoutes,
319
+ routeExists,
320
+ getTransitionReachableRoutes,
321
+ isRouteReachable,
322
+ } from "@xmachines/play-router";
323
+
324
+ // Child routes (hierarchical + transition-reachable)
325
+ const navigable = getNavigableRoutes(tree, "dashboard");
231
326
 
232
- // Check if route exists
327
+ // All routable routes as flat array
328
+ const allRoutes = getRoutableRoutes(tree);
329
+
330
+ // Check path exists
233
331
  const exists = routeExists(tree, "/profile/:userId");
234
- ```
235
332
 
236
- **Complete API:** See [API Documentation](../../docs/api/@xmachines/play-router)
333
+ // Transition-aware: which state IDs are reachable via transitions?
334
+ const reachableIds = getTransitionReachableRoutes(tree.graph!, "myMachine.home");
335
+
336
+ // Is there any path from state A to state B via transitions?
337
+ const canReach = isRouteReachable(tree.graph!, "myMachine.home", "myMachine.dashboard");
338
+ ```
237
339
 
238
340
  ## Examples
239
341
 
240
342
  ### Route Detection
241
343
 
242
344
  ```typescript
243
- import { extractMachineRoutes } from "@xmachines/play-router";
345
+ import { extractMachineRoutes, findRouteByPath } from "@xmachines/play-router";
244
346
  import { createMachine } from "xstate";
245
347
 
246
348
  const machine = createMachine({
@@ -263,11 +365,11 @@ const machine = createMachine({
263
365
 
264
366
  const tree = extractMachineRoutes(machine);
265
367
 
266
- // Bidirectional mapping
267
- const profileById = tree.byId.get("profile");
268
- console.log(profileById?.path); // "/profile/:userId"
368
+ // Bidirectional mapping — use fullPath for URLs; path is the raw meta.route segment
369
+ const profileById = tree.byStateId.get("profile");
370
+ console.log(profileById?.fullPath); // "/profile/:userId"
269
371
 
270
- const profileByPath = tree.byPath.get("/profile/user123");
372
+ const profileByPath = findRouteByPath(tree, "/profile/user123");
271
373
  console.log(profileByPath?.id); // "profile"
272
374
  ```
273
375
 
@@ -311,14 +413,14 @@ const dashboardChildren = getNavigableRoutes(tree, "dashboard");
311
413
  console.log(dashboardChildren.map((r) => r.id)); // ["overview", "analytics"]
312
414
 
313
415
  // Route inheritance
314
- const analyticsRoute = tree.byId.get("analytics");
315
- console.log(analyticsRoute?.path); // "/dashboard/analytics" (inherited parent path)
416
+ const analyticsRoute = tree.byStateId.get("analytics");
417
+ console.log(analyticsRoute?.fullPath); // "/analytics" (absolute route)
316
418
  ```
317
419
 
318
420
  ### Pattern Matching
319
421
 
320
422
  ```typescript
321
- import { extractMachineRoutes } from "@xmachines/play-router";
423
+ import { extractMachineRoutes, findRouteByPath } from "@xmachines/play-router";
322
424
 
323
425
  const machine = createMachine({
324
426
  states: {
@@ -336,13 +438,13 @@ const machine = createMachine({
336
438
  const tree = extractMachineRoutes(machine);
337
439
 
338
440
  // Pattern matching for actual URLs
339
- const userRoute = tree.byPath.get("/user/user123");
441
+ const userRoute = findRouteByPath(tree, "/user/user123");
340
442
  console.log(userRoute?.id); // "user"
341
443
 
342
- const settingsDefault = tree.byPath.get("/settings");
444
+ const settingsDefault = findRouteByPath(tree, "/settings");
343
445
  console.log(settingsDefault?.id); // "settings" (optional param)
344
446
 
345
- const settingsProfile = tree.byPath.get("/settings/profile");
447
+ const settingsProfile = findRouteByPath(tree, "/settings/profile");
346
448
  console.log(settingsProfile?.id); // "settings" (with param)
347
449
  ```
348
450
 
@@ -388,7 +490,7 @@ states: {
388
490
  },
389
491
  relative: {
390
492
  id: "relative",
391
- meta: { route: "relative", view: { component: "Relative" } }, // No leading / → inherits parent
493
+ meta: { route: "relative", view: { component: "Relative" } }, // No leading / → inherits parent path prefix
392
494
  // Final path: "/parent/relative"
393
495
  },
394
496
  },
@@ -409,20 +511,78 @@ meta: {
409
511
 
410
512
  **Parameter substitution:** Values extracted from context or event params (handled by play-xstate adapter).
411
513
 
514
+ ## Security Utilities
515
+
516
+ ### `sanitizePathname()`
517
+
518
+ Normalize a raw pathname before route-map lookup. Used internally by `RouterBridgeBase.syncActorFromRouter()` and available to adapters that bypass the base class:
519
+
520
+ ```typescript
521
+ import { sanitizePathname } from "@xmachines/play-router";
522
+
523
+ // In a custom adapter's route-watch callback:
524
+ const clean = sanitizePathname(rawPath);
525
+ if (clean === null) return; // Reject malformed/oversized paths (> 2048 chars)
526
+ // Proceed with clean normalized path
527
+ ```
528
+
529
+ Returns `null` for paths exceeding 2048 characters. Strips query fragments, hash fragments, and collapses duplicate slashes.
530
+
531
+ ---
532
+
533
+ ## Error Handling
534
+
535
+ All runtime errors thrown by this package extend `PlayError` from `@xmachines/play` and
536
+ are exported from the `./errors` subpath:
537
+
538
+ ```typescript
539
+ import {
540
+ RouterSyncError,
541
+ URLPatternUnavailableError,
542
+ InvalidRoutePatternError,
543
+ } from "@xmachines/play-router/errors";
544
+ ```
545
+
546
+ | Class | Code | When thrown |
547
+ | ---------------------------- | --------------------------------------- | -------------------------------------------------------------- |
548
+ | `RouterSyncError` | `PLAY_ROUTER_SYNC_FAILED` | `syncActorFromRouter()` fails to send a `play.route` event |
549
+ | `URLPatternUnavailableError` | `PLAY_ROUTE_MAP_URLPATTERN_UNAVAILABLE` | `createRouteMap()` called before URLPattern polyfill is loaded |
550
+ | `InvalidRoutePatternError` | `PLAY_ROUTE_MAP_INVALID_PATTERN` | A route pattern string is rejected by URLPattern constructor |
551
+
552
+ `InvalidRoutePatternError` carries a `pattern: string` field with the offending pattern.
553
+
554
+ ```typescript
555
+ import { PlayError } from "@xmachines/play";
556
+ import { RouterSyncError } from "@xmachines/play-router/errors";
557
+
558
+ try {
559
+ bridge.connect();
560
+ } catch (err) {
561
+ if (err instanceof RouterSyncError) {
562
+ // err.cause — the original error that triggered the sync failure
563
+ monitoringService.record(err);
564
+ } else if (err instanceof PlayError) {
565
+ console.error(`[${err.scope}:${err.code}] ${err.message}`);
566
+ }
567
+ }
568
+ ```
569
+
412
570
  ## Architecture
413
571
 
414
572
  This package enables **Actor Authority (INV-01)**:
415
573
 
416
574
  1. **Routes derive from machine:** Business logic defines routes in state machine, not external config
417
- 2. **BFS traversal:** Systematic state discovery ensures all nested states visited
418
- 3. **Bidirectional mapping:** Fast lookup by path (browser URL) or by ID (state machine)
419
- 4. **Build-time validation:** Invalid routes throw errors during extraction, not runtime
575
+ 2. **Graph-based extraction:** `machineToGraph()` converts machines to typed `@statelyai/graph` `Graph` objects with hierarchy, transition edges, and route metadata
576
+ 3. **Bidirectional mapping:** Fast lookup by path (browser URL) or by state ID (state machine)
577
+ 4. **Transition-aware queries:** `getTransitionReachableRoutes()` and `isRouteReachable()` use graph algorithms for navigation reachability
578
+ 5. **Build-time validation:** Invalid routes throw errors during extraction, not runtime
420
579
 
421
580
  **Enhancements:**
422
581
 
423
- - `meta.route` detection via state metadata
582
+ - `meta.route` detection via state metadata — extracted into typed graph node data
424
583
  - Pattern matching for dynamic routes (`:param` and `:param?`)
425
584
  - State ID ↔ path bidirectional maps for `play.route` events
585
+ - `@statelyai/graph` integration — hierarchy queries, reachability checks, transition traversal
426
586
 
427
587
  ## Related Packages
428
588
 
@@ -433,4 +593,7 @@ This package enables **Actor Authority (INV-01)**:
433
593
 
434
594
  ## License
435
595
 
436
- MIT
596
+ Copyright (c) 2016 [Mikael Karon](mailto:mikael@karon.se). All rights reserved.
597
+
598
+ This work is licensed under the terms of the MIT license.
599
+ For a copy, see <https://opensource.org/licenses/MIT>.
@@ -0,0 +1,123 @@
1
+ /**
2
+ * RouteMap — Shared bidirectional route mapping base class
3
+ *
4
+ * Provides bucket-based pattern matching shared across all framework adapters.
5
+ * Adapters extend this class rather than duplicating the pattern-match logic.
6
+ *
7
+ * Algorithm: O(1) exact match via Map, then bucket-based O(k) pattern match
8
+ * where k = routes in the first-segment bucket (typically << total routes).
9
+ * Uses URLPattern for parameterized route matching (same engine as createRouteMap).
10
+ */
11
+ /**
12
+ * A single state ID ↔ path mapping entry.
13
+ *
14
+ * Both fields are `readonly` — mappings are immutable once passed to `RouteMap`.
15
+ * Adapter packages re-export a structurally compatible `RouteMapping` type under
16
+ * their own name. This type is published from `@xmachines/play-router` as
17
+ * `RouteMapping` to avoid name collisions with those adapter-local types.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * const mapping: RouteMapping = { stateId: "home", path: "/" };
22
+ * const paramMapping: RouteMapping = { stateId: "profile", path: "/profile/:userId" };
23
+ * const optionalMapping: RouteMapping = { stateId: "settings", path: "/settings/:section?" };
24
+ * ```
25
+ */
26
+ export interface RouteMapping {
27
+ /** State machine state ID (e.g., `"home"`, `"#profile"`) */
28
+ readonly stateId: string;
29
+ /** URL path pattern (e.g., `"/"`, `"/profile/:userId"`, `"/settings/:section?"`) */
30
+ readonly path: string;
31
+ }
32
+ /**
33
+ * Shared bidirectional route map base class.
34
+ *
35
+ * All framework adapters use this class as their route map — they add no logic of their
36
+ * own and inherit the full public API from here.
37
+ *
38
+ * **Lookup strategy:**
39
+ * - Static paths (no `:param`) → O(1) `Map` lookup
40
+ * - Dynamic paths → O(k) bucket-indexed scan using `URLPattern`, where `k` is the number
41
+ * of routes sharing the same first path segment
42
+ * - Results are cached after the first match in an LRU cache (default 500 entries,
43
+ * configurable via the `cacheSize` constructor option)
44
+ *
45
+ * **Pattern syntax** (`:param` / `:param?`):
46
+ * - `:param` — required segment, matches exactly one non-`/` segment
47
+ * - `:param?` — optional segment, matches zero or one non-`/` segment
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * import { RouteMap } from "@xmachines/play-router";
52
+ *
53
+ * const map = new RouteMap([
54
+ * { stateId: "home", path: "/" },
55
+ * { stateId: "profile", path: "/profile/:userId" },
56
+ * { stateId: "settings", path: "/settings/:section?" },
57
+ * ]);
58
+ *
59
+ * map.getStateIdByPath("/"); // "home"
60
+ * map.getStateIdByPath("/profile/123"); // "profile"
61
+ * map.getStateIdByPath("/settings"); // "settings"
62
+ * map.getStateIdByPath("/unknown"); // null
63
+ *
64
+ * map.getPathByStateId("profile"); // "/profile/:userId"
65
+ * map.getPathByStateId("missing"); // null
66
+ * ```
67
+ */
68
+ export declare class RouteMap {
69
+ private stateIdToPath;
70
+ private pathToStateId;
71
+ private patternBuckets;
72
+ private pathMatchCache;
73
+ /**
74
+ * Build a route map from an array of state ID ↔ path mappings.
75
+ *
76
+ * Static paths (no `:param`) are indexed in an O(1) `Map`.
77
+ * Parameterized paths are compiled to `URLPattern` and grouped into first-segment
78
+ * buckets for efficient candidate selection.
79
+ *
80
+ * @param mappings - Array of `{ stateId, path }` entries. Order determines
81
+ * priority when multiple patterns could match the same path.
82
+ * @param options - Optional configuration.
83
+ * `options.cacheSize`: Maximum number of resolved parameterized path lookups
84
+ * to cache. Defaults to `500`. Increase for applications with many unique
85
+ * parameterized URL values (e.g. user profile pages with thousands of distinct IDs).
86
+ * After eviction the path falls back to the O(k) bucket pattern scan — correct
87
+ * but slower. Minimum effective value is `1` (QuickLRU constraint).
88
+ */
89
+ constructor(mappings: RouteMapping[], { cacheSize }?: {
90
+ cacheSize?: number;
91
+ });
92
+ /**
93
+ * Resolve a URL path to its mapped state ID.
94
+ *
95
+ * Strips query strings and hash fragments before matching. Tries an O(1) exact
96
+ * lookup first, then falls back to bucket-indexed pattern matching. Results are
97
+ * cached after the first pattern match.
98
+ *
99
+ * @param path - URL pathname, optionally including query/hash (e.g., `"/profile/123?ref=nav"`)
100
+ * @returns The mapped state ID, or `null` if no route matches
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * map.getStateIdByPath("/profile/123"); // "profile"
105
+ * map.getStateIdByPath("/unknown"); // null
106
+ * ```
107
+ */
108
+ getStateIdByPath(path: string): string | null;
109
+ /**
110
+ * Look up the path pattern registered for a state ID.
111
+ *
112
+ * @param stateId - State machine state ID (e.g., `"profile"`, `"#settings"`)
113
+ * @returns The registered path pattern, or `null` if the state ID is unknown
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * map.getPathByStateId("profile"); // "/profile/:userId"
118
+ * map.getPathByStateId("missing"); // null
119
+ * ```
120
+ */
121
+ getPathByStateId(stateId: string): string | null;
122
+ }
123
+ //# sourceMappingURL=base-route-map.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-route-map.d.ts","sourceRoot":"","sources":["../src/base-route-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAgCH;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,YAAY;IAC5B,4DAA4D;IAC5D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oFAAoF;IACpF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,qBAAa,QAAQ;IACpB,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,cAAc,CAGpB;IACF,OAAO,CAAC,cAAc,CAAkC;IAExD;;;;;;;;;;;;;;;OAeG;gBACS,QAAQ,EAAE,YAAY,EAAE,EAAE,EAAE,SAAe,EAAE,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO;IAiCtF;;;;;;;;;;;;;;;OAeG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAuB7C;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;CAGhD"}