@xmachines/play-router 1.0.0-beta.2 → 1.0.0-beta.20
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 +169 -47
- package/dist/base-route-map.d.ts +116 -0
- package/dist/base-route-map.d.ts.map +1 -0
- package/dist/base-route-map.js +206 -0
- package/dist/base-route-map.js.map +1 -0
- package/dist/build-tree.d.ts.map +1 -1
- package/dist/build-tree.js +6 -5
- package/dist/build-tree.js.map +1 -1
- package/dist/connect-router.d.ts.map +1 -1
- package/dist/connect-router.js +35 -45
- package/dist/connect-router.js.map +1 -1
- package/dist/create-browser-history.d.ts +38 -5
- package/dist/create-browser-history.d.ts.map +1 -1
- package/dist/create-browser-history.js +43 -17
- package/dist/create-browser-history.js.map +1 -1
- package/dist/create-route-map.d.ts +21 -1
- package/dist/create-route-map.d.ts.map +1 -1
- package/dist/create-route-map.js +73 -22
- package/dist/create-route-map.js.map +1 -1
- package/dist/errors.d.ts +75 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +85 -0
- package/dist/errors.js.map +1 -0
- package/dist/extract-routes.d.ts +5 -31
- package/dist/extract-routes.d.ts.map +1 -1
- package/dist/extract-routes.js +70 -49
- package/dist/extract-routes.js.map +1 -1
- package/dist/find-route.d.ts +44 -0
- package/dist/find-route.d.ts.map +1 -0
- package/dist/find-route.js +126 -0
- package/dist/find-route.js.map +1 -0
- package/dist/index.d.ts +9 -48
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -131
- package/dist/index.js.map +1 -1
- package/dist/machine-to-graph.d.ts +17 -0
- package/dist/machine-to-graph.d.ts.map +1 -0
- package/dist/machine-to-graph.js +115 -0
- package/dist/machine-to-graph.js.map +1 -0
- package/dist/query.d.ts +44 -1
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +80 -3
- package/dist/query.js.map +1 -1
- package/dist/router-bridge-base.d.ts +49 -19
- package/dist/router-bridge-base.d.ts.map +1 -1
- package/dist/router-bridge-base.js +120 -56
- package/dist/router-bridge-base.js.map +1 -1
- package/dist/router-sync.d.ts +62 -0
- package/dist/router-sync.d.ts.map +1 -0
- package/dist/router-sync.js +87 -0
- package/dist/router-sync.js.map +1 -0
- package/dist/types.d.ts +73 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-routes.d.ts +9 -9
- package/dist/validate-routes.d.ts.map +1 -1
- package/dist/validate-routes.js +12 -11
- package/dist/validate-routes.js.map +1 -1
- package/package.json +36 -18
- package/dist/crawl-machine.d.ts +0 -74
- package/dist/crawl-machine.d.ts.map +0 -1
- package/dist/crawl-machine.js +0 -95
- package/dist/crawl-machine.js.map +0 -1
- package/dist/extract-route.d.ts +0 -25
- package/dist/extract-route.d.ts.map +0 -1
- package/dist/extract-route.js +0 -63
- package/dist/extract-route.js.map +0 -1
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
|
-
|
|
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
|
|
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
|
-
|
|
15
|
+
The low-level `connectRouter()` path now 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
|
|
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
|
|
69
|
-
console.log(tree.
|
|
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
|
|
93
|
+
const settingsRoute = findRouteByPath(tree, "/settings/profile");
|
|
73
94
|
console.log(settingsRoute?.id); // "settings"
|
|
74
95
|
```
|
|
75
96
|
|
|
@@ -159,6 +180,17 @@ Bridge teardown must be explicit and deterministic:
|
|
|
159
180
|
- `disconnect`/`dispose` must unwatch signal subscriptions and unhook router listeners.
|
|
160
181
|
- Do not rely on GC-only cleanup guidance.
|
|
161
182
|
- Infrastructure remains passive: bridges observe and forward intents, actors decide validity.
|
|
183
|
+
- `createBrowserHistory().destroy()` is idempotent and restores shared `window.history` patches only after the last wrapper for that window is removed.
|
|
184
|
+
|
|
185
|
+
## Diagnostics
|
|
186
|
+
|
|
187
|
+
Router infrastructure reports runtime failures through `PlayDiagnostics` from `@xmachines/play`.
|
|
188
|
+
|
|
189
|
+
- Default behavior uses `consoleDiagnostics`.
|
|
190
|
+
- Messages follow a stable `[scope:code] message` format.
|
|
191
|
+
- Example: `[RouterBridgeBase:PLAY_ROUTER_SYNC_FAILED] Failed to sync actor state from router location.`
|
|
192
|
+
|
|
193
|
+
This keeps router errors observable while letting applications swap in their own diagnostics implementation upstream.
|
|
162
194
|
|
|
163
195
|
## API Reference
|
|
164
196
|
|
|
@@ -176,9 +208,10 @@ const tree = extractMachineRoutes(machine: AnyStateMachine): RouteTree;
|
|
|
176
208
|
|
|
177
209
|
**Returns:** `RouteTree` with:
|
|
178
210
|
|
|
179
|
-
- `
|
|
180
|
-
- `
|
|
181
|
-
- `
|
|
211
|
+
- `byPath: Map<string, RouteNode>` — URL path → route node
|
|
212
|
+
- `byStateId: Map<string, RouteNode>` — State ID → route node
|
|
213
|
+
- `root: RouteNode` — synthetic root node
|
|
214
|
+
- `graph: MachineGraph` — `@statelyai/graph` Graph for hierarchy queries, reachability checks, and transition-aware navigation
|
|
182
215
|
|
|
183
216
|
**Throws:** Error if routes are invalid (malformed paths, missing state IDs, duplicates)
|
|
184
217
|
|
|
@@ -190,57 +223,85 @@ import { extractMachineRoutes } from "@xmachines/play-router";
|
|
|
190
223
|
const tree = extractMachineRoutes(authMachine);
|
|
191
224
|
|
|
192
225
|
// Query routes
|
|
193
|
-
const loginRoute = tree.
|
|
226
|
+
const loginRoute = tree.byStateId.get("login");
|
|
194
227
|
console.log(loginRoute?.path); // "/login"
|
|
195
228
|
|
|
196
229
|
const dashboardRoute = tree.byPath.get("/dashboard");
|
|
197
230
|
console.log(dashboardRoute?.id); // "dashboard"
|
|
198
231
|
```
|
|
199
232
|
|
|
200
|
-
###
|
|
233
|
+
### machineToGraph()
|
|
201
234
|
|
|
202
|
-
|
|
235
|
+
Converts an XState v5 machine to a typed `@statelyai/graph` `Graph`:
|
|
203
236
|
|
|
204
237
|
```typescript
|
|
205
|
-
|
|
206
|
-
|
|
238
|
+
import { machineToGraph } from "@xmachines/play-router";
|
|
239
|
+
import { getChildren, getSuccessors, hasPath } from "@statelyai/graph";
|
|
207
240
|
|
|
208
|
-
|
|
241
|
+
const graph = machineToGraph(machine);
|
|
209
242
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
243
|
+
// Hierarchy queries
|
|
244
|
+
const children = getChildren(graph, "myMachine");
|
|
245
|
+
const successors = getSuccessors(graph, "myMachine.home"); // transition-reachable states
|
|
213
246
|
|
|
214
|
-
|
|
247
|
+
// Reachability
|
|
248
|
+
const reachable = hasPath(graph, "myMachine.home", "myMachine.dashboard");
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The graph is also available on any `RouteTree` returned by `extractMachineRoutes()`:
|
|
215
252
|
|
|
216
253
|
```typescript
|
|
217
|
-
|
|
254
|
+
const tree = extractMachineRoutes(machine);
|
|
218
255
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
console.log("State:", visit.path.join("."));
|
|
222
|
-
console.log("Parent:", visit.parent?.id ?? "root");
|
|
223
|
-
});
|
|
256
|
+
// Use @statelyai/graph algorithms directly on the graph
|
|
257
|
+
const successors = getSuccessors(tree.graph!, "myMachine.home");
|
|
224
258
|
```
|
|
225
259
|
|
|
260
|
+
**Node data** (`MachineNodeData`):
|
|
261
|
+
|
|
262
|
+
- `stateId: string` — XState state ID (e.g., `"myMachine.dashboard.overview"`)
|
|
263
|
+
- `type` — `"atomic" | "compound" | "parallel" | "final" | "history"`
|
|
264
|
+
- `meta?: Record<string, unknown>` — original state meta object
|
|
265
|
+
- `route?: string` — extracted route path from `meta.route`
|
|
266
|
+
|
|
267
|
+
**Edge data** (`MachineEdgeData`):
|
|
268
|
+
|
|
269
|
+
- `eventType: string` — event type triggering this transition
|
|
270
|
+
- `guardType?: string` — guard name/description (if transition is guarded)
|
|
271
|
+
|
|
226
272
|
### Query Utilities
|
|
227
273
|
|
|
228
274
|
```typescript
|
|
229
|
-
|
|
230
|
-
|
|
275
|
+
import {
|
|
276
|
+
getNavigableRoutes,
|
|
277
|
+
getRoutableRoutes,
|
|
278
|
+
routeExists,
|
|
279
|
+
getTransitionReachableRoutes,
|
|
280
|
+
isRouteReachable,
|
|
281
|
+
} from "@xmachines/play-router";
|
|
282
|
+
|
|
283
|
+
// Child routes (hierarchical + transition-reachable)
|
|
284
|
+
const navigable = getNavigableRoutes(tree, "dashboard");
|
|
231
285
|
|
|
232
|
-
//
|
|
286
|
+
// All routable routes as flat array
|
|
287
|
+
const allRoutes = getRoutableRoutes(tree);
|
|
288
|
+
|
|
289
|
+
// Check path exists
|
|
233
290
|
const exists = routeExists(tree, "/profile/:userId");
|
|
234
|
-
```
|
|
235
291
|
|
|
236
|
-
|
|
292
|
+
// Transition-aware: which state IDs are reachable via transitions?
|
|
293
|
+
const reachableIds = getTransitionReachableRoutes(tree.graph!, "myMachine.home");
|
|
294
|
+
|
|
295
|
+
// Is there any path from state A to state B via transitions?
|
|
296
|
+
const canReach = isRouteReachable(tree.graph!, "myMachine.home", "myMachine.dashboard");
|
|
297
|
+
```
|
|
237
298
|
|
|
238
299
|
## Examples
|
|
239
300
|
|
|
240
301
|
### Route Detection
|
|
241
302
|
|
|
242
303
|
```typescript
|
|
243
|
-
import { extractMachineRoutes } from "@xmachines/play-router";
|
|
304
|
+
import { extractMachineRoutes, findRouteByPath } from "@xmachines/play-router";
|
|
244
305
|
import { createMachine } from "xstate";
|
|
245
306
|
|
|
246
307
|
const machine = createMachine({
|
|
@@ -264,10 +325,10 @@ const machine = createMachine({
|
|
|
264
325
|
const tree = extractMachineRoutes(machine);
|
|
265
326
|
|
|
266
327
|
// Bidirectional mapping
|
|
267
|
-
const profileById = tree.
|
|
328
|
+
const profileById = tree.byStateId.get("profile");
|
|
268
329
|
console.log(profileById?.path); // "/profile/:userId"
|
|
269
330
|
|
|
270
|
-
const profileByPath = tree
|
|
331
|
+
const profileByPath = findRouteByPath(tree, "/profile/user123");
|
|
271
332
|
console.log(profileByPath?.id); // "profile"
|
|
272
333
|
```
|
|
273
334
|
|
|
@@ -311,14 +372,14 @@ const dashboardChildren = getNavigableRoutes(tree, "dashboard");
|
|
|
311
372
|
console.log(dashboardChildren.map((r) => r.id)); // ["overview", "analytics"]
|
|
312
373
|
|
|
313
374
|
// Route inheritance
|
|
314
|
-
const analyticsRoute = tree.
|
|
315
|
-
console.log(analyticsRoute?.
|
|
375
|
+
const analyticsRoute = tree.byStateId.get("analytics");
|
|
376
|
+
console.log(analyticsRoute?.fullPath); // "/analytics" (absolute route)
|
|
316
377
|
```
|
|
317
378
|
|
|
318
379
|
### Pattern Matching
|
|
319
380
|
|
|
320
381
|
```typescript
|
|
321
|
-
import { extractMachineRoutes } from "@xmachines/play-router";
|
|
382
|
+
import { extractMachineRoutes, findRouteByPath } from "@xmachines/play-router";
|
|
322
383
|
|
|
323
384
|
const machine = createMachine({
|
|
324
385
|
states: {
|
|
@@ -336,13 +397,13 @@ const machine = createMachine({
|
|
|
336
397
|
const tree = extractMachineRoutes(machine);
|
|
337
398
|
|
|
338
399
|
// Pattern matching for actual URLs
|
|
339
|
-
const userRoute = tree
|
|
400
|
+
const userRoute = findRouteByPath(tree, "/user/user123");
|
|
340
401
|
console.log(userRoute?.id); // "user"
|
|
341
402
|
|
|
342
|
-
const settingsDefault = tree
|
|
403
|
+
const settingsDefault = findRouteByPath(tree, "/settings");
|
|
343
404
|
console.log(settingsDefault?.id); // "settings" (optional param)
|
|
344
405
|
|
|
345
|
-
const settingsProfile = tree
|
|
406
|
+
const settingsProfile = findRouteByPath(tree, "/settings/profile");
|
|
346
407
|
console.log(settingsProfile?.id); // "settings" (with param)
|
|
347
408
|
```
|
|
348
409
|
|
|
@@ -388,7 +449,7 @@ states: {
|
|
|
388
449
|
},
|
|
389
450
|
relative: {
|
|
390
451
|
id: "relative",
|
|
391
|
-
meta: { route: "relative", view: { component: "Relative" } }, // No leading / → inherits parent
|
|
452
|
+
meta: { route: "relative", view: { component: "Relative" } }, // No leading / → inherits parent path prefix
|
|
392
453
|
// Final path: "/parent/relative"
|
|
393
454
|
},
|
|
394
455
|
},
|
|
@@ -409,20 +470,78 @@ meta: {
|
|
|
409
470
|
|
|
410
471
|
**Parameter substitution:** Values extracted from context or event params (handled by play-xstate adapter).
|
|
411
472
|
|
|
473
|
+
## Security Utilities
|
|
474
|
+
|
|
475
|
+
### `sanitizePathname()`
|
|
476
|
+
|
|
477
|
+
Normalize a raw pathname before route-map lookup. Used internally by `RouterBridgeBase.syncActorFromRouter()` and available to adapters that bypass the base class:
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
import { sanitizePathname } from "@xmachines/play-router";
|
|
481
|
+
|
|
482
|
+
// In a custom adapter's route-watch callback:
|
|
483
|
+
const clean = sanitizePathname(rawPath);
|
|
484
|
+
if (clean === null) return; // Reject malformed/oversized paths (> 2048 chars)
|
|
485
|
+
// Proceed with clean normalized path
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Returns `null` for paths exceeding 2048 characters. Strips query fragments, hash fragments, and collapses duplicate slashes.
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## Error Handling
|
|
493
|
+
|
|
494
|
+
All runtime errors thrown by this package extend `PlayError` from `@xmachines/play` and
|
|
495
|
+
are exported from the `./errors` subpath:
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
import {
|
|
499
|
+
RouterSyncError,
|
|
500
|
+
URLPatternUnavailableError,
|
|
501
|
+
InvalidRoutePatternError,
|
|
502
|
+
} from "@xmachines/play-router/errors";
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
| Class | Code | When thrown |
|
|
506
|
+
| ---------------------------- | --------------------------------------- | -------------------------------------------------------------- |
|
|
507
|
+
| `RouterSyncError` | `PLAY_ROUTER_SYNC_FAILED` | `syncActorFromRouter()` fails to send a `play.route` event |
|
|
508
|
+
| `URLPatternUnavailableError` | `PLAY_ROUTE_MAP_URLPATTERN_UNAVAILABLE` | `createRouteMap()` called before URLPattern polyfill is loaded |
|
|
509
|
+
| `InvalidRoutePatternError` | `PLAY_ROUTE_MAP_INVALID_PATTERN` | A route pattern string is rejected by URLPattern constructor |
|
|
510
|
+
|
|
511
|
+
`InvalidRoutePatternError` carries a `pattern: string` field with the offending pattern.
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
import { PlayError } from "@xmachines/play";
|
|
515
|
+
import { RouterSyncError } from "@xmachines/play-router/errors";
|
|
516
|
+
|
|
517
|
+
try {
|
|
518
|
+
bridge.connect();
|
|
519
|
+
} catch (err) {
|
|
520
|
+
if (err instanceof RouterSyncError) {
|
|
521
|
+
// err.cause — the original error that triggered the sync failure
|
|
522
|
+
monitoringService.record(err);
|
|
523
|
+
} else if (err instanceof PlayError) {
|
|
524
|
+
console.error(`[${err.scope}:${err.code}] ${err.message}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
412
529
|
## Architecture
|
|
413
530
|
|
|
414
531
|
This package enables **Actor Authority (INV-01)**:
|
|
415
532
|
|
|
416
533
|
1. **Routes derive from machine:** Business logic defines routes in state machine, not external config
|
|
417
|
-
2. **
|
|
418
|
-
3. **Bidirectional mapping:** Fast lookup by path (browser URL) or by ID (state machine)
|
|
419
|
-
4. **
|
|
534
|
+
2. **Graph-based extraction:** `machineToGraph()` converts machines to typed `@statelyai/graph` `Graph` objects with hierarchy, transition edges, and route metadata
|
|
535
|
+
3. **Bidirectional mapping:** Fast lookup by path (browser URL) or by state ID (state machine)
|
|
536
|
+
4. **Transition-aware queries:** `getTransitionReachableRoutes()` and `isRouteReachable()` use graph algorithms for navigation reachability
|
|
537
|
+
5. **Build-time validation:** Invalid routes throw errors during extraction, not runtime
|
|
420
538
|
|
|
421
539
|
**Enhancements:**
|
|
422
540
|
|
|
423
|
-
- `meta.route` detection via state metadata
|
|
541
|
+
- `meta.route` detection via state metadata — extracted into typed graph node data
|
|
424
542
|
- Pattern matching for dynamic routes (`:param` and `:param?`)
|
|
425
543
|
- State ID ↔ path bidirectional maps for `play.route` events
|
|
544
|
+
- `@statelyai/graph` integration — hierarchy queries, reachability checks, transition traversal
|
|
426
545
|
|
|
427
546
|
## Related Packages
|
|
428
547
|
|
|
@@ -433,4 +552,7 @@ This package enables **Actor Authority (INV-01)**:
|
|
|
433
552
|
|
|
434
553
|
## License
|
|
435
554
|
|
|
436
|
-
|
|
555
|
+
Copyright (c) 2016 [Mikael Karon](mailto:mikael@karon.se). All rights reserved.
|
|
556
|
+
|
|
557
|
+
This work is licensed under the terms of the MIT license.
|
|
558
|
+
For a copy, see <https://opensource.org/licenses/MIT>.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseRouteMap — 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 `BaseRouteMap`.
|
|
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
|
+
* `BaseRouteMapping` to avoid name collisions with those adapter-local types.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const mapping: BaseRouteMapping = { stateId: "home", path: "/" };
|
|
22
|
+
* const paramMapping: BaseRouteMapping = { stateId: "profile", path: "/profile/:userId" };
|
|
23
|
+
* const optionalMapping: BaseRouteMapping = { 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 adapter `RouteMap` classes extend this — 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
|
|
43
|
+
*
|
|
44
|
+
* **Pattern syntax** (`:param` / `:param?`):
|
|
45
|
+
* - `:param` — required segment, matches exactly one non-`/` segment
|
|
46
|
+
* - `:param?` — optional segment, matches zero or one non-`/` segment
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* import { BaseRouteMap } from "@xmachines/play-router";
|
|
51
|
+
*
|
|
52
|
+
* const map = new BaseRouteMap([
|
|
53
|
+
* { stateId: "home", path: "/" },
|
|
54
|
+
* { stateId: "profile", path: "/profile/:userId" },
|
|
55
|
+
* { stateId: "settings", path: "/settings/:section?" },
|
|
56
|
+
* ]);
|
|
57
|
+
*
|
|
58
|
+
* map.getStateIdByPath("/"); // "home"
|
|
59
|
+
* map.getStateIdByPath("/profile/123"); // "profile"
|
|
60
|
+
* map.getStateIdByPath("/settings"); // "settings"
|
|
61
|
+
* map.getStateIdByPath("/unknown"); // null
|
|
62
|
+
*
|
|
63
|
+
* map.getPathByStateId("profile"); // "/profile/:userId"
|
|
64
|
+
* map.getPathByStateId("missing"); // null
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare class BaseRouteMap {
|
|
68
|
+
private stateIdToPath;
|
|
69
|
+
private pathToStateId;
|
|
70
|
+
private patternBuckets;
|
|
71
|
+
private pathMatchCache;
|
|
72
|
+
/**
|
|
73
|
+
* Build a route map from an array of state ID ↔ path mappings.
|
|
74
|
+
*
|
|
75
|
+
* Static paths (no `:param`) are indexed in an O(1) `Map`.
|
|
76
|
+
* Parameterized paths are compiled to `URLPattern` and grouped into first-segment
|
|
77
|
+
* buckets for efficient candidate selection.
|
|
78
|
+
*
|
|
79
|
+
* @param mappings - Array of `{ stateId, path }` entries. Order determines
|
|
80
|
+
* priority when multiple patterns could match the same path.
|
|
81
|
+
*/
|
|
82
|
+
constructor(mappings: RouteMapping[]);
|
|
83
|
+
/**
|
|
84
|
+
* Resolve a URL path to its mapped state ID.
|
|
85
|
+
*
|
|
86
|
+
* Strips query strings and hash fragments before matching. Tries an O(1) exact
|
|
87
|
+
* lookup first, then falls back to bucket-indexed pattern matching. Results are
|
|
88
|
+
* cached after the first pattern match.
|
|
89
|
+
*
|
|
90
|
+
* @param path - URL pathname, optionally including query/hash (e.g., `"/profile/123?ref=nav"`)
|
|
91
|
+
* @returns The mapped state ID, or `null` if no route matches
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* map.getStateIdByPath("/profile/123"); // "profile"
|
|
96
|
+
* map.getStateIdByPath("/unknown"); // null
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
getStateIdByPath(path: string): string | null;
|
|
100
|
+
/**
|
|
101
|
+
* Look up the path pattern registered for a state ID.
|
|
102
|
+
*
|
|
103
|
+
* @param stateId - State machine state ID (e.g., `"profile"`, `"#settings"`)
|
|
104
|
+
* @returns The registered path pattern, or `null` if the state ID is unknown
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* map.getPathByStateId("profile"); // "/profile/:userId"
|
|
109
|
+
* map.getPathByStateId("missing"); // null
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
getPathByStateId(stateId: string): string | null;
|
|
113
|
+
private getIndexKey;
|
|
114
|
+
private getCandidates;
|
|
115
|
+
}
|
|
116
|
+
//# 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;AAuCH;;;;;;;;;;;;;;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,qBAAa,YAAY;IACxB,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,cAAc,CAGpB;IACF,OAAO,CAAC,cAAc,CAAkC;IAExD;;;;;;;;;OASG;gBACS,QAAQ,EAAE,YAAY,EAAE;IAkCpC;;;;;;;;;;;;;;;OAeG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAuB7C;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAIhD,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,aAAa;CA8BrB"}
|