@xmachines/play-router 1.0.0-beta.9 → 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.
- package/LICENSE +21 -0
- package/README.md +246 -341
- package/dist/base-route-map.d.ts +38 -17
- package/dist/base-route-map.d.ts.map +1 -1
- package/dist/base-route-map.js +69 -62
- package/dist/base-route-map.js.map +1 -1
- package/dist/build-tree.d.ts +7 -0
- package/dist/build-tree.d.ts.map +1 -1
- package/dist/build-tree.js +21 -6
- package/dist/build-tree.js.map +1 -1
- package/dist/create-route-map-from-tree.d.ts +33 -0
- package/dist/create-route-map-from-tree.d.ts.map +1 -0
- package/dist/create-route-map-from-tree.js +43 -0
- package/dist/create-route-map-from-tree.js.map +1 -0
- package/dist/create-route-map.d.ts +26 -50
- package/dist/create-route-map.d.ts.map +1 -1
- package/dist/create-route-map.js +24 -114
- package/dist/create-route-map.js.map +1 -1
- package/dist/errors.d.ts +152 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +186 -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 +73 -51
- 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 +103 -0
- package/dist/find-route.js.map +1 -0
- package/dist/index.d.ts +13 -52
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -137
- 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 +49 -1
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +98 -3
- package/dist/query.js.map +1 -1
- package/dist/router-bridge-base.d.ts +187 -48
- package/dist/router-bridge-base.d.ts.map +1 -1
- package/dist/router-bridge-base.js +312 -120
- package/dist/router-bridge-base.js.map +1 -1
- package/dist/router-sync.d.ts +95 -0
- package/dist/router-sync.d.ts.map +1 -0
- package/dist/router-sync.js +161 -0
- package/dist/router-sync.js.map +1 -0
- package/dist/types.d.ts +163 -22
- package/dist/types.d.ts.map +1 -1
- package/dist/url-pattern-utils.d.ts +117 -0
- package/dist/url-pattern-utils.d.ts.map +1 -0
- package/dist/url-pattern-utils.js +176 -0
- package/dist/url-pattern-utils.js.map +1 -0
- package/dist/validate-routes.d.ts +32 -14
- package/dist/validate-routes.d.ts.map +1 -1
- package/dist/validate-routes.js +28 -21
- package/dist/validate-routes.js.map +1 -1
- package/package.json +34 -14
- package/dist/connect-router.d.ts +0 -56
- package/dist/connect-router.d.ts.map +0 -1
- package/dist/connect-router.js +0 -112
- package/dist/connect-router.js.map +0 -1
- 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/create-browser-history.d.ts +0 -93
- package/dist/create-browser-history.d.ts.map +0 -1
- package/dist/create-browser-history.js +0 -94
- package/dist/create-browser-history.js.map +0 -1
- package/dist/create-router.d.ts +0 -73
- package/dist/create-router.d.ts.map +0 -1
- package/dist/create-router.js +0 -63
- package/dist/create-router.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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { toDirectedGraph } from "xstate/graph";
|
|
2
|
+
import { createGraph } from "@statelyai/graph";
|
|
3
|
+
import { UnknownStateTypeError } from "./errors.js";
|
|
4
|
+
/**
|
|
5
|
+
* Convert an XState v5 state machine to a typed @statelyai/graph Graph.
|
|
6
|
+
*
|
|
7
|
+
* The conversion pipeline is:
|
|
8
|
+
* `XState Machine → toDirectedGraph() → DirectedGraphNode hierarchy → createGraph() → Graph`
|
|
9
|
+
*
|
|
10
|
+
* The resulting graph captures:
|
|
11
|
+
* - **Hierarchy**: node parentId reflects XState compound/parallel nesting
|
|
12
|
+
* - **Edges**: all transitions with event types and optional guard types
|
|
13
|
+
* - **Metadata**: state type, meta object, and extracted route path
|
|
14
|
+
*
|
|
15
|
+
* @param machine - XState v5 state machine (any shape)
|
|
16
|
+
* @returns Typed directed graph with MachineNodeData on nodes and MachineEdgeData on edges
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* Convert a simple machine and query its routes
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { createMachine } from "xstate";
|
|
22
|
+
* import { getChildren, getSuccessors } from "@statelyai/graph";
|
|
23
|
+
* import { machineToGraph } from "@xmachines/play-router";
|
|
24
|
+
*
|
|
25
|
+
* const machine = createMachine({
|
|
26
|
+
* id: "app",
|
|
27
|
+
* initial: "home",
|
|
28
|
+
* states: {
|
|
29
|
+
* home: { meta: { route: "/home" }, on: { GO_ABOUT: "about" } },
|
|
30
|
+
* about: { meta: { route: "/about" } },
|
|
31
|
+
* },
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* const graph = machineToGraph(machine);
|
|
35
|
+
* const children = getChildren(graph, "app"); // [home, about]
|
|
36
|
+
* const successors = getSuccessors(graph, "app.home"); // [about]
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @see {@link MachineNodeData} for node data shape
|
|
40
|
+
* @see {@link MachineEdgeData} for edge data shape
|
|
41
|
+
* @see https://github.com/statelyai/graph for @statelyai/graph API reference
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* Set of valid XState state types for runtime validation.
|
|
45
|
+
* Guards against future XState changes or unexpected machine shapes.
|
|
46
|
+
*/
|
|
47
|
+
const VALID_STATE_TYPES = new Set([
|
|
48
|
+
"atomic",
|
|
49
|
+
"compound",
|
|
50
|
+
"parallel",
|
|
51
|
+
"final",
|
|
52
|
+
"history",
|
|
53
|
+
]);
|
|
54
|
+
export const machineToGraph = (machine) => {
|
|
55
|
+
const dg = toDirectedGraph(machine);
|
|
56
|
+
// Flatten DirectedGraphNode hierarchy into flat node/edge arrays
|
|
57
|
+
const nodes = [];
|
|
58
|
+
const edges = [];
|
|
59
|
+
let edgeCounter = 0;
|
|
60
|
+
const walk = (dgNode, parentId) => {
|
|
61
|
+
const sn = dgNode.stateNode;
|
|
62
|
+
const meta = sn.meta;
|
|
63
|
+
const metaRoute = meta?.["route"];
|
|
64
|
+
const route = typeof metaRoute === "string"
|
|
65
|
+
? metaRoute
|
|
66
|
+
: typeof metaRoute === "object" && metaRoute !== null && "path" in metaRoute
|
|
67
|
+
? metaRoute.path
|
|
68
|
+
: undefined;
|
|
69
|
+
// Runtime assertion: validate state type before casting
|
|
70
|
+
if (!VALID_STATE_TYPES.has(sn.type)) {
|
|
71
|
+
throw new UnknownStateTypeError(sn.type, sn.id, [...VALID_STATE_TYPES]);
|
|
72
|
+
}
|
|
73
|
+
const nodeData = {
|
|
74
|
+
stateId: sn.id,
|
|
75
|
+
type: sn.type,
|
|
76
|
+
};
|
|
77
|
+
if (meta !== undefined)
|
|
78
|
+
nodeData.meta = meta;
|
|
79
|
+
if (route !== undefined)
|
|
80
|
+
nodeData.route = route;
|
|
81
|
+
if (parentId !== undefined) {
|
|
82
|
+
nodes.push({ id: dgNode.id, parentId, data: nodeData });
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
nodes.push({ id: dgNode.id, data: nodeData });
|
|
86
|
+
}
|
|
87
|
+
// Collect edges from THIS node (edges are on source node, not graph root)
|
|
88
|
+
for (const edge of dgNode.edges) {
|
|
89
|
+
const edgeData = {
|
|
90
|
+
eventType: edge.label.text,
|
|
91
|
+
};
|
|
92
|
+
if (edge.transition.guard) {
|
|
93
|
+
edgeData.guardType = String(edge.transition.guard);
|
|
94
|
+
}
|
|
95
|
+
edges.push({
|
|
96
|
+
id: `e${edgeCounter++}`,
|
|
97
|
+
sourceId: edge.source.id,
|
|
98
|
+
targetId: edge.target.id,
|
|
99
|
+
data: edgeData,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// Recurse into children with current node as parent
|
|
103
|
+
for (const child of dgNode.children) {
|
|
104
|
+
walk(child, dgNode.id);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
walk(dg);
|
|
108
|
+
return createGraph({
|
|
109
|
+
mode: "directed",
|
|
110
|
+
initialNodeId: dg.id,
|
|
111
|
+
nodes,
|
|
112
|
+
edges,
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
//# sourceMappingURL=machine-to-graph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine-to-graph.js","sourceRoot":"","sources":["../src/machine-to-graph.ts"],"names":[],"mappings":"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"}
|
package/dist/query.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Graph } from "@statelyai/graph";
|
|
2
|
+
import type { RouteTree, RouteNode, MachineNodeData, MachineEdgeData } from "./types.js";
|
|
2
3
|
/**
|
|
3
4
|
* Get all routes navigable from given state
|
|
4
5
|
*
|
|
@@ -49,4 +50,51 @@ export declare const getRoutableRoutes: (tree: RouteTree) => RouteNode[];
|
|
|
49
50
|
* @returns true if path exists, false otherwise
|
|
50
51
|
*/
|
|
51
52
|
export declare const routeExists: (tree: RouteTree, path: string) => boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Get routes reachable via transitions from current state
|
|
55
|
+
*
|
|
56
|
+
* Uses the @statelyai/graph successor algorithm to find all states
|
|
57
|
+
* directly reachable via transition edges from the given state,
|
|
58
|
+
* then filters to those with defined routes.
|
|
59
|
+
*
|
|
60
|
+
* Returned values are the RAW `meta.route` strings from the machine — relative
|
|
61
|
+
* routes (e.g. `"detail"`) are NOT resolved to full paths and cannot be used as
|
|
62
|
+
* `tree.byPath` keys. For resolved `RouteNode`s use `getNavigableRoutes`, which
|
|
63
|
+
* resolves reachable states through `tree.byStateId`.
|
|
64
|
+
*
|
|
65
|
+
* @param graph - Machine graph from RouteTree.graph
|
|
66
|
+
* @param stateId - Current state ID (e.g., "test.home")
|
|
67
|
+
* @returns Array of raw route strings reachable via transitions
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* const tree = extractMachineRoutes(machine);
|
|
72
|
+
* if (tree.graph) {
|
|
73
|
+
* const reachable = getTransitionReachableRoutes(tree.graph, 'auth.loggedIn');
|
|
74
|
+
* // ['/dashboard', '/settings'] — routes reachable via transitions
|
|
75
|
+
* }
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export declare const getTransitionReachableRoutes: (graph: Graph<MachineNodeData, MachineEdgeData>, stateId: string) => string[];
|
|
79
|
+
/**
|
|
80
|
+
* Check if a route is reachable from current state via transitions
|
|
81
|
+
*
|
|
82
|
+
* Uses @statelyai/graph path-finding to determine if there exists
|
|
83
|
+
* a chain of transition edges from the source state to the target state.
|
|
84
|
+
*
|
|
85
|
+
* @param graph - Machine graph from RouteTree.graph
|
|
86
|
+
* @param fromStateId - Source state ID
|
|
87
|
+
* @param toStateId - Target state ID
|
|
88
|
+
* @returns true if a transition path exists, false otherwise
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* const tree = extractMachineRoutes(machine);
|
|
93
|
+
* if (tree.graph) {
|
|
94
|
+
* const canReach = isRouteReachable(tree.graph, 'auth.login', 'auth.dashboard');
|
|
95
|
+
* // true if login → dashboard transition path exists
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export declare const isRouteReachable: (graph: Graph<MachineNodeData, MachineEdgeData>, fromStateId: string, toStateId: string) => boolean;
|
|
52
100
|
//# sourceMappingURL=query.d.ts.map
|
package/dist/query.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAa,MAAM,kBAAkB,CAAC;AAEzD,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAqBzF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,SAAS,EAAE,SAAS,MAAM,KAAG,SAAS,EAyB9E,CAAC;AAgBF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,iBAAiB,GAAI,MAAM,SAAS,KAAG,SAAS,EAW5D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,SAAS,EAAE,MAAM,MAAM,KAAG,OAE3D,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,4BAA4B,GACxC,OAAO,KAAK,CAAC,eAAe,EAAE,eAAe,CAAC,EAC9C,SAAS,MAAM,KACb,MAAM,EAER,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,gBAAgB,GAC5B,OAAO,KAAK,CAAC,eAAe,EAAE,eAAe,CAAC,EAC9C,aAAa,MAAM,EACnB,WAAW,MAAM,KACf,OAKF,CAAC"}
|
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
|
-
|
|
23
|
-
|
|
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":"
|
|
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
|
-
* -
|
|
7
|
-
* -
|
|
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:
|
|
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
|
|
42
|
+
* @see [Play RFC](../../docs/rfc/play.md) - Invariant INV-04
|
|
42
43
|
*/
|
|
43
44
|
import { Signal } from "@xmachines/play-signals";
|
|
44
|
-
import type {
|
|
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,23 +70,36 @@ 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:
|
|
73
|
+
protected readonly actor: RoutableActor;
|
|
56
74
|
protected readonly routeMap: {
|
|
57
75
|
getStateIdByPath(path: string): string | null | undefined;
|
|
58
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
|
-
*
|
|
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
|
-
*
|
|
69
|
-
*
|
|
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
|
-
|
|
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: {
|
|
72
103
|
getStateIdByPath(path: string): string | null | undefined;
|
|
73
104
|
getPathByStateId(id: string): string | null | undefined;
|
|
74
105
|
});
|
|
@@ -77,6 +108,14 @@ export declare abstract class RouterBridgeBase implements RouterBridge {
|
|
|
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,50 +127,102 @@ export declare abstract class RouterBridgeBase implements RouterBridge {
|
|
|
88
127
|
/**
|
|
89
128
|
* Sync router location when actor route signal changes.
|
|
90
129
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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;
|
|
95
147
|
/**
|
|
96
|
-
*
|
|
148
|
+
* Resolve an actor route to its concrete URL path and push it to the router.
|
|
97
149
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
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.
|
|
100
160
|
*/
|
|
101
|
-
|
|
161
|
+
private pushResolvedRoute;
|
|
102
162
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* - Enforces maximum length (2048 chars) to prevent DoS via expensive URLPattern.exec()
|
|
106
|
-
* - Strips query string (everything from ? onwards) — some adapters may pass
|
|
107
|
-
* a pathname that still contains ? if extraction was imprecise
|
|
108
|
-
* - Strips hash fragment (everything from # onwards)
|
|
109
|
-
* - Normalizes consecutive slashes to single slash
|
|
110
|
-
*
|
|
111
|
-
* Returns null if the path should be rejected (too long). Returns the
|
|
112
|
-
* normalized path string otherwise.
|
|
163
|
+
* Sync actor state when router location changes.
|
|
113
164
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
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.
|
|
117
168
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
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.
|
|
120
177
|
*/
|
|
121
|
-
|
|
178
|
+
protected syncActorFromRouter(pathname: string, search?: string): void;
|
|
122
179
|
/**
|
|
123
180
|
* Extract path parameters from URL using the URLPattern API.
|
|
124
181
|
*
|
|
125
182
|
* Accesses `globalThis.URLPattern` at runtime — no polyfill is imported by this
|
|
126
|
-
* library. If `URLPattern` is unavailable
|
|
127
|
-
*
|
|
128
|
-
*
|
|
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).
|
|
129
186
|
*
|
|
130
187
|
* @param pathname - The actual URL path (e.g., '/profile/john')
|
|
131
|
-
* @param stateId - The matched state ID for looking up route pattern
|
|
132
|
-
* @returns Extracted path parameters, or empty object if
|
|
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
|
|
133
191
|
*/
|
|
134
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;
|
|
135
226
|
/**
|
|
136
227
|
* Extract query parameters from URL search string.
|
|
137
228
|
*
|
|
@@ -140,17 +231,46 @@ export declare abstract class RouterBridgeBase implements RouterBridge {
|
|
|
140
231
|
*/
|
|
141
232
|
protected extractQuery(search: string): Record<string, string>;
|
|
142
233
|
/**
|
|
143
|
-
* Navigate the framework router to the given path.
|
|
144
|
-
*
|
|
145
|
-
* Called when actor's currentRoute signal changes to a new path.
|
|
146
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.
|
|
147
241
|
*/
|
|
148
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;
|
|
149
260
|
/**
|
|
150
261
|
* Start watching for router location changes.
|
|
151
262
|
*
|
|
152
263
|
* Called by connect(). Should set up the framework-specific subscription
|
|
153
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.
|
|
154
274
|
*/
|
|
155
275
|
protected abstract watchRouterChanges(): void;
|
|
156
276
|
/**
|
|
@@ -160,7 +280,7 @@ export declare abstract class RouterBridgeBase implements RouterBridge {
|
|
|
160
280
|
*/
|
|
161
281
|
protected abstract unwatchRouterChanges(): void;
|
|
162
282
|
/**
|
|
163
|
-
* Return the router's current pathname at connect() time
|
|
283
|
+
* Return the router's current pathname at connect() time.
|
|
164
284
|
*
|
|
165
285
|
* Called once during connect() to perform the initial URL → actor sync.
|
|
166
286
|
* router.subscribe() only fires on *future* navigation events; it does not
|
|
@@ -169,9 +289,28 @@ export declare abstract class RouterBridgeBase implements RouterBridge {
|
|
|
169
289
|
* should override this method so that deep-link / direct-URL loads drive the
|
|
170
290
|
* actor to the correct state instead of leaving it at its machine default.
|
|
171
291
|
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
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.
|
|
174
313
|
*/
|
|
175
|
-
protected
|
|
314
|
+
protected getInitialRouterSearch(): string | undefined;
|
|
176
315
|
}
|
|
177
316
|
//# sourceMappingURL=router-bridge-base.d.ts.map
|