@xmachines/play-router 1.0.0-beta.2 → 1.0.0-beta.21

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 (66) hide show
  1. package/README.md +169 -47
  2. package/dist/base-route-map.d.ts +116 -0
  3. package/dist/base-route-map.d.ts.map +1 -0
  4. package/dist/base-route-map.js +206 -0
  5. package/dist/base-route-map.js.map +1 -0
  6. package/dist/build-tree.d.ts.map +1 -1
  7. package/dist/build-tree.js +6 -5
  8. package/dist/build-tree.js.map +1 -1
  9. package/dist/connect-router.d.ts.map +1 -1
  10. package/dist/connect-router.js +35 -45
  11. package/dist/connect-router.js.map +1 -1
  12. package/dist/create-browser-history.d.ts +38 -5
  13. package/dist/create-browser-history.d.ts.map +1 -1
  14. package/dist/create-browser-history.js +43 -17
  15. package/dist/create-browser-history.js.map +1 -1
  16. package/dist/create-route-map.d.ts +21 -1
  17. package/dist/create-route-map.d.ts.map +1 -1
  18. package/dist/create-route-map.js +73 -22
  19. package/dist/create-route-map.js.map +1 -1
  20. package/dist/errors.d.ts +75 -0
  21. package/dist/errors.d.ts.map +1 -0
  22. package/dist/errors.js +85 -0
  23. package/dist/errors.js.map +1 -0
  24. package/dist/extract-routes.d.ts +5 -31
  25. package/dist/extract-routes.d.ts.map +1 -1
  26. package/dist/extract-routes.js +70 -49
  27. package/dist/extract-routes.js.map +1 -1
  28. package/dist/find-route.d.ts +44 -0
  29. package/dist/find-route.d.ts.map +1 -0
  30. package/dist/find-route.js +126 -0
  31. package/dist/find-route.js.map +1 -0
  32. package/dist/index.d.ts +9 -48
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +16 -131
  35. package/dist/index.js.map +1 -1
  36. package/dist/machine-to-graph.d.ts +17 -0
  37. package/dist/machine-to-graph.d.ts.map +1 -0
  38. package/dist/machine-to-graph.js +115 -0
  39. package/dist/machine-to-graph.js.map +1 -0
  40. package/dist/query.d.ts +44 -1
  41. package/dist/query.d.ts.map +1 -1
  42. package/dist/query.js +80 -3
  43. package/dist/query.js.map +1 -1
  44. package/dist/router-bridge-base.d.ts +49 -19
  45. package/dist/router-bridge-base.d.ts.map +1 -1
  46. package/dist/router-bridge-base.js +120 -56
  47. package/dist/router-bridge-base.js.map +1 -1
  48. package/dist/router-sync.d.ts +62 -0
  49. package/dist/router-sync.d.ts.map +1 -0
  50. package/dist/router-sync.js +87 -0
  51. package/dist/router-sync.js.map +1 -0
  52. package/dist/types.d.ts +73 -14
  53. package/dist/types.d.ts.map +1 -1
  54. package/dist/validate-routes.d.ts +9 -9
  55. package/dist/validate-routes.d.ts.map +1 -1
  56. package/dist/validate-routes.js +12 -11
  57. package/dist/validate-routes.js.map +1 -1
  58. package/package.json +36 -18
  59. package/dist/crawl-machine.d.ts +0 -74
  60. package/dist/crawl-machine.d.ts.map +0 -1
  61. package/dist/crawl-machine.js +0 -95
  62. package/dist/crawl-machine.js.map +0 -1
  63. package/dist/extract-route.d.ts +0 -25
  64. package/dist/extract-route.d.ts.map +0 -1
  65. package/dist/extract-route.js +0 -63
  66. package/dist/extract-route.js.map +0 -1
package/dist/query.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { getSuccessors, hasPath } from "@statelyai/graph";
1
2
  /**
2
3
  * Get all routes navigable from given state
3
4
  *
@@ -19,9 +20,21 @@ export const getNavigableRoutes = (tree, stateId) => {
19
20
  const node = tree.byStateId.get(stateId);
20
21
  if (!node)
21
22
  return [];
22
- return [
23
- ...node.children, // Direct child routes
24
- ];
23
+ const results = [...node.children]; // Direct child routes (existing behavior)
24
+ // If graph is available, also include routes reachable via transitions
25
+ if (tree.graph) {
26
+ const transitionRoutes = getTransitionReachableRoutes(tree.graph, stateId);
27
+ const existingPaths = new Set(results.map((r) => r.fullPath));
28
+ for (const routePath of transitionRoutes) {
29
+ // Find the RouteNode for this path and add if not already included
30
+ const routeNode = tree.byPath.get(routePath);
31
+ if (routeNode && !existingPaths.has(routeNode.fullPath)) {
32
+ results.push(routeNode);
33
+ existingPaths.add(routeNode.fullPath);
34
+ }
35
+ }
36
+ }
37
+ return results;
25
38
  };
26
39
  /**
27
40
  * Get all routable routes from tree as flat array
@@ -66,4 +79,68 @@ export const getRoutableRoutes = (tree) => {
66
79
  export const routeExists = (tree, path) => {
67
80
  return tree.byPath.has(path);
68
81
  };
82
+ /**
83
+ * Get routes reachable via transitions from current state
84
+ *
85
+ * Uses the @statelyai/graph successor algorithm to find all states
86
+ * directly reachable via transition edges from the given state,
87
+ * then filters to those with defined routes.
88
+ *
89
+ * @param graph - Machine graph from RouteTree.graph
90
+ * @param stateId - Current state ID (e.g., "test.home")
91
+ * @returns Array of route paths reachable via transitions
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * const tree = extractMachineRoutes(machine);
96
+ * if (tree.graph) {
97
+ * const reachable = getTransitionReachableRoutes(tree.graph, 'auth.loggedIn');
98
+ * // ['/dashboard', '/settings'] — routes reachable via transitions
99
+ * }
100
+ * ```
101
+ */
102
+ export const getTransitionReachableRoutes = (graph, stateId) => {
103
+ try {
104
+ return getSuccessors(graph, stateId)
105
+ .filter((n) => n.data.route !== undefined)
106
+ .map((n) => n.data.route);
107
+ }
108
+ catch (err) {
109
+ if (err instanceof Error && /not found|does not exist/i.test(err.message)) {
110
+ return [];
111
+ }
112
+ throw err;
113
+ }
114
+ };
115
+ /**
116
+ * Check if a route is reachable from current state via transitions
117
+ *
118
+ * Uses @statelyai/graph path-finding to determine if there exists
119
+ * a chain of transition edges from the source state to the target state.
120
+ *
121
+ * @param graph - Machine graph from RouteTree.graph
122
+ * @param fromStateId - Source state ID
123
+ * @param toStateId - Target state ID
124
+ * @returns true if a transition path exists, false otherwise
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * const tree = extractMachineRoutes(machine);
129
+ * if (tree.graph) {
130
+ * const canReach = isRouteReachable(tree.graph, 'auth.login', 'auth.dashboard');
131
+ * // true if login → dashboard transition path exists
132
+ * }
133
+ * ```
134
+ */
135
+ export const isRouteReachable = (graph, fromStateId, toStateId) => {
136
+ try {
137
+ return hasPath(graph, fromStateId, toStateId);
138
+ }
139
+ catch (err) {
140
+ if (err instanceof Error && /not found|does not exist/i.test(err.message)) {
141
+ return false;
142
+ }
143
+ throw err;
144
+ }
145
+ };
69
146
  //# 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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"}
@@ -17,7 +17,7 @@
17
17
  * export class MyRouterBridge extends RouterBridgeBase {
18
18
  * private unsubscribe: (() => void) | null = null;
19
19
  *
20
- * constructor(private router: MyRouter, actor: any, routeMap: any) {
20
+ * constructor(private router: MyRouter, actor: AbstractActor<AnyActorLogic> & Routable, routeMap: { getStateIdByPath(path: string): string | undefined; getPathByStateId(id: string): string | undefined; }) {
21
21
  * super(actor, routeMap);
22
22
  * }
23
23
  *
@@ -38,12 +38,30 @@
38
38
  * }
39
39
  * ```
40
40
  *
41
- * @see {@link https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md | RFC Play v1 - Invariant INV-04}
41
+ * @see [Play RFC](../../docs/rfc/play.md) - Invariant INV-04
42
42
  */
43
43
  import { Signal } from "@xmachines/play-signals";
44
44
  import type { AbstractActor, Routable } from "@xmachines/play-actor";
45
45
  import type { AnyActorLogic } from "xstate";
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.State<unknown>): 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
  *
@@ -54,21 +72,23 @@ import type { RouterBridge } from "./types.js";
54
72
  export declare abstract class RouterBridgeBase implements RouterBridge {
55
73
  protected readonly actor: AbstractActor<AnyActorLogic> & Routable;
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
- protected lastSyncedPath: string;
78
+ protected isConnected: boolean;
79
+ protected hasConnectedOnce: boolean;
80
+ protected lastSyncedPath: string | null;
61
81
  protected isProcessingNavigation: boolean;
62
- protected routeWatcher: Signal.subtle.Watcher | null;
82
+ protected routeWatcher: RouteWatcherHandle | null;
63
83
  /**
64
- * Constructor receives the 3 things all bridges need.
65
- *
66
- * @param actor - Actor with currentRoute signal and send method
67
- * @param routeMap - Bidirectional route map for stateId ↔ path resolution
84
+ * @param actor - A `Routable` actor exposing `currentRoute` and `send`.
85
+ * @param routeMap - Bidirectional route map for `stateId ↔ path` resolution.
86
+ * Provide `getStateIdByPath` and `getPathByStateId`. Framework adapters
87
+ * typically wrap the result of `createRouteMap()` or an equivalent.
68
88
  */
69
89
  constructor(actor: AbstractActor<AnyActorLogic> & Routable, routeMap: {
70
- getStateIdByPath(path: string): string | undefined;
71
- getPathByStateId(id: string): string | undefined;
90
+ getStateIdByPath(path: string): string | null | undefined;
91
+ getPathByStateId(id: string): string | null | undefined;
72
92
  });
73
93
  /**
74
94
  * Connect the router bridge to the Actor.
@@ -98,11 +118,16 @@ export declare abstract class RouterBridgeBase implements RouterBridge {
98
118
  */
99
119
  protected syncActorFromRouter(pathname: string, search?: string): void;
100
120
  /**
101
- * Extract path parameters from URL using URLPattern API.
121
+ * Extract path parameters from URL using the URLPattern API.
122
+ *
123
+ * Accesses `globalThis.URLPattern` at runtime — no polyfill is imported by this
124
+ * library. If `URLPattern` is unavailable (Node.js < 24, older browsers without a
125
+ * polyfill), this method returns `{}` silently (graceful degradation — routing still
126
+ * works, params will be empty).
102
127
  *
103
128
  * @param pathname - The actual URL path (e.g., '/profile/john')
104
- * @param stateId - The matched state ID for looking up route pattern
105
- * @returns Extracted path parameters or empty object
129
+ * @param stateId - The matched state ID for looking up the route pattern
130
+ * @returns Extracted path parameters, or empty object if URLPattern is unavailable or no match
106
131
  */
107
132
  protected extractParams(pathname: string, stateId: string): Record<string, string>;
108
133
  /**
@@ -133,7 +158,7 @@ export declare abstract class RouterBridgeBase implements RouterBridge {
133
158
  */
134
159
  protected abstract unwatchRouterChanges(): void;
135
160
  /**
136
- * Return the router's current pathname at connect() time, or null if unavailable.
161
+ * Return the router's current pathname at connect() time.
137
162
  *
138
163
  * Called once during connect() to perform the initial URL → actor sync.
139
164
  * router.subscribe() only fires on *future* navigation events; it does not
@@ -142,9 +167,14 @@ export declare abstract class RouterBridgeBase implements RouterBridge {
142
167
  * should override this method so that deep-link / direct-URL loads drive the
143
168
  * actor to the correct state instead of leaving it at its machine default.
144
169
  *
145
- * The default returns null (no initial router → actor sync), preserving the
146
- * previous behaviour for bridges that have not yet implemented this hook.
170
+ * Return semantics:
171
+ * - `string` router has a current path; base connect() will sync actor from router
172
+ * - `null` → router is active but has no current path yet; base connect() will sync router from actor
173
+ * - `undefined` → adapter handles initial sync itself and base connect() should stay out of the way
174
+ *
175
+ * The default returns `undefined`, preserving the previous behaviour for
176
+ * bridges that have not yet implemented this hook.
147
177
  */
148
- protected getInitialRouterPath(): string | null;
178
+ protected getInitialRouterPath(): string | null | undefined;
149
179
  }
150
180
  //# 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;IAa5D,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;IAfF,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;IAsCf;;;;OAIG;IACH,UAAU,IAAI,IAAI;IAOlB;;;;;OAKG;IACH,SAAS,CAAC,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,GAAG,IAAI;IAanE;;;;;OAKG;IACH,SAAS,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAyCtE;;;;;;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;IAU9D;;;;;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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAAE,MAAM,EAAe,MAAM,yBAAyB,CAAC;AAE9D,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAO5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,kBAAkB;IAClC,mDAAmD;IACnD,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;IAC3C,8CAA8C;IAC9C,OAAO,IAAI,IAAI,CAAC;CAChB;AAED;;;;;;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,IAAI,GAAG,SAAS,CAAC;QAC1D,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;KACxD;IAjBF,SAAS,CAAC,WAAW,EAAE,OAAO,CAAS;IACvC,SAAS,CAAC,gBAAgB,EAAE,OAAO,CAAS;IAC5C,SAAS,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAQ;IAC/C,SAAS,CAAC,sBAAsB,EAAE,OAAO,CAAS;IAClD,SAAS,CAAC,YAAY,EAAE,kBAAkB,GAAG,IAAI,CAAQ;IAEzD;;;;;OAKG;gBAEiB,KAAK,EAAE,aAAa,CAAC,aAAa,CAAC,GAAG,QAAQ,EAC9C,QAAQ,EAAE;QAC5B,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;QAC1D,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;KACxD;IASF;;;;;OAKG;IACH,OAAO,IAAI,IAAI;IA+Df;;;;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;IAwCtE;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAqBlF;;;;;OAKG;IACH,SAAS,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAM9D;;;;;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;;;;;;;;;;;;;;;;;OAiBG;IACH,SAAS,CAAC,oBAAoB,IAAI,MAAM,GAAG,IAAI,GAAG,SAAS;CAG3D"}
@@ -17,7 +17,7 @@
17
17
  * export class MyRouterBridge extends RouterBridgeBase {
18
18
  * private unsubscribe: (() => void) | null = null;
19
19
  *
20
- * constructor(private router: MyRouter, actor: any, routeMap: any) {
20
+ * constructor(private router: MyRouter, actor: AbstractActor<AnyActorLogic> & Routable, routeMap: { getStateIdByPath(path: string): string | undefined; getPathByStateId(id: string): string | undefined; }) {
21
21
  * super(actor, routeMap);
22
22
  * }
23
23
  *
@@ -38,9 +38,11 @@
38
38
  * }
39
39
  * ```
40
40
  *
41
- * @see {@link https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md | RFC Play v1 - Invariant INV-04}
41
+ * @see [Play RFC](../../docs/rfc/play.md) - Invariant INV-04
42
42
  */
43
- import { Signal } from "@xmachines/play-signals";
43
+ import { Signal, watchSignal } from "@xmachines/play-signals";
44
+ import { RouterSyncError } from "./errors.js";
45
+ import { buildPlayRouteEvent, extractQuery, resolveRouteMapMatch, sanitizePathname, } from "./router-sync.js";
44
46
  /**
45
47
  * Abstract base class for all `@xmachines` router adapter bridges.
46
48
  *
@@ -52,21 +54,23 @@ export class RouterBridgeBase {
52
54
  actor;
53
55
  routeMap;
54
56
  // ── Common state (identical across all 4 existing bridges) ──
55
- lastSyncedPath = "";
57
+ isConnected = false;
58
+ hasConnectedOnce = false;
59
+ lastSyncedPath = null;
56
60
  isProcessingNavigation = false;
57
61
  routeWatcher = null;
58
62
  /**
59
- * Constructor receives the 3 things all bridges need.
60
- *
61
- * @param actor - Actor with currentRoute signal and send method
62
- * @param routeMap - Bidirectional route map for stateId ↔ path resolution
63
+ * @param actor - A `Routable` actor exposing `currentRoute` and `send`.
64
+ * @param routeMap - Bidirectional route map for `stateId ↔ path` resolution.
65
+ * Provide `getStateIdByPath` and `getPathByStateId`. Framework adapters
66
+ * typically wrap the result of `createRouteMap()` or an equivalent.
63
67
  */
64
68
  constructor(actor, routeMap) {
65
69
  this.actor = actor;
66
70
  this.routeMap = routeMap;
67
- // Initialize lastSyncedPath to actor's current route (prevents initial sync loop)
68
- // Pattern from VueRouterBridge and SolidRouterBridge
69
- this.lastSyncedPath = this.actor.currentRoute.get() || "";
71
+ // Initialize lastSyncedPath to actor's current route (prevents initial sync loop).
72
+ // null means "nothing synced yet" — distinct from any real path string.
73
+ this.lastSyncedPath = this.actor.currentRoute.get() ?? null;
70
74
  }
71
75
  // ── RouterBridge protocol (final — subclasses must not override) ──
72
76
  /**
@@ -76,16 +80,15 @@ export class RouterBridgeBase {
76
80
  * starts watching router changes (framework-specific).
77
81
  */
78
82
  connect() {
83
+ if (this.isConnected) {
84
+ return;
85
+ }
86
+ this.isConnected = true;
87
+ this.hasConnectedOnce = true;
79
88
  // Set up TC39 Signal watcher for actor → router direction
80
- this.routeWatcher = new Signal.subtle.Watcher(() => {
81
- queueMicrotask(() => {
82
- this.routeWatcher?.getPending(); // Acknowledge notification
83
- const route = this.actor.currentRoute.get();
84
- this.syncRouterFromActor(route);
85
- this.routeWatcher?.watch(this.actor.currentRoute); // Re-watch (one-shot)
86
- });
89
+ this.routeWatcher = createRouteWatcher(this.actor.currentRoute, (route) => {
90
+ this.syncRouterFromActor(route);
87
91
  });
88
- this.routeWatcher.watch(this.actor.currentRoute);
89
92
  // Start watching router changes (framework-specific)
90
93
  this.watchRouterChanges();
91
94
  // Initial sync: router → actor takes priority over actor → router.
@@ -101,9 +104,33 @@ export class RouterBridgeBase {
101
104
  // current location synchronously should override getInitialRouterPath().
102
105
  const initialRouterPath = this.getInitialRouterPath();
103
106
  const initialActorRoute = this.actor.currentRoute.get();
104
- if (initialRouterPath && initialRouterPath !== initialActorRoute) {
105
- // Router path differs from actor state sync actor from router
106
- this.syncActorFromRouter(initialRouterPath);
107
+ if (typeof initialRouterPath === "string" && initialRouterPath !== initialActorRoute) {
108
+ // Router path differs from actor route but is this a deep-link or a restore?
109
+ //
110
+ // Deep-link: router is at a non-initial URL the actor hasn't seen yet.
111
+ // → router wins: syncActorFromRouter (guards then evaluate access).
112
+ //
113
+ // Restore: browser is at the machine's initial URL while the actor was
114
+ // restored to a different route from a snapshot.
115
+ // → actor wins: push actor's restored route to the router.
116
+ //
117
+ // Detection: if the router URL equals the machine's initial route AND the
118
+ // actor is at a different route, this is a restore scenario.
119
+ if (initialActorRoute &&
120
+ initialRouterPath === this.actor.initialRoute &&
121
+ initialActorRoute !== this.actor.initialRoute) {
122
+ this.lastSyncedPath = initialActorRoute;
123
+ this.navigateRouter(initialActorRoute);
124
+ }
125
+ else {
126
+ this.syncActorFromRouter(initialRouterPath);
127
+ }
128
+ }
129
+ else if (initialActorRoute && initialRouterPath === null) {
130
+ // Explicit null: no router path available (restore/bootstrap) → push actor route.
131
+ // undefined means the adapter handles initial sync itself — fall through.
132
+ this.lastSyncedPath = initialActorRoute;
133
+ this.navigateRouter(initialActorRoute);
107
134
  }
108
135
  else if (initialActorRoute && initialActorRoute !== this.lastSyncedPath) {
109
136
  // No router path override → sync router from actor (original behaviour)
@@ -116,8 +143,22 @@ export class RouterBridgeBase {
116
143
  * Stops signal watching and unregisters framework-specific router listener.
117
144
  */
118
145
  disconnect() {
146
+ const hadRouteWatcher = this.routeWatcher !== null;
147
+ if (this.routeWatcher) {
148
+ try {
149
+ this.routeWatcher.unwatch();
150
+ }
151
+ catch {
152
+ // Ignore detached watcher errors to keep disconnect idempotent.
153
+ }
154
+ }
119
155
  this.routeWatcher = null;
120
- this.unwatchRouterChanges();
156
+ if (this.isConnected || hadRouteWatcher) {
157
+ this.unwatchRouterChanges();
158
+ }
159
+ this.isProcessingNavigation = false;
160
+ // (removed: this.hasConnectedOnce = true; — copy-paste artifact from connect(), per CONS-07)
161
+ this.isConnected = false;
121
162
  }
122
163
  // ── Sync methods (protected, overridable if subclass needs custom behavior) ──
123
164
  /**
@@ -127,6 +168,8 @@ export class RouterBridgeBase {
127
168
  * Prevents circular updates via isProcessingNavigation flag.
128
169
  */
129
170
  syncRouterFromActor(route) {
171
+ if (this.hasConnectedOnce && !this.isConnected)
172
+ return;
130
173
  if (!route || typeof route !== "string")
131
174
  return;
132
175
  if (route === this.lastSyncedPath)
@@ -147,37 +190,35 @@ export class RouterBridgeBase {
147
190
  * Prevents circular updates via isProcessingNavigation flag.
148
191
  */
149
192
  syncActorFromRouter(pathname, search) {
193
+ if (this.hasConnectedOnce && !this.isConnected)
194
+ return;
150
195
  if (typeof pathname !== "string")
151
196
  return;
152
- if (pathname === this.lastSyncedPath)
197
+ const sanitized = sanitizePathname(pathname);
198
+ if (sanitized === null)
199
+ return; // Path too long — reject
200
+ if (sanitized === this.lastSyncedPath)
153
201
  return;
154
202
  if (this.isProcessingNavigation)
155
203
  return;
156
204
  this.isProcessingNavigation = true;
157
205
  try {
158
- const stateId = this.routeMap.getStateIdByPath(pathname);
159
- if (!stateId) {
206
+ const nextRoute = buildPlayRouteEvent({
207
+ pathname,
208
+ search,
209
+ resolve: (nextPathname) => resolveRouteMapMatch(nextPathname, this.routeMap, (resolvedPathname, stateId) => this.extractParams(resolvedPathname, stateId)),
210
+ });
211
+ if (!nextRoute) {
160
212
  this.isProcessingNavigation = false;
161
213
  return;
162
214
  }
163
- const params = this.extractParams(pathname, stateId);
164
- const query = this.extractQuery(search || "");
165
- // formatPlayRouteTransitions generates guards that match event.to === "#stateId"
166
- // RouteMap stateIds from extractMachineRoutes don't include the "#" prefix,
167
- // so we normalise here. If the routeMap already stores "#stateId" (as some
168
- // manually-constructed maps do) we avoid doubling the prefix.
169
- const eventTarget = stateId.startsWith("#") ? stateId : `#${stateId}`;
170
- const event = {
171
- type: "play.route",
172
- to: eventTarget,
173
- params,
174
- query,
175
- };
176
- this.actor.send(event);
177
- this.lastSyncedPath = pathname;
215
+ this.actor.send(nextRoute.event);
216
+ this.lastSyncedPath = nextRoute.pathname;
178
217
  }
179
218
  catch (error) {
180
- console.error("[RouterBridgeBase] Error sending event:", error);
219
+ throw new RouterSyncError("Failed to sync actor state from router location.", {
220
+ cause: error,
221
+ });
181
222
  }
182
223
  finally {
183
224
  this.isProcessingNavigation = false;
@@ -185,18 +226,26 @@ export class RouterBridgeBase {
185
226
  }
186
227
  // ── Utilities (protected, overridable for framework-native param extraction) ──
187
228
  /**
188
- * Extract path parameters from URL using URLPattern API.
229
+ * Extract path parameters from URL using the URLPattern API.
230
+ *
231
+ * Accesses `globalThis.URLPattern` at runtime — no polyfill is imported by this
232
+ * library. If `URLPattern` is unavailable (Node.js < 24, older browsers without a
233
+ * polyfill), this method returns `{}` silently (graceful degradation — routing still
234
+ * works, params will be empty).
189
235
  *
190
236
  * @param pathname - The actual URL path (e.g., '/profile/john')
191
- * @param stateId - The matched state ID for looking up route pattern
192
- * @returns Extracted path parameters or empty object
237
+ * @param stateId - The matched state ID for looking up the route pattern
238
+ * @returns Extracted path parameters, or empty object if URLPattern is unavailable or no match
193
239
  */
194
240
  extractParams(pathname, stateId) {
195
241
  const pattern = this.routeMap.getPathByStateId(stateId);
196
242
  if (!pattern || !pattern.includes(":"))
197
243
  return {};
198
244
  try {
199
- const urlPattern = new URLPattern({ pathname: pattern });
245
+ const URLPatternCtor = globalThis["URLPattern"];
246
+ if (!URLPatternCtor)
247
+ return {};
248
+ const urlPattern = new URLPatternCtor({ pathname: pattern });
200
249
  const result = urlPattern.exec({ pathname });
201
250
  if (!result)
202
251
  return {};
@@ -213,15 +262,10 @@ export class RouterBridgeBase {
213
262
  * @returns Extracted query parameters or empty object
214
263
  */
215
264
  extractQuery(search) {
216
- try {
217
- return Object.fromEntries(new URLSearchParams(search));
218
- }
219
- catch {
220
- return {};
221
- }
265
+ return extractQuery(search);
222
266
  }
223
267
  /**
224
- * Return the router's current pathname at connect() time, or null if unavailable.
268
+ * Return the router's current pathname at connect() time.
225
269
  *
226
270
  * Called once during connect() to perform the initial URL → actor sync.
227
271
  * router.subscribe() only fires on *future* navigation events; it does not
@@ -230,11 +274,31 @@ export class RouterBridgeBase {
230
274
  * should override this method so that deep-link / direct-URL loads drive the
231
275
  * actor to the correct state instead of leaving it at its machine default.
232
276
  *
233
- * The default returns null (no initial router → actor sync), preserving the
234
- * previous behaviour for bridges that have not yet implemented this hook.
277
+ * Return semantics:
278
+ * - `string` router has a current path; base connect() will sync actor from router
279
+ * - `null` → router is active but has no current path yet; base connect() will sync router from actor
280
+ * - `undefined` → adapter handles initial sync itself and base connect() should stay out of the way
281
+ *
282
+ * The default returns `undefined`, preserving the previous behaviour for
283
+ * bridges that have not yet implemented this hook.
235
284
  */
236
285
  getInitialRouterPath() {
237
- return null;
286
+ return undefined;
238
287
  }
239
288
  }
289
+ function createRouteWatcher(signal, onRoute) {
290
+ let cleanup = () => { };
291
+ const watcher = {
292
+ watch(nextSignal) {
293
+ // Signal.Computed<string | null> extends Signal.State structurally;
294
+ // cast bridges the variance since RouteWatcherHandle hides implementation detail.
295
+ cleanup = watchSignal(nextSignal, onRoute);
296
+ },
297
+ unwatch() {
298
+ cleanup();
299
+ },
300
+ };
301
+ watcher.watch(signal);
302
+ return watcher;
303
+ }
240
304
  //# sourceMappingURL=router-bridge-base.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"router-bridge-base.js","sourceRoot":"","sources":["../src/router-bridge-base.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"router-bridge-base.js","sourceRoot":"","sources":["../src/router-bridge-base.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import type { PlayRouteEvent } from "./types.js";
2
+ interface RouteMatch {
3
+ to: string | null | undefined;
4
+ params?: Record<string, string>;
5
+ }
6
+ interface BuildPlayRouteEventOptions {
7
+ pathname: string;
8
+ search?: string | undefined;
9
+ resolve: (sanitizedPathname: string) => RouteMatch;
10
+ }
11
+ export interface RouterStateIdMap {
12
+ getStateIdByPath(path: string): string | null | undefined;
13
+ getPathByStateId(id: string): string | null | undefined;
14
+ }
15
+ /**
16
+ * Resolve a sanitized pathname against a route map and extract any matched params.
17
+ *
18
+ * Shared by low-level and framework router sync paths to keep route-map matching
19
+ * semantics aligned.
20
+ */
21
+ export declare function resolveRouteMapMatch(pathname: string, routeMap: RouterStateIdMap, extractParams: (pathname: string, stateId: string) => Record<string, string>): RouteMatch;
22
+ /**
23
+ * Normalize a pathname before route-map lookup.
24
+ *
25
+ * This strips query/hash fragments, collapses duplicate slashes, and rejects
26
+ * implausibly long paths that should never enter route resolution.
27
+ *
28
+ * Returns `null` for paths exceeding 2048 characters (malformed/adversarial input).
29
+ * Adapters that bypass `RouterBridgeBase.syncActorFromRouter()` (e.g. `VueRouterBridge`)
30
+ * must call this function directly and return early on `null` to apply the same
31
+ * defense-in-depth security guarantee as the shared base class.
32
+ *
33
+ * @param pathname - Raw path string from router navigation event.
34
+ * @returns Normalized path string, or `null` if the path is malformed/too long.
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * import { sanitizePathname } from "@xmachines/play-router";
39
+ *
40
+ * const clean = sanitizePathname(to.path);
41
+ * if (clean === null) return; // reject malformed path
42
+ * ```
43
+ */
44
+ export declare function sanitizePathname(pathname: string): string | null;
45
+ /**
46
+ * Parse a URL search string into the plain object shape expected by
47
+ * `play.route` events.
48
+ */
49
+ export declare function extractQuery(search: string): Record<string, string>;
50
+ /**
51
+ * Build a normalized `play.route` event from raw router/browser input.
52
+ *
53
+ * Both `connectRouter()` and `RouterBridgeBase` use this helper so low-level and
54
+ * framework adapters share the same pathname sanitization, route resolution, and
55
+ * query extraction behavior.
56
+ */
57
+ export declare function buildPlayRouteEvent(options: BuildPlayRouteEventOptions): {
58
+ pathname: string;
59
+ event: PlayRouteEvent;
60
+ } | null;
61
+ export {};
62
+ //# sourceMappingURL=router-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router-sync.d.ts","sourceRoot":"","sources":["../src/router-sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,UAAU,UAAU;IACnB,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,UAAU,0BAA0B;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,OAAO,EAAE,CAAC,iBAAiB,EAAE,MAAM,KAAK,UAAU,CAAC;CACnD;AAED,MAAM,WAAW,gBAAgB;IAChC,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC1D,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACxD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CACnC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,gBAAgB,EAC1B,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC1E,UAAU,CAUZ;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKhE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUnE;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,0BAA0B,GACjC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,cAAc,CAAA;CAAE,GAAG,IAAI,CAiBpD"}