@xmachines/play-router 1.0.0-beta.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +259 -335
  3. package/dist/base-route-map.d.ts +136 -0
  4. package/dist/base-route-map.d.ts.map +1 -0
  5. package/dist/base-route-map.js +182 -0
  6. package/dist/base-route-map.js.map +1 -0
  7. package/dist/build-tree.d.ts +7 -0
  8. package/dist/build-tree.d.ts.map +1 -1
  9. package/dist/build-tree.js +21 -6
  10. package/dist/build-tree.js.map +1 -1
  11. package/dist/create-route-map-from-tree.d.ts +33 -0
  12. package/dist/create-route-map-from-tree.d.ts.map +1 -0
  13. package/dist/create-route-map-from-tree.js +43 -0
  14. package/dist/create-route-map-from-tree.js.map +1 -0
  15. package/dist/create-route-map.d.ts +29 -33
  16. package/dist/create-route-map.d.ts.map +1 -1
  17. package/dist/create-route-map.js +25 -64
  18. package/dist/create-route-map.js.map +1 -1
  19. package/dist/errors.d.ts +152 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +186 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/extract-routes.d.ts +5 -31
  24. package/dist/extract-routes.d.ts.map +1 -1
  25. package/dist/extract-routes.js +73 -51
  26. package/dist/extract-routes.js.map +1 -1
  27. package/dist/find-route.d.ts +44 -0
  28. package/dist/find-route.d.ts.map +1 -0
  29. package/dist/find-route.js +103 -0
  30. package/dist/find-route.js.map +1 -0
  31. package/dist/index.d.ts +13 -51
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +13 -133
  34. package/dist/index.js.map +1 -1
  35. package/dist/machine-to-graph.d.ts +17 -0
  36. package/dist/machine-to-graph.d.ts.map +1 -0
  37. package/dist/machine-to-graph.js +115 -0
  38. package/dist/machine-to-graph.js.map +1 -0
  39. package/dist/query.d.ts +49 -1
  40. package/dist/query.d.ts.map +1 -1
  41. package/dist/query.js +98 -3
  42. package/dist/query.js.map +1 -1
  43. package/dist/router-bridge-base.d.ts +196 -32
  44. package/dist/router-bridge-base.d.ts.map +1 -1
  45. package/dist/router-bridge-base.js +319 -85
  46. package/dist/router-bridge-base.js.map +1 -1
  47. package/dist/router-sync.d.ts +95 -0
  48. package/dist/router-sync.d.ts.map +1 -0
  49. package/dist/router-sync.js +161 -0
  50. package/dist/router-sync.js.map +1 -0
  51. package/dist/types.d.ts +163 -22
  52. package/dist/types.d.ts.map +1 -1
  53. package/dist/url-pattern-utils.d.ts +117 -0
  54. package/dist/url-pattern-utils.d.ts.map +1 -0
  55. package/dist/url-pattern-utils.js +176 -0
  56. package/dist/url-pattern-utils.js.map +1 -0
  57. package/dist/validate-routes.d.ts +32 -14
  58. package/dist/validate-routes.d.ts.map +1 -1
  59. package/dist/validate-routes.js +28 -21
  60. package/dist/validate-routes.js.map +1 -1
  61. package/package.json +42 -14
  62. package/dist/connect-router.d.ts +0 -56
  63. package/dist/connect-router.d.ts.map +0 -1
  64. package/dist/connect-router.js +0 -119
  65. package/dist/connect-router.js.map +0 -1
  66. package/dist/crawl-machine.d.ts +0 -74
  67. package/dist/crawl-machine.d.ts.map +0 -1
  68. package/dist/crawl-machine.js +0 -95
  69. package/dist/crawl-machine.js.map +0 -1
  70. package/dist/create-browser-history.d.ts +0 -68
  71. package/dist/create-browser-history.d.ts.map +0 -1
  72. package/dist/create-browser-history.js +0 -94
  73. package/dist/create-browser-history.js.map +0 -1
  74. package/dist/create-router.d.ts +0 -73
  75. package/dist/create-router.d.ts.map +0 -1
  76. package/dist/create-router.js +0 -63
  77. package/dist/create-router.js.map +0 -1
  78. package/dist/extract-route.d.ts +0 -25
  79. package/dist/extract-route.d.ts.map +0 -1
  80. package/dist/extract-route.js +0 -63
  81. package/dist/extract-route.js.map +0 -1
package/dist/query.js CHANGED
@@ -1,3 +1,19 @@
1
+ import { getSuccessors, hasNode, hasPath } from "@statelyai/graph";
2
+ /**
3
+ * Get the routable successor nodes of a state — states directly reachable via
4
+ * transition edges that have a defined `meta.route`.
5
+ *
6
+ * Shared traversal core for `getTransitionReachableStateIds` and
7
+ * `getTransitionReachableRoutes`; returns full nodes so callers can project
8
+ * whichever field they need (stateId vs raw route string).
9
+ */
10
+ const getRoutableSuccessors = (graph, stateId) =>
11
+ // A state ID with no graph node (e.g. a non-routable or removed state) is a
12
+ // normal "nothing reachable" outcome, checked explicitly rather than by
13
+ // pattern-matching thrown error messages.
14
+ hasNode(graph, stateId)
15
+ ? getSuccessors(graph, stateId).filter((n) => n.data.route !== undefined)
16
+ : [];
1
17
  /**
2
18
  * Get all routes navigable from given state
3
19
  *
@@ -19,9 +35,34 @@ export const getNavigableRoutes = (tree, stateId) => {
19
35
  const node = tree.byStateId.get(stateId);
20
36
  if (!node)
21
37
  return [];
22
- return [
23
- ...node.children, // Direct child routes
24
- ];
38
+ const results = [...node.children]; // Direct child routes (existing behavior)
39
+ // If graph is available, also include routes reachable via transitions.
40
+ // Look nodes up by state ID: graph nodes carry the RAW meta.route string
41
+ // (possibly relative, e.g. "detail"), while tree.byPath is keyed by resolved
42
+ // fullPath ("/section/detail") — a path-based lookup would silently drop
43
+ // every relative route. State IDs are unambiguous in both structures.
44
+ if (tree.graph) {
45
+ const reachableStateIds = getTransitionReachableStateIds(tree.graph, stateId);
46
+ const existingPaths = new Set(results.map((r) => r.fullPath));
47
+ for (const reachableStateId of reachableStateIds) {
48
+ const routeNode = tree.byStateId.get(reachableStateId);
49
+ if (routeNode && !existingPaths.has(routeNode.fullPath)) {
50
+ results.push(routeNode);
51
+ existingPaths.add(routeNode.fullPath);
52
+ }
53
+ }
54
+ }
55
+ return results;
56
+ };
57
+ /**
58
+ * Get state IDs of routable states reachable via transitions from a state.
59
+ *
60
+ * Same traversal as `getTransitionReachableRoutes`, but returns state IDs
61
+ * instead of raw `meta.route` strings so callers can resolve nodes through
62
+ * `tree.byStateId` without raw-vs-resolved path ambiguity.
63
+ */
64
+ const getTransitionReachableStateIds = (graph, stateId) => {
65
+ return getRoutableSuccessors(graph, stateId).map((n) => n.data.stateId);
25
66
  };
26
67
  /**
27
68
  * Get all routable routes from tree as flat array
@@ -66,4 +107,58 @@ export const getRoutableRoutes = (tree) => {
66
107
  export const routeExists = (tree, path) => {
67
108
  return tree.byPath.has(path);
68
109
  };
110
+ /**
111
+ * Get routes reachable via transitions from current state
112
+ *
113
+ * Uses the @statelyai/graph successor algorithm to find all states
114
+ * directly reachable via transition edges from the given state,
115
+ * then filters to those with defined routes.
116
+ *
117
+ * Returned values are the RAW `meta.route` strings from the machine — relative
118
+ * routes (e.g. `"detail"`) are NOT resolved to full paths and cannot be used as
119
+ * `tree.byPath` keys. For resolved `RouteNode`s use `getNavigableRoutes`, which
120
+ * resolves reachable states through `tree.byStateId`.
121
+ *
122
+ * @param graph - Machine graph from RouteTree.graph
123
+ * @param stateId - Current state ID (e.g., "test.home")
124
+ * @returns Array of raw route strings reachable via transitions
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * const tree = extractMachineRoutes(machine);
129
+ * if (tree.graph) {
130
+ * const reachable = getTransitionReachableRoutes(tree.graph, 'auth.loggedIn');
131
+ * // ['/dashboard', '/settings'] — routes reachable via transitions
132
+ * }
133
+ * ```
134
+ */
135
+ export const getTransitionReachableRoutes = (graph, stateId) => {
136
+ return getRoutableSuccessors(graph, stateId).map((n) => n.data.route);
137
+ };
138
+ /**
139
+ * Check if a route is reachable from current state via transitions
140
+ *
141
+ * Uses @statelyai/graph path-finding to determine if there exists
142
+ * a chain of transition edges from the source state to the target state.
143
+ *
144
+ * @param graph - Machine graph from RouteTree.graph
145
+ * @param fromStateId - Source state ID
146
+ * @param toStateId - Target state ID
147
+ * @returns true if a transition path exists, false otherwise
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * const tree = extractMachineRoutes(machine);
152
+ * if (tree.graph) {
153
+ * const canReach = isRouteReachable(tree.graph, 'auth.login', 'auth.dashboard');
154
+ * // true if login → dashboard transition path exists
155
+ * }
156
+ * ```
157
+ */
158
+ export const isRouteReachable = (graph, fromStateId, toStateId) => {
159
+ // Missing endpoints are a normal "not reachable" outcome.
160
+ return hasNode(graph, fromStateId) && hasNode(graph, toStateId)
161
+ ? hasPath(graph, fromStateId, toStateId)
162
+ : false;
163
+ };
69
164
  //# sourceMappingURL=query.js.map
package/dist/query.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,IAAe,EAAE,OAAe,EAAe,EAAE;IACnF,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACzC,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IAErB,OAAO;QACN,GAAG,IAAI,CAAC,QAAQ,EAAE,sBAAsB;KACxC,CAAC;AACH,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,IAAe,EAAe,EAAE;IACjE,MAAM,MAAM,GAAgB,EAAE,CAAC;IAE/B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,CAAC;QAC5C,wEAAwE;QACxE,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,EAAE,KAAK,UAAU,EAAE,CAAC;YAC7C,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;IACF,CAAC;IAED,OAAO,MAAM,CAAC;AACf,CAAC,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,IAAe,EAAE,IAAY,EAAW,EAAE;IACrE,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAC9B,CAAC,CAAC"}
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"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"}
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Captures the 90% identical logic from all existing router bridges:
5
5
  * - TC39 Signal watcher for actor → router direction
6
- * - isProcessingNavigation flag for circular update prevention
7
- * - syncRouterFromActor / syncActorFromRouter with URL parameter extraction
6
+ * - `lastSyncedPath` for echo suppression in the actor→router direction
7
+ * - `isProcessingNavigation` flag for guard-redirect loop prevention in `syncActorFromRouter` only
8
+ * - `syncRouterFromActor` / `syncActorFromRouter` with URL parameter extraction
8
9
  * - connect / disconnect lifecycle matching the RouterBridge protocol
9
10
  *
10
11
  * Subclasses implement only the 3 framework-specific abstract methods:
@@ -17,7 +18,7 @@
17
18
  * export class MyRouterBridge extends RouterBridgeBase {
18
19
  * private unsubscribe: (() => void) | null = null;
19
20
  *
20
- * constructor(private router: MyRouter, actor: AbstractActor<AnyActorLogic> & Routable, routeMap: { getStateIdByPath(path: string): string | undefined; getPathByStateId(id: string): string | undefined; }) {
21
+ * constructor(private router: MyRouter, actor: RoutableActor, routeMap: { getStateIdByPath(path: string): string | undefined; getPathByStateId(id: string): string | undefined; }) {
21
22
  * super(actor, routeMap);
22
23
  * }
23
24
  *
@@ -38,12 +39,29 @@
38
39
  * }
39
40
  * ```
40
41
  *
41
- * @see {@link https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md | RFC Play v1 - Invariant INV-04}
42
+ * @see [Play RFC](../../docs/rfc/play.md) - Invariant INV-04
42
43
  */
43
44
  import { Signal } from "@xmachines/play-signals";
44
- import type { AbstractActor, Routable } from "@xmachines/play-actor";
45
- import type { AnyActorLogic } from "xstate";
45
+ import type { RoutableActor } from "./types.js";
46
46
  import type { RouterBridge } from "./types.js";
47
+ /**
48
+ * Narrow interface for the TC39 Signal watcher used by `RouterBridgeBase` to
49
+ * monitor `actor.currentRoute` changes.
50
+ *
51
+ * This interface hides the full `Signal.subtle.Watcher` surface and exposes only
52
+ * the two operations that `RouterBridgeBase` actually needs:
53
+ * - `watch(signal)` — arm the watcher on a specific signal
54
+ * - `unwatch()` — stop watching and release resources
55
+ *
56
+ * Framework adapter subclasses never interact with this handle directly; it is
57
+ * created and managed internally by `RouterBridgeBase`.
58
+ */
59
+ export interface RouteWatcherHandle {
60
+ /** Arm the watcher to observe the given signal. */
61
+ watch(signal: Signal.Computed<string | null>): void;
62
+ /** Stop observing and release the watcher. */
63
+ unwatch(): void;
64
+ }
47
65
  /**
48
66
  * Abstract base class for all `@xmachines` router adapter bridges.
49
67
  *
@@ -52,31 +70,52 @@ import type { RouterBridge } from "./types.js";
52
70
  * between frameworks.
53
71
  */
54
72
  export declare abstract class RouterBridgeBase implements RouterBridge {
55
- protected readonly actor: AbstractActor<AnyActorLogic> & Routable;
73
+ protected readonly actor: RoutableActor;
56
74
  protected readonly routeMap: {
57
- getStateIdByPath(path: string): string | undefined;
58
- getPathByStateId(id: string): string | undefined;
75
+ getStateIdByPath(path: string): string | null | undefined;
76
+ getPathByStateId(id: string): string | null | undefined;
59
77
  };
60
78
  protected isConnected: boolean;
61
79
  protected hasConnectedOnce: boolean;
62
- protected lastSyncedPath: string;
63
- protected isProcessingNavigation: boolean;
64
- protected routeWatcher: Signal.subtle.Watcher | null;
80
+ protected lastSyncedPath: string | null;
65
81
  /**
66
- * Constructor receives the 3 things all bridges need.
82
+ * Guards `syncActorFromRouter` against re-entrant calls triggered by the
83
+ * actor's own guard redirects (router→actor send → signal fires → actor→router
84
+ * push → another syncActorFromRouter before the first one returns).
67
85
  *
68
- * @param actor - Actor with currentRoute signal and send method
69
- * @param routeMap - Bidirectional route map for stateId ↔ path resolution
86
+ * NOT used for actor→router echo suppression — that is handled exclusively by
87
+ * `lastSyncedPath`, which is updated before `navigateRouter()` is called so
88
+ * any router callback for the same path short-circuits at the
89
+ * `sanitized === lastSyncedPath` check in `syncActorFromRouter`.
70
90
  */
71
- constructor(actor: AbstractActor<AnyActorLogic> & Routable, routeMap: {
72
- getStateIdByPath(path: string): string | undefined;
73
- getPathByStateId(id: string): string | undefined;
91
+ protected isProcessingNavigation: boolean;
92
+ protected routeWatcher: RouteWatcherHandle | null;
93
+ /**
94
+ * @param actor - A `RoutableActor` exposing `currentRoute`, `initialRoute`, and `send`.
95
+ * @param routeMap - Bidirectional route map for `stateId ↔ path` resolution.
96
+ * Provide `getStateIdByPath` and `getPathByStateId`. Framework adapters
97
+ * typically wrap the result of `createRouteMap(machine)` or an equivalent.
98
+ * `getPathByStateId` may be keyed on either the `"#stateId"` or the bare
99
+ * `"stateId"` form — both forms are tried automatically by the bridge, so
100
+ * custom implementations (e.g. plain test objects) need only handle one.
101
+ */
102
+ constructor(actor: RoutableActor, routeMap: {
103
+ getStateIdByPath(path: string): string | null | undefined;
104
+ getPathByStateId(id: string): string | null | undefined;
74
105
  });
75
106
  /**
76
107
  * Connect the router bridge to the Actor.
77
108
  *
78
109
  * Sets up the TC39 Signal watcher for actor → router direction and
79
110
  * starts watching router changes (framework-specific).
111
+ *
112
+ * Ordering here is part of the bridge contract:
113
+ * - `lastSyncedPath` is seeded in the constructor from `actor.currentRoute`
114
+ * - the actor watcher is installed before adapter router subscriptions
115
+ * - initial sync then resolves deep-link vs restore using `actor.initialRoute`
116
+ *
117
+ * Adapters that need custom initial-sync behavior should override
118
+ * `getInitialRouterPath()` rather than reordering `connect()` steps.
80
119
  */
81
120
  connect(): void;
82
121
  /**
@@ -88,25 +127,102 @@ export declare abstract class RouterBridgeBase implements RouterBridge {
88
127
  /**
89
128
  * Sync router location when actor route signal changes.
90
129
  *
91
- * Calls navigateRouter() for framework-specific navigation.
92
- * Prevents circular updates via isProcessingNavigation flag.
130
+ * Resolves the actor route to a concrete URL path and calls navigateRouter()
131
+ * for framework-specific navigation. When the route cannot be resolved
132
+ * (unknown stateId, parameterized pattern with no concrete values) the push
133
+ * is skipped entirely — `navigateRouter` is never called with a raw stateId
134
+ * or pattern.
135
+ *
136
+ * Echo suppression — preventing the router's own callback from re-driving the
137
+ * actor — is handled entirely by `lastSyncedPath`: it is set to the resolved
138
+ * path before `navigateRouter()` is called, so any `syncActorFromRouter`
139
+ * invocation for the same path short-circuits at the
140
+ * `sanitized === lastSyncedPath` check and sends no event regardless of
141
+ * whether the callback fires synchronously or asynchronously.
142
+ *
143
+ * `isProcessingNavigation` is NOT set here — it is only used inside
144
+ * `syncActorFromRouter` to guard against re-entrant guard-redirect loops.
93
145
  */
94
146
  protected syncRouterFromActor(route: string | null | unknown): void;
147
+ /**
148
+ * Resolve an actor route to its concrete URL path and push it to the router.
149
+ *
150
+ * lastSyncedPath must store the concrete path so that the router callback
151
+ * (which fires with the concrete path) matches and short-circuits in
152
+ * syncActorFromRouter. Storing the raw stateId (e.g. "#home") would cause a
153
+ * mismatch against the sanitized path ("/") in the watcher.
154
+ *
155
+ * When resolveNavigationPath returns null (parameterized/wildcard pattern,
156
+ * unknown id) the push is skipped entirely — pushing the raw value would write
157
+ * stateIds, `:param` patterns, or a literal `*` into the browser URL.
158
+ * lastSyncedPath is still set to the
159
+ * raw route so the dedup guard fires correctly on the next identical signal value.
160
+ */
161
+ private pushResolvedRoute;
95
162
  /**
96
163
  * Sync actor state when router location changes.
97
164
  *
98
- * Sends play.route event to actor with resolved stateId, params, and query.
99
- * Prevents circular updates via isProcessingNavigation flag.
165
+ * **Known path:** sends a `play.route` event to the actor with the matched
166
+ * stateId, params, and query. Prevents circular updates via the
167
+ * `isProcessingNavigation` flag.
168
+ *
169
+ * **Unknown/unmapped path:** does NOT send a `play.route` event (actor state
170
+ * is unchanged). Instead, actively corrects the browser URL by calling
171
+ * `navigateRouter(actor.currentRoute.get())` — keeping the URL in sync with
172
+ * actor state even when the user types an invalid path into the address bar
173
+ * or pushes one programmatically mid-session. `lastSyncedPath` is set to the
174
+ * resolved concrete path before calling `navigateRouter` so the router's own
175
+ * callback for that navigation short-circuits the echo-suppression guard and
176
+ * sends no spurious event.
100
177
  */
101
178
  protected syncActorFromRouter(pathname: string, search?: string): void;
102
179
  /**
103
- * Extract path parameters from URL using URLPattern API.
180
+ * Extract path parameters from URL using the URLPattern API.
181
+ *
182
+ * Accesses `globalThis.URLPattern` at runtime — no polyfill is imported by this
183
+ * library. If `URLPattern` is unavailable and the matched route has parameterized
184
+ * segments, a `URLPatternUnavailableError` is thrown — callers must provide a polyfill
185
+ * for environments without native URLPattern support (Node.js < 24, older browsers).
104
186
  *
105
187
  * @param pathname - The actual URL path (e.g., '/profile/john')
106
- * @param stateId - The matched state ID for looking up route pattern
107
- * @returns Extracted path parameters or empty object
188
+ * @param stateId - The matched state ID for looking up the route pattern
189
+ * @returns Extracted path parameters, or empty object if no match
190
+ * @throws {URLPatternUnavailableError} When URLPattern is absent and the route is parameterized
108
191
  */
109
192
  protected extractParams(pathname: string, stateId: string): Record<string, string>;
193
+ /**
194
+ * Resolve an actor route value to a concrete URL path for navigation.
195
+ *
196
+ * Bridges that receive raw `actor.currentRoute` values in `navigateRouter`
197
+ * can call this to normalize stateIds (with or without `#` prefix) to paths.
198
+ * Returns `null` when navigation is not possible:
199
+ * - unknown stateId with no route map entry
200
+ * - parameterized or wildcard pattern (e.g. `/profile/:id`, `/docs/*`) — no
201
+ * concrete values available to build a real URL
202
+ * - non-path string that isn't a known stateId
203
+ *
204
+ * StateId lookups try both the `"#stateId"` and bare `"stateId"` forms
205
+ * automatically (via `lookupPathByStateId`), so custom route maps keyed on
206
+ * only one form still resolve.
207
+ *
208
+ * @param route - Raw actor route value (stateId, `#`-stateId, or concrete path)
209
+ * @returns Concrete URL path, or `null` if navigation should be skipped
210
+ */
211
+ protected resolveNavigationPath(route: string): string | null;
212
+ /**
213
+ * Look up a route map path for an actor route value, trying both stateId forms.
214
+ *
215
+ * The actor may emit `"#stateId"` while a custom route map is keyed on the bare
216
+ * `"stateId"` form (or vice versa). `RouteMap` canonicalizes internally, but the
217
+ * constructor accepts any structural `{ getStateIdByPath, getPathByStateId }`
218
+ * object — plain test objects and adapter-specific maps are documented-legal —
219
+ * so both forms are tried automatically here: raw first, then the `#`-stripped
220
+ * form, then the `#`-prefixed form.
221
+ *
222
+ * @param route - Raw actor route value (stateId with or without `#` prefix)
223
+ * @returns The mapped path, or `null`/`undefined` if no form is registered
224
+ */
225
+ private lookupPathByStateId;
110
226
  /**
111
227
  * Extract query parameters from URL search string.
112
228
  *
@@ -115,17 +231,46 @@ export declare abstract class RouterBridgeBase implements RouterBridge {
115
231
  */
116
232
  protected extractQuery(search: string): Record<string, string>;
117
233
  /**
118
- * Navigate the framework router to the given path.
119
- *
120
- * Called when actor's currentRoute signal changes to a new path.
121
234
  * Must trigger the framework router's navigation (e.g., router.navigate(path)).
235
+ *
236
+ * Contract: `path` is always a RESOLVED concrete URL path (e.g. `"/home"`).
237
+ * The base class resolves actor route values (stateIds like `"#home"`,
238
+ * parameterized patterns) via `resolveNavigationPath()` before calling this
239
+ * method and skips the push entirely when resolution fails — implementations
240
+ * must NOT re-resolve and can push the value as-is.
122
241
  */
123
242
  protected abstract navigateRouter(path: string): void;
243
+ /**
244
+ * Sanitize and validate a raw URL pathname received from the router.
245
+ *
246
+ * Applies the path-length cap (2048 chars), strips query strings and fragments
247
+ * that may have been included in the pathname segment, and normalises
248
+ * consecutive slashes.
249
+ *
250
+ * Implementations that bypass `syncActorFromRouter()` (e.g. when using
251
+ * framework-native reactive watchers that receive pre-parsed route objects)
252
+ * MUST call this method before passing the path to any route-map lookup.
253
+ * `syncActorFromRouter()` calls this internally, so bridges that delegate
254
+ * to it do not need to call `sanitizePath` themselves.
255
+ *
256
+ * @param pathname - Raw URL pathname from the framework router.
257
+ * @returns Sanitized pathname, or `null` if the path is invalid / too long.
258
+ */
259
+ protected sanitizePath(pathname: string): string | null;
124
260
  /**
125
261
  * Start watching for router location changes.
126
262
  *
127
263
  * Called by connect(). Should set up the framework-specific subscription
128
264
  * for location changes and call syncActorFromRouter() on each change.
265
+ *
266
+ * **Implementations that call `syncActorFromRouter`** get path sanitization
267
+ * applied automatically — no extra work needed.
268
+ *
269
+ * **Implementations that bypass `syncActorFromRouter`** (e.g. `VueRouterBridge`,
270
+ * which builds the `play.route` event directly from framework-native route params)
271
+ * MUST call `this.sanitizePath(path)` and return early when it returns `null`.
272
+ * Skipping this allows oversized or malformed paths through without the length
273
+ * and content guards that protect the route-map lookup.
129
274
  */
130
275
  protected abstract watchRouterChanges(): void;
131
276
  /**
@@ -135,7 +280,7 @@ export declare abstract class RouterBridgeBase implements RouterBridge {
135
280
  */
136
281
  protected abstract unwatchRouterChanges(): void;
137
282
  /**
138
- * Return the router's current pathname at connect() time, or null if unavailable.
283
+ * Return the router's current pathname at connect() time.
139
284
  *
140
285
  * Called once during connect() to perform the initial URL → actor sync.
141
286
  * router.subscribe() only fires on *future* navigation events; it does not
@@ -144,9 +289,28 @@ export declare abstract class RouterBridgeBase implements RouterBridge {
144
289
  * should override this method so that deep-link / direct-URL loads drive the
145
290
  * actor to the correct state instead of leaving it at its machine default.
146
291
  *
147
- * The default returns null (no initial router → actor sync), preserving the
148
- * previous behaviour for bridges that have not yet implemented this hook.
292
+ * Return semantics:
293
+ * - `string` → router has a current path; base connect() will sync actor from router
294
+ * - `null` → router is active but has no current path yet; base connect() will sync router from actor
295
+ * - `undefined` → adapter handles initial sync itself and base connect() should stay out of the way
296
+ *
297
+ * The default returns `undefined`, preserving the previous behaviour for
298
+ * bridges that have not yet implemented this hook.
299
+ */
300
+ protected getInitialRouterPath(): string | null | undefined;
301
+ /**
302
+ * Return the router's current search string at connect() time.
303
+ *
304
+ * Paired with `getInitialRouterPath()` — called once during connect() to pass
305
+ * the initial URL query string to `syncActorFromRouter()`. If the router's current
306
+ * URL has no search (or the subclass doesn't override this), returns `undefined`
307
+ * and `syncActorFromRouter` will produce an empty `query: {}` in the event.
308
+ *
309
+ * Subclasses that override `getInitialRouterPath()` and have a query string
310
+ * available should also override this method.
311
+ *
312
+ * @returns URL search string (e.g. `"?tab=security"`), or `undefined` if not available.
149
313
  */
150
- protected getInitialRouterPath(): string | null;
314
+ protected getInitialRouterSearch(): string | undefined;
151
315
  }
152
316
  //# sourceMappingURL=router-bridge-base.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"router-bridge-base.d.ts","sourceRoot":"","sources":["../src/router-bridge-base.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/D;;;;;;GAMG;AACH,8BAAsB,gBAAiB,YAAW,YAAY;IAe5D,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,aAAa,CAAC,GAAG,QAAQ;IACjE,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE;QAC5B,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;QACnD,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;KACjD;IAjBF,SAAS,CAAC,WAAW,EAAE,OAAO,CAAS;IACvC,SAAS,CAAC,gBAAgB,EAAE,OAAO,CAAS;IAC5C,SAAS,CAAC,cAAc,EAAE,MAAM,CAAM;IACtC,SAAS,CAAC,sBAAsB,EAAE,OAAO,CAAS;IAClD,SAAS,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,GAAG,IAAI,CAAQ;IAE5D;;;;;OAKG;gBAEiB,KAAK,EAAE,aAAa,CAAC,aAAa,CAAC,GAAG,QAAQ,EAC9C,QAAQ,EAAE;QAC5B,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;QACnD,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;KACjD;IASF;;;;;OAKG;IACH,OAAO,IAAI,IAAI;IA6Cf;;;;OAIG;IACH,UAAU,IAAI,IAAI;IAqBlB;;;;;OAKG;IACH,SAAS,CAAC,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,GAAG,IAAI;IAcnE;;;;;OAKG;IACH,SAAS,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IA0CtE;;;;;;OAMG;IACH,SAAS,CAAC,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAalF;;;;;OAKG;IACH,SAAS,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAc9D;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAErD;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,CAAC,kBAAkB,IAAI,IAAI;IAE7C;;;;OAIG;IACH,SAAS,CAAC,QAAQ,CAAC,oBAAoB,IAAI,IAAI;IAE/C;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,oBAAoB,IAAI,MAAM,GAAG,IAAI;CAG/C"}
1
+ {"version":3,"file":"router-bridge-base.d.ts","sourceRoot":"","sources":["../src/router-bridge-base.ts"],"names":[],"mappings":"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"}