@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.
- package/README.md +169 -47
- package/dist/base-route-map.d.ts +116 -0
- package/dist/base-route-map.d.ts.map +1 -0
- package/dist/base-route-map.js +206 -0
- package/dist/base-route-map.js.map +1 -0
- package/dist/build-tree.d.ts.map +1 -1
- package/dist/build-tree.js +6 -5
- package/dist/build-tree.js.map +1 -1
- package/dist/connect-router.d.ts.map +1 -1
- package/dist/connect-router.js +35 -45
- package/dist/connect-router.js.map +1 -1
- package/dist/create-browser-history.d.ts +38 -5
- package/dist/create-browser-history.d.ts.map +1 -1
- package/dist/create-browser-history.js +43 -17
- package/dist/create-browser-history.js.map +1 -1
- package/dist/create-route-map.d.ts +21 -1
- package/dist/create-route-map.d.ts.map +1 -1
- package/dist/create-route-map.js +73 -22
- package/dist/create-route-map.js.map +1 -1
- package/dist/errors.d.ts +75 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +85 -0
- package/dist/errors.js.map +1 -0
- package/dist/extract-routes.d.ts +5 -31
- package/dist/extract-routes.d.ts.map +1 -1
- package/dist/extract-routes.js +70 -49
- package/dist/extract-routes.js.map +1 -1
- package/dist/find-route.d.ts +44 -0
- package/dist/find-route.d.ts.map +1 -0
- package/dist/find-route.js +126 -0
- package/dist/find-route.js.map +1 -0
- package/dist/index.d.ts +9 -48
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -131
- package/dist/index.js.map +1 -1
- package/dist/machine-to-graph.d.ts +17 -0
- package/dist/machine-to-graph.d.ts.map +1 -0
- package/dist/machine-to-graph.js +115 -0
- package/dist/machine-to-graph.js.map +1 -0
- package/dist/query.d.ts +44 -1
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +80 -3
- package/dist/query.js.map +1 -1
- package/dist/router-bridge-base.d.ts +49 -19
- package/dist/router-bridge-base.d.ts.map +1 -1
- package/dist/router-bridge-base.js +120 -56
- package/dist/router-bridge-base.js.map +1 -1
- package/dist/router-sync.d.ts +62 -0
- package/dist/router-sync.d.ts.map +1 -0
- package/dist/router-sync.js +87 -0
- package/dist/router-sync.js.map +1 -0
- package/dist/types.d.ts +73 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-routes.d.ts +9 -9
- package/dist/validate-routes.d.ts.map +1 -1
- package/dist/validate-routes.js +12 -11
- package/dist/validate-routes.js.map +1 -1
- package/package.json +36 -18
- package/dist/crawl-machine.d.ts +0 -74
- package/dist/crawl-machine.d.ts.map +0 -1
- package/dist/crawl-machine.js +0 -95
- package/dist/crawl-machine.js.map +0 -1
- package/dist/extract-route.d.ts +0 -25
- package/dist/extract-route.d.ts.map +0 -1
- package/dist/extract-route.js +0 -63
- package/dist/extract-route.js.map +0 -1
package/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
|
-
|
|
23
|
-
|
|
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":"
|
|
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:
|
|
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
|
|
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
|
|
78
|
+
protected isConnected: boolean;
|
|
79
|
+
protected hasConnectedOnce: boolean;
|
|
80
|
+
protected lastSyncedPath: string | null;
|
|
61
81
|
protected isProcessingNavigation: boolean;
|
|
62
|
-
protected routeWatcher:
|
|
82
|
+
protected routeWatcher: RouteWatcherHandle | null;
|
|
63
83
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
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
|
|
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
|
-
*
|
|
146
|
-
*
|
|
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,
|
|
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:
|
|
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
|
|
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
|
-
|
|
57
|
+
isConnected = false;
|
|
58
|
+
hasConnectedOnce = false;
|
|
59
|
+
lastSyncedPath = null;
|
|
56
60
|
isProcessingNavigation = false;
|
|
57
61
|
routeWatcher = null;
|
|
58
62
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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
|
-
//
|
|
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 =
|
|
81
|
-
|
|
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
|
|
106
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
159
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
234
|
-
*
|
|
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
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;
|
|
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"}
|