@xmachines/play-router 2.0.0 → 2.1.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/README.md +93 -76
- package/dist/base-route-map.d.ts +63 -57
- package/dist/base-route-map.d.ts.map +1 -1
- package/dist/base-route-map.js +65 -59
- package/dist/base-route-map.js.map +1 -1
- package/dist/build-tree.d.ts +13 -12
- package/dist/build-tree.d.ts.map +1 -1
- package/dist/build-tree.js +30 -28
- package/dist/build-tree.js.map +1 -1
- package/dist/create-route-map-from-tree.d.ts +15 -15
- package/dist/create-route-map-from-tree.js +15 -15
- package/dist/create-route-map.d.ts +18 -16
- package/dist/create-route-map.d.ts.map +1 -1
- package/dist/create-route-map.js +10 -9
- package/dist/create-route-map.js.map +1 -1
- package/dist/errors.d.ts +40 -38
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +40 -38
- package/dist/errors.js.map +1 -1
- package/dist/extract-routes.d.ts +8 -7
- package/dist/extract-routes.d.ts.map +1 -1
- package/dist/extract-routes.js +31 -27
- package/dist/extract-routes.js.map +1 -1
- package/dist/find-route.d.ts +18 -15
- package/dist/find-route.d.ts.map +1 -1
- package/dist/find-route.js +42 -38
- package/dist/find-route.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -10
- package/dist/index.js.map +1 -1
- package/dist/machine-to-graph.d.ts +3 -2
- package/dist/machine-to-graph.d.ts.map +1 -1
- package/dist/machine-to-graph.js +20 -19
- package/dist/machine-to-graph.js.map +1 -1
- package/dist/query.d.ts +39 -37
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +62 -57
- package/dist/query.js.map +1 -1
- package/dist/router-bridge-base.d.ts +208 -190
- package/dist/router-bridge-base.d.ts.map +1 -1
- package/dist/router-bridge-base.js +235 -211
- package/dist/router-bridge-base.js.map +1 -1
- package/dist/router-sync.d.ts +41 -35
- package/dist/router-sync.d.ts.map +1 -1
- package/dist/router-sync.js +53 -45
- package/dist/router-sync.js.map +1 -1
- package/dist/types.d.ts +165 -147
- package/dist/types.d.ts.map +1 -1
- package/dist/url-pattern-utils.d.ts +53 -47
- package/dist/url-pattern-utils.d.ts.map +1 -1
- package/dist/url-pattern-utils.js +61 -55
- package/dist/url-pattern-utils.js.map +1 -1
- package/dist/validate-routes.d.ts +32 -31
- package/dist/validate-routes.d.ts.map +1 -1
- package/dist/validate-routes.js +30 -29
- package/dist/validate-routes.js.map +1 -1
- package/package.json +6 -5
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* RouterBridgeBase —
|
|
2
|
+
* RouterBridgeBase — the abstract base class of every framework router adapter
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
* - `
|
|
8
|
-
* - `
|
|
9
|
-
* -
|
|
4
|
+
* The class holds the logic that each router bridge shares, and that is 90% of the
|
|
5
|
+
* code of every bridge:
|
|
6
|
+
* - The TC39 Signal watcher of the direction from the actor to the router
|
|
7
|
+
* - `lastSyncedPath`, for the echo suppression in the direction from the actor to the router
|
|
8
|
+
* - The `isProcessingNavigation` flag, which stops a loop of a guard redirect in `syncActorFromRouter` only
|
|
9
|
+
* - `syncRouterFromActor` and `syncActorFromRouter`, with the read of each URL parameter
|
|
10
|
+
* - The connect and disconnect lifecycle of the RouterBridge protocol
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
* - navigateRouter(path):
|
|
13
|
-
* - watchRouterChanges():
|
|
14
|
-
* - unwatchRouterChanges():
|
|
12
|
+
* A subclass implements the 3 abstract methods of its framework only:
|
|
13
|
+
* - navigateRouter(path): how the bridge tells the framework router to navigate
|
|
14
|
+
* - watchRouterChanges(): how the bridge subscribes to each location change of the router
|
|
15
|
+
* - unwatchRouterChanges(): how the bridge cancels that subscription
|
|
15
16
|
*
|
|
16
17
|
* @example
|
|
17
18
|
* ```typescript
|
|
@@ -39,7 +40,7 @@
|
|
|
39
40
|
* }
|
|
40
41
|
* ```
|
|
41
42
|
*
|
|
42
|
-
* @see [Play RFC](../../docs/rfc/play.md) -
|
|
43
|
+
* @see [Play RFC](../../docs/rfc/play.md) - invariant INV-04
|
|
43
44
|
*/
|
|
44
45
|
import { Signal, watchSignal } from "@xmachines/play-signals";
|
|
45
46
|
import { DuplicateBridgeError, RouterSyncError } from "./errors.js";
|
|
@@ -47,79 +48,86 @@ import { buildPlayRouteEvent, extractQuery, extractRouteParams, matchRouteMap, s
|
|
|
47
48
|
import { URLPatternUnavailableError } from "./errors.js";
|
|
48
49
|
import { isParameterizedPattern } from "./url-pattern-utils.js";
|
|
49
50
|
/**
|
|
50
|
-
*
|
|
51
|
+
* The registry of the actors with an active bridge connection, at the module level.
|
|
51
52
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* `play.route` events
|
|
53
|
+
* The key is the actor instance, and the registry is a WeakMap, which holds no
|
|
54
|
+
* reference. The class uses it to find and refuse a second `connect()` call of the
|
|
55
|
+
* same actor. Such a call makes two `play.route` events for one navigation, and it
|
|
56
|
+
* makes two `lastSyncedPath` values of the echo suppression that contradict each
|
|
57
|
+
* other.
|
|
55
58
|
*
|
|
56
59
|
* @internal
|
|
57
60
|
*/
|
|
58
61
|
const activeBridges = new WeakMap();
|
|
59
62
|
/**
|
|
60
|
-
*
|
|
63
|
+
* The abstract base class of every router adapter bridge of `@xmachines`.
|
|
61
64
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
+
* The class implements the RouterBridge protocol, and it holds every part of the
|
|
66
|
+
* bridge logic that the adapters share. A subclass implements the 3 abstract methods
|
|
67
|
+
* that are different in each framework, and it implements nothing more.
|
|
65
68
|
*/
|
|
66
69
|
export class RouterBridgeBase {
|
|
67
70
|
actor;
|
|
68
71
|
routeMap;
|
|
69
|
-
// ──
|
|
72
|
+
// ── The common state. It is identical in each of the 4 bridges ──
|
|
70
73
|
isConnected = false;
|
|
71
74
|
hasConnectedOnce = false;
|
|
72
75
|
lastSyncedPath = null;
|
|
73
76
|
/**
|
|
74
|
-
*
|
|
75
|
-
* actor
|
|
76
|
-
*
|
|
77
|
+
* The flag guards `syncActorFromRouter` against a re-entrant call from a guard
|
|
78
|
+
* redirect of the actor itself. Such a call has this sequence: the bridge sends to
|
|
79
|
+
* the actor, the signal fires, the bridge pushes to the router, and a second
|
|
80
|
+
* `syncActorFromRouter` call starts before the first one returns.
|
|
77
81
|
*
|
|
78
|
-
*
|
|
79
|
-
* `lastSyncedPath
|
|
80
|
-
*
|
|
81
|
-
* `sanitized === lastSyncedPath`
|
|
82
|
+
* The flag is NOT the echo suppression of the direction from the actor to the
|
|
83
|
+
* router. `lastSyncedPath` does that work alone: the bridge writes it before the
|
|
84
|
+
* `navigateRouter()` call. Therefore each router callback of the same path stops at
|
|
85
|
+
* the `sanitized === lastSyncedPath` test in `syncActorFromRouter`.
|
|
82
86
|
*/
|
|
83
87
|
isProcessingNavigation = false;
|
|
84
88
|
routeWatcher = null;
|
|
85
89
|
/**
|
|
86
|
-
* @param actor - A `RoutableActor
|
|
87
|
-
* @param routeMap -
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* `"stateId"
|
|
92
|
-
*
|
|
90
|
+
* @param actor - A `RoutableActor`, with `currentRoute`, `initialRoute`, and `send`.
|
|
91
|
+
* @param routeMap - The route map of both directions, for the resolution between a
|
|
92
|
+
* `stateId` and a `path`. Give `getStateIdByPath` and `getPathByStateId`. A
|
|
93
|
+
* framework adapter usually wraps the result of `createRouteMap(machine)`, or an
|
|
94
|
+
* equivalent value. The key of `getPathByStateId` is the form `"#stateId"` or the
|
|
95
|
+
* bare form `"stateId"`. The bridge tries both forms. Therefore an implementation
|
|
96
|
+
* of your own, for example a plain test object, handles one form only.
|
|
93
97
|
*/
|
|
94
98
|
constructor(actor, routeMap) {
|
|
95
99
|
this.actor = actor;
|
|
96
100
|
this.routeMap = routeMap;
|
|
97
|
-
//
|
|
98
|
-
// null means "nothing
|
|
101
|
+
// Set lastSyncedPath to the current route of the actor. This stops a loop in the
|
|
102
|
+
// first synchronization. The value null means "nothing is in step yet", and it is
|
|
103
|
+
// therefore different from each real path string.
|
|
99
104
|
this.lastSyncedPath = this.actor.currentRoute.get() ?? null;
|
|
100
105
|
}
|
|
101
|
-
// ── RouterBridge protocol
|
|
106
|
+
// ── The RouterBridge protocol. It is final: a subclass must override nothing here ──
|
|
102
107
|
/**
|
|
103
|
-
*
|
|
108
|
+
* Connects the router bridge to the Actor.
|
|
104
109
|
*
|
|
105
|
-
*
|
|
106
|
-
* starts
|
|
110
|
+
* The method installs the TC39 Signal watcher of the direction from the actor to the
|
|
111
|
+
* router. It then starts the watch of the router changes, which each framework does
|
|
112
|
+
* in its own way.
|
|
107
113
|
*
|
|
108
|
-
*
|
|
109
|
-
* - `lastSyncedPath`
|
|
110
|
-
* - the actor watcher
|
|
111
|
-
* -
|
|
114
|
+
* The order of these steps is part of the contract of the bridge:
|
|
115
|
+
* - The constructor seeds `lastSyncedPath` from `actor.currentRoute`
|
|
116
|
+
* - The method installs the actor watcher before the router subscriptions of the adapter
|
|
117
|
+
* - The first synchronization then separates a deep link from a restore, with `actor.initialRoute`
|
|
112
118
|
*
|
|
113
|
-
*
|
|
114
|
-
* `getInitialRouterPath()
|
|
119
|
+
* An adapter that needs a different behavior of the first synchronization overrides
|
|
120
|
+
* `getInitialRouterPath()`. It does not change the order of the steps of
|
|
121
|
+
* `connect()`.
|
|
115
122
|
*/
|
|
116
123
|
connect() {
|
|
117
124
|
if (this.isConnected) {
|
|
118
125
|
return;
|
|
119
126
|
}
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
// events and
|
|
127
|
+
// Permit one bridge for each actor: two connected bridges of the same actor both
|
|
128
|
+
// receive every navigation event of the router. They therefore send two play.route
|
|
129
|
+
// events, and their lastSyncedPath values of the echo suppression contradict each
|
|
130
|
+
// other.
|
|
123
131
|
const existingBridge = activeBridges.get(this.actor);
|
|
124
132
|
if (existingBridge !== undefined && existingBridge !== this) {
|
|
125
133
|
throw new DuplicateBridgeError();
|
|
@@ -127,41 +135,42 @@ export class RouterBridgeBase {
|
|
|
127
135
|
activeBridges.set(this.actor, this);
|
|
128
136
|
this.isConnected = true;
|
|
129
137
|
this.hasConnectedOnce = true;
|
|
130
|
-
//
|
|
138
|
+
// Install the TC39 Signal watcher of the direction from the actor to the router
|
|
131
139
|
this.routeWatcher = createRouteWatcher(this.actor.currentRoute, (route) => {
|
|
132
140
|
this.syncRouterFromActor(route);
|
|
133
141
|
});
|
|
134
|
-
// Start
|
|
142
|
+
// Start the watch of the router changes. Each framework does this in its own way
|
|
135
143
|
this.watchRouterChanges();
|
|
136
|
-
//
|
|
144
|
+
// The first synchronization: the direction from the router to the actor has the
|
|
145
|
+
// priority over the other direction.
|
|
137
146
|
//
|
|
138
|
-
//
|
|
139
|
-
// user
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
// actor
|
|
147
|
+
// The page can load on a URL that is not the initial state of the actor. For
|
|
148
|
+
// example, the user types "/about", or the user follows a deep link. The router
|
|
149
|
+
// holds the correct path then, and the actor is still at its initial state ("/").
|
|
150
|
+
// The code must therefore drive the actor to the URL, and it must not write the
|
|
151
|
+
// default of the actor to the URL.
|
|
143
152
|
//
|
|
144
|
-
// router.subscribe()
|
|
145
|
-
//
|
|
146
|
-
//
|
|
153
|
+
// router.subscribe() fires on a *later* navigation event only. It does NOT fire for
|
|
154
|
+
// the location that the page loaded. A subclass that can read the current location
|
|
155
|
+
// of its router synchronously overrides getInitialRouterPath().
|
|
147
156
|
const initialRouterPath = this.getInitialRouterPath();
|
|
148
157
|
const initialRouterSearch = this.getInitialRouterSearch();
|
|
149
158
|
const initialActorRoute = this.actor.currentRoute.get();
|
|
150
159
|
if (typeof initialRouterPath === "string") {
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
// is a URL path
|
|
154
|
-
// to
|
|
160
|
+
// Test if the actor is at the current location of the router already.
|
|
161
|
+
// The currentRoute of the actor can be a stateId, for example "#app.home", and
|
|
162
|
+
// initialRouterPath is a URL path, for example "/home". Therefore resolve the path
|
|
163
|
+
// to a stateId before the comparison. Without this step, a false "the two are
|
|
164
|
+
// different" starts a first synchronization that nothing needs.
|
|
155
165
|
const resolvedStateId = this.routeMap.getStateIdByPath(sanitizePathname(initialRouterPath) ?? initialRouterPath);
|
|
156
|
-
// The actor is
|
|
157
|
-
// directly, or
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
// path only.
|
|
166
|
+
// The actor is at the location of the router in two cases: the two values match
|
|
167
|
+
// directly, or the actor route is a stateId that resolves to the same registered
|
|
168
|
+
// path as the stateId of the match. Each lookup goes through lookupPathByStateId,
|
|
169
|
+
// which tries the form "#stateId" and also the bare form "stateId". Therefore a
|
|
170
|
+
// route map with "#about" recognizes an actor route of "about", and the opposite
|
|
171
|
+
// also works, and this is correct for a structural map of your own that holds one
|
|
172
|
+
// form. An actor route with a concrete path, which starts with "/", goes to a direct
|
|
173
|
+
// comparison with the router path only.
|
|
165
174
|
const actorAlreadyAtRouterLocation = resolvedStateId !== null &&
|
|
166
175
|
resolvedStateId !== undefined &&
|
|
167
176
|
(resolvedStateId === initialActorRoute ||
|
|
@@ -172,17 +181,19 @@ export class RouterBridgeBase {
|
|
|
172
181
|
this.lookupPathByStateId(initialActorRoute) ===
|
|
173
182
|
this.lookupPathByStateId(resolvedStateId)));
|
|
174
183
|
if (!actorAlreadyAtRouterLocation) {
|
|
175
|
-
//
|
|
184
|
+
// The path of the router is different from the actor route. Is this a deep link, or
|
|
185
|
+
// is it a restore?
|
|
176
186
|
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
187
|
+
// A deep link: the router is at a URL that is not the initial one, and the actor saw
|
|
188
|
+
// that URL not yet.
|
|
189
|
+
// → the router wins: syncActorFromRouter. The guards then decide the access.
|
|
179
190
|
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
// → actor wins: push
|
|
191
|
+
// A restore: the browser is at the initial URL of the machine, and a snapshot
|
|
192
|
+
// restored the actor to a different route.
|
|
193
|
+
// → the actor wins: push the restored route of the actor to the router.
|
|
183
194
|
//
|
|
184
|
-
//
|
|
185
|
-
// actor is at a different route
|
|
195
|
+
// The detection: the URL of the router is the initial route of the machine, AND the
|
|
196
|
+
// actor is at a different route. This is then a restore.
|
|
186
197
|
if (initialActorRoute &&
|
|
187
198
|
initialRouterPath === this.actor.initialRoute &&
|
|
188
199
|
initialActorRoute !== this.actor.initialRoute) {
|
|
@@ -194,19 +205,22 @@ export class RouterBridgeBase {
|
|
|
194
205
|
}
|
|
195
206
|
}
|
|
196
207
|
else if (initialActorRoute && initialRouterPath === null) {
|
|
197
|
-
//
|
|
198
|
-
//
|
|
208
|
+
// An explicit null: no path of the router is available, from a restore or from a
|
|
209
|
+
// bootstrap → push the actor route.
|
|
210
|
+
// The value undefined means that the adapter does the first synchronization itself.
|
|
211
|
+
// Continue.
|
|
199
212
|
this.pushResolvedRoute(initialActorRoute);
|
|
200
213
|
}
|
|
201
214
|
else if (initialActorRoute && initialActorRoute !== this.lastSyncedPath) {
|
|
202
|
-
// No router
|
|
215
|
+
// No path of the router replaces the actor route → write the router from the actor, as before
|
|
203
216
|
this.syncRouterFromActor(initialActorRoute);
|
|
204
217
|
}
|
|
205
218
|
}
|
|
206
219
|
/**
|
|
207
|
-
*
|
|
220
|
+
* Disconnects the router bridge from the Actor.
|
|
208
221
|
*
|
|
209
|
-
*
|
|
222
|
+
* The method stops the watch of the signal, and it removes the router listener of the
|
|
223
|
+
* framework.
|
|
210
224
|
*/
|
|
211
225
|
disconnect() {
|
|
212
226
|
const hadRouteWatcher = this.routeWatcher !== null;
|
|
@@ -215,7 +229,7 @@ export class RouterBridgeBase {
|
|
|
215
229
|
this.routeWatcher.unwatch();
|
|
216
230
|
}
|
|
217
231
|
catch {
|
|
218
|
-
// Ignore detached watcher
|
|
232
|
+
// Ignore an error of a detached watcher. disconnect() therefore stays idempotent.
|
|
219
233
|
}
|
|
220
234
|
}
|
|
221
235
|
this.routeWatcher = null;
|
|
@@ -224,32 +238,33 @@ export class RouterBridgeBase {
|
|
|
224
238
|
}
|
|
225
239
|
this.isProcessingNavigation = false;
|
|
226
240
|
this.isConnected = false;
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
241
|
+
// Free the slot of the actor, so that a new bridge can connect to the same actor.
|
|
242
|
+
// Clear the slot only when this bridge is the registered one: a second bridge that
|
|
243
|
+
// connect() refused must not remove the legitimate bridge.
|
|
230
244
|
if (activeBridges.get(this.actor) === this) {
|
|
231
245
|
activeBridges.delete(this.actor);
|
|
232
246
|
}
|
|
233
247
|
}
|
|
234
|
-
// ──
|
|
248
|
+
// ── The sync methods. They are protected, and a subclass overrides one for its own behavior ──
|
|
235
249
|
/**
|
|
236
|
-
*
|
|
250
|
+
* Writes the location of the router when the route signal of the actor changes.
|
|
237
251
|
*
|
|
238
|
-
*
|
|
239
|
-
* for
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
* or pattern.
|
|
252
|
+
* The method resolves the actor route to a concrete URL path, then it calls
|
|
253
|
+
* navigateRouter() for the navigation of the framework. When it cannot resolve the
|
|
254
|
+
* route, which happens for an unknown stateId and for a parameterized pattern
|
|
255
|
+
* without concrete values, it skips the push completely: `navigateRouter` receives a
|
|
256
|
+
* raw stateId or a pattern never.
|
|
243
257
|
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* `sanitized === lastSyncedPath`
|
|
249
|
-
*
|
|
258
|
+
* `lastSyncedPath` does the complete echo suppression, which stops the callback of
|
|
259
|
+
* the router from a send to the actor: the method writes the resolved path to
|
|
260
|
+
* `lastSyncedPath` before the `navigateRouter()` call. Therefore each
|
|
261
|
+
* `syncActorFromRouter` call of the same path stops at the
|
|
262
|
+
* `sanitized === lastSyncedPath` test, and it sends no event. This is correct for a
|
|
263
|
+
* synchronous callback and for an asynchronous callback.
|
|
250
264
|
*
|
|
251
|
-
*
|
|
252
|
-
* `syncActorFromRouter
|
|
265
|
+
* The method does NOT set `isProcessingNavigation`. That flag lives inside
|
|
266
|
+
* `syncActorFromRouter`, and it guards against a re-entrant loop of a guard
|
|
267
|
+
* redirect.
|
|
253
268
|
*/
|
|
254
269
|
syncRouterFromActor(route) {
|
|
255
270
|
if (this.hasConnectedOnce && !this.isConnected)
|
|
@@ -261,18 +276,20 @@ export class RouterBridgeBase {
|
|
|
261
276
|
this.pushResolvedRoute(route);
|
|
262
277
|
}
|
|
263
278
|
/**
|
|
264
|
-
*
|
|
279
|
+
* Resolves an actor route to its concrete URL path, and pushes that path to the
|
|
280
|
+
* router.
|
|
265
281
|
*
|
|
266
|
-
* lastSyncedPath must
|
|
267
|
-
*
|
|
268
|
-
* syncActorFromRouter.
|
|
269
|
-
*
|
|
282
|
+
* lastSyncedPath must hold the concrete path, because the callback of the router
|
|
283
|
+
* fires with the concrete path. The two values therefore match, and
|
|
284
|
+
* syncActorFromRouter stops. A raw stateId in that field, for example "#home", does
|
|
285
|
+
* not match the sanitized path ("/") in the watcher.
|
|
270
286
|
*
|
|
271
|
-
* When resolveNavigationPath returns null
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
287
|
+
* When resolveNavigationPath returns null, which happens for a parameterized
|
|
288
|
+
* pattern, for a wildcard pattern, and for an unknown id, the method skips the push
|
|
289
|
+
* completely: a push of the raw value writes a stateId, a `:param` pattern, or a
|
|
290
|
+
* literal `*` into the browser URL. The method still writes the raw route to
|
|
291
|
+
* lastSyncedPath. Therefore the dedup guard fires correctly on the next identical
|
|
292
|
+
* value of the signal.
|
|
276
293
|
*/
|
|
277
294
|
pushResolvedRoute(route) {
|
|
278
295
|
const resolved = this.resolveNavigationPath(route);
|
|
@@ -282,20 +299,20 @@ export class RouterBridgeBase {
|
|
|
282
299
|
this.navigateRouter(resolved);
|
|
283
300
|
}
|
|
284
301
|
/**
|
|
285
|
-
*
|
|
302
|
+
* Writes the actor state when the location of the router changes.
|
|
286
303
|
*
|
|
287
|
-
* **
|
|
288
|
-
* stateId, params, and query.
|
|
289
|
-
*
|
|
304
|
+
* **A known path:** the method sends a `play.route` event to the actor, with the
|
|
305
|
+
* stateId of the match, the params, and the query. The `isProcessingNavigation` flag
|
|
306
|
+
* stops a circular update.
|
|
290
307
|
*
|
|
291
|
-
* **
|
|
292
|
-
*
|
|
293
|
-
* `navigateRouter(actor.currentRoute.get())`
|
|
294
|
-
* actor state
|
|
295
|
-
* or pushes
|
|
296
|
-
* resolved concrete path
|
|
297
|
-
* callback for that navigation
|
|
298
|
-
* sends no
|
|
308
|
+
* **An unknown path, or a path with no entry in the map:** the method sends NO
|
|
309
|
+
* `play.route` event, and the actor state stays as it is. It corrects the browser URL
|
|
310
|
+
* instead, with a `navigateRouter(actor.currentRoute.get())` call. The URL therefore
|
|
311
|
+
* follows the actor state, also when the user types an invalid path in the address
|
|
312
|
+
* bar, or when the code pushes such a path during a session. The method writes the
|
|
313
|
+
* resolved concrete path to `lastSyncedPath` before the `navigateRouter` call.
|
|
314
|
+
* Therefore the callback of the router for that navigation stops at the guard of the
|
|
315
|
+
* echo suppression, and it sends no false event.
|
|
299
316
|
*/
|
|
300
317
|
syncActorFromRouter(pathname, search) {
|
|
301
318
|
if (this.hasConnectedOnce && !this.isConnected)
|
|
@@ -304,7 +321,7 @@ export class RouterBridgeBase {
|
|
|
304
321
|
return;
|
|
305
322
|
const sanitized = sanitizePathname(pathname);
|
|
306
323
|
if (sanitized === null)
|
|
307
|
-
return; //
|
|
324
|
+
return; // The path is too long. Refuse it
|
|
308
325
|
if (sanitized === this.lastSyncedPath)
|
|
309
326
|
return;
|
|
310
327
|
if (this.isProcessingNavigation)
|
|
@@ -317,21 +334,22 @@ export class RouterBridgeBase {
|
|
|
317
334
|
match: (nextPathname) => matchRouteMap(nextPathname, this.routeMap, (resolvedPathname, stateId) => this.extractParams(resolvedPathname, stateId)),
|
|
318
335
|
});
|
|
319
336
|
if (!nextRoute) {
|
|
320
|
-
//
|
|
321
|
-
// Push the
|
|
322
|
-
//
|
|
323
|
-
//
|
|
337
|
+
// The path is unknown, because the map holds no route of it.
|
|
338
|
+
// Push the current route of the actor back to the router. The browser URL therefore
|
|
339
|
+
// follows the actor state, and this corrects an invalid entry in the address bar on a
|
|
340
|
+
// cold load, and also a navigation to a path that is absent during a session.
|
|
324
341
|
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
// guard and
|
|
342
|
+
// The code calls pushResolvedRoute() directly, and not syncRouterFromActor(). It
|
|
343
|
+
// therefore passes around the dedup guard, because lastSyncedPath holds the route of
|
|
344
|
+
// the actor already. pushResolvedRoute sets lastSyncedPath to the resolved concrete
|
|
345
|
+
// path before the push. Therefore the callback of the router for this navigation, a
|
|
346
|
+
// hashchange event or a history event, stops at the
|
|
347
|
+
// sanitized === lastSyncedPath guard, and it sends no false play.route event to the
|
|
348
|
+
// actor.
|
|
331
349
|
//
|
|
332
|
-
// isProcessingNavigation
|
|
333
|
-
//
|
|
334
|
-
// re-
|
|
350
|
+
// isProcessingNavigation stays true here. The finally block below clears it after
|
|
351
|
+
// navigateRouter returns. That block therefore guards synchronously against a
|
|
352
|
+
// re-entrance of each router callback that fires at once.
|
|
335
353
|
const currentActorRoute = this.actor.currentRoute.get();
|
|
336
354
|
if (currentActorRoute) {
|
|
337
355
|
this.pushResolvedRoute(currentActorRoute);
|
|
@@ -342,8 +360,9 @@ export class RouterBridgeBase {
|
|
|
342
360
|
this.lastSyncedPath = nextRoute.pathname;
|
|
343
361
|
}
|
|
344
362
|
catch (error) {
|
|
345
|
-
// URLPatternUnavailableError
|
|
346
|
-
//
|
|
363
|
+
// A URLPatternUnavailableError needs an action of the user: it goes to the caller
|
|
364
|
+
// without a change. The caller can therefore find it, and ask the user for a
|
|
365
|
+
// URLPattern polyfill.
|
|
347
366
|
if (error instanceof URLPatternUnavailableError) {
|
|
348
367
|
throw error;
|
|
349
368
|
}
|
|
@@ -355,19 +374,20 @@ export class RouterBridgeBase {
|
|
|
355
374
|
this.isProcessingNavigation = false;
|
|
356
375
|
}
|
|
357
376
|
}
|
|
358
|
-
// ──
|
|
377
|
+
// ── The utilities. They are protected, and a subclass overrides one to read the params of its framework ──
|
|
359
378
|
/**
|
|
360
|
-
*
|
|
379
|
+
* Reads the path parameters of a URL, with the URLPattern API.
|
|
361
380
|
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
381
|
+
* The method reads `globalThis.URLPattern` at run time, because this library imports
|
|
382
|
+
* no polyfill. When `URLPattern` is absent and the route of the match holds a
|
|
383
|
+
* parameterized segment, the method throws a `URLPatternUnavailableError`. A caller
|
|
384
|
+
* must therefore give a polyfill in an environment without the native URLPattern:
|
|
385
|
+
* Node.js < 24, and an older browser.
|
|
366
386
|
*
|
|
367
|
-
* @param pathname - The
|
|
368
|
-
* @param stateId - The
|
|
369
|
-
* @returns
|
|
370
|
-
* @throws {URLPatternUnavailableError} When URLPattern is absent and the route
|
|
387
|
+
* @param pathname - The real URL path, for example '/profile/john'
|
|
388
|
+
* @param stateId - The stateId of the match, for the lookup of the route pattern
|
|
389
|
+
* @returns The path parameters of the read, or an empty object when nothing matches
|
|
390
|
+
* @throws {URLPatternUnavailableError} When URLPattern is absent and the route holds a parameter
|
|
371
391
|
*/
|
|
372
392
|
extractParams(pathname, stateId) {
|
|
373
393
|
const pattern = this.routeMap.getPathByStateId(stateId);
|
|
@@ -376,22 +396,22 @@ export class RouterBridgeBase {
|
|
|
376
396
|
return extractRouteParams(pathname, pattern);
|
|
377
397
|
}
|
|
378
398
|
/**
|
|
379
|
-
*
|
|
399
|
+
* Resolves a value of an actor route to a concrete URL path for a navigation.
|
|
380
400
|
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
* - unknown stateId with no route map
|
|
385
|
-
* - parameterized or wildcard pattern
|
|
386
|
-
* concrete
|
|
387
|
-
* -
|
|
401
|
+
* A bridge that receives a raw `actor.currentRoute` value in `navigateRouter` calls
|
|
402
|
+
* this method to convert a stateId, with a `#` prefix or without one, into a path.
|
|
403
|
+
* The method returns `null` when a navigation is not possible:
|
|
404
|
+
* - an unknown stateId, with no entry in the route map
|
|
405
|
+
* - a parameterized pattern or a wildcard pattern, for example `/profile/:id` or
|
|
406
|
+
* `/docs/*`, because no concrete value is available for a real URL
|
|
407
|
+
* - a string that is no path and no known stateId
|
|
388
408
|
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
* only
|
|
409
|
+
* A lookup of a stateId tries the form `"#stateId"` and the bare form `"stateId"`,
|
|
410
|
+
* through `lookupPathByStateId`. Therefore a route map of your own that holds one
|
|
411
|
+
* form only still resolves the path.
|
|
392
412
|
*
|
|
393
|
-
* @param route -
|
|
394
|
-
* @returns
|
|
413
|
+
* @param route - The raw value of the actor route: a stateId, a stateId with a `#`, or a concrete path
|
|
414
|
+
* @returns The concrete URL path, or `null` when the bridge must skip the navigation
|
|
395
415
|
*/
|
|
396
416
|
resolveNavigationPath(route) {
|
|
397
417
|
const mapped = this.lookupPathByStateId(route);
|
|
@@ -401,17 +421,19 @@ export class RouterBridgeBase {
|
|
|
401
421
|
return path;
|
|
402
422
|
}
|
|
403
423
|
/**
|
|
404
|
-
*
|
|
424
|
+
* Returns the path of the route map for a value of an actor route, and it tries both
|
|
425
|
+
* forms of a stateId.
|
|
405
426
|
*
|
|
406
|
-
* The actor
|
|
407
|
-
* `"stateId"
|
|
408
|
-
* constructor accepts
|
|
409
|
-
* object
|
|
410
|
-
*
|
|
411
|
-
* form, then the
|
|
427
|
+
* The actor can emit `"#stateId"` while a route map of your own holds the bare form
|
|
428
|
+
* `"stateId"`, and also the opposite. `RouteMap` makes the canonical form
|
|
429
|
+
* internally, but the constructor accepts every structural
|
|
430
|
+
* `{ getStateIdByPath, getPathByStateId }` object: a plain test object and a map of
|
|
431
|
+
* one adapter are legal, and the documentation says so. Therefore this method tries
|
|
432
|
+
* both forms: the raw form first, then the form without the `#`, then the form with
|
|
433
|
+
* the `#`.
|
|
412
434
|
*
|
|
413
|
-
* @param route -
|
|
414
|
-
* @returns The
|
|
435
|
+
* @param route - The raw value of the actor route: a stateId, with a `#` prefix or without one
|
|
436
|
+
* @returns The path of the map, or `null` or `undefined` when no form is registered
|
|
415
437
|
*/
|
|
416
438
|
lookupPathByStateId(route) {
|
|
417
439
|
const direct = this.routeMap.getPathByStateId(route);
|
|
@@ -421,66 +443,68 @@ export class RouterBridgeBase {
|
|
|
421
443
|
return this.routeMap.getPathByStateId(alternate);
|
|
422
444
|
}
|
|
423
445
|
/**
|
|
424
|
-
*
|
|
446
|
+
* Reads the query parameters of a URL search string.
|
|
425
447
|
*
|
|
426
|
-
* @param search - URL search string
|
|
427
|
-
* @returns
|
|
448
|
+
* @param search - The URL search string, for example '?tab=security&page=1'
|
|
449
|
+
* @returns The query parameters of the read, or an empty object
|
|
428
450
|
*/
|
|
429
451
|
extractQuery(search) {
|
|
430
452
|
return extractQuery(search);
|
|
431
453
|
}
|
|
432
454
|
/**
|
|
433
|
-
*
|
|
455
|
+
* Cleans a raw URL pathname of the router, and checks it.
|
|
434
456
|
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
457
|
+
* The method applies the limit of the path length, which is 2048 characters. It
|
|
458
|
+
* removes a query string and a fragment from the segment of the pathname, and it
|
|
459
|
+
* normalizes each sequence of slashes.
|
|
438
460
|
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
* MUST call this method before
|
|
442
|
-
* `syncActorFromRouter()` calls
|
|
443
|
-
*
|
|
461
|
+
* An implementation that passes around `syncActorFromRouter()`, for example with a
|
|
462
|
+
* reactive watcher of its framework that receives a route object of a parse before,
|
|
463
|
+
* MUST call this method before it gives the path to a lookup in the route map.
|
|
464
|
+
* `syncActorFromRouter()` calls the method internally. Therefore a bridge that uses
|
|
465
|
+
* that method calls `sanitizePath` never itself.
|
|
444
466
|
*
|
|
445
|
-
* @param pathname -
|
|
446
|
-
* @returns
|
|
467
|
+
* @param pathname - The raw URL pathname of the framework router.
|
|
468
|
+
* @returns The clean pathname, or `null` when the path is invalid or too long.
|
|
447
469
|
*/
|
|
448
470
|
sanitizePath(pathname) {
|
|
449
471
|
return sanitizePathname(pathname);
|
|
450
472
|
}
|
|
451
473
|
/**
|
|
452
|
-
*
|
|
474
|
+
* Returns the current pathname of the router at the moment of `connect()`.
|
|
453
475
|
*
|
|
454
|
-
*
|
|
455
|
-
* router.subscribe()
|
|
456
|
-
*
|
|
457
|
-
* current location synchronously
|
|
458
|
-
*
|
|
459
|
-
* actor to the correct state
|
|
476
|
+
* `connect()` calls this method one time, for the first synchronization from the URL
|
|
477
|
+
* to the actor. router.subscribe() fires on a *later* navigation event only. It does
|
|
478
|
+
* not repeat the location that the router loaded before. A subclass that can read the
|
|
479
|
+
* current location of its router synchronously, for example with
|
|
480
|
+
* `router.state.location.pathname`, overrides this method. A deep link and a direct
|
|
481
|
+
* URL then drive the actor to the correct state, and they leave it not at the default
|
|
482
|
+
* of its machine.
|
|
460
483
|
*
|
|
461
|
-
*
|
|
462
|
-
* - `string` → router has a current path
|
|
463
|
-
* - `null` → router is active but has no current path yet
|
|
464
|
-
* - `undefined` → adapter
|
|
484
|
+
* The meaning of each return value:
|
|
485
|
+
* - `string` → the router has a current path, and the base `connect()` writes the actor state from the router
|
|
486
|
+
* - `null` → the router is active, but it has no current path yet, and the base `connect()` writes the router from the actor
|
|
487
|
+
* - `undefined` → the adapter does the first synchronization itself, and the base `connect()` does nothing
|
|
465
488
|
*
|
|
466
|
-
* The default
|
|
467
|
-
*
|
|
489
|
+
* The default value is `undefined`. Therefore a bridge without this hook keeps its
|
|
490
|
+
* earlier behavior.
|
|
468
491
|
*/
|
|
469
492
|
getInitialRouterPath() {
|
|
470
493
|
return undefined;
|
|
471
494
|
}
|
|
472
495
|
/**
|
|
473
|
-
*
|
|
496
|
+
* Returns the current search string of the router at the moment of `connect()`.
|
|
474
497
|
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
498
|
+
* This method is the pair of `getInitialRouterPath()`. `connect()` calls it one time,
|
|
499
|
+
* to give the query string of the first URL to `syncActorFromRouter()`. It returns
|
|
500
|
+
* `undefined` when the current URL of the router has no search string, and also when
|
|
501
|
+
* the subclass overrides the method not. `syncActorFromRouter` then makes an empty
|
|
502
|
+
* `query: {}` in the event.
|
|
479
503
|
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
504
|
+
* A subclass that overrides `getInitialRouterPath()` and has a query string
|
|
505
|
+
* overrides this method too.
|
|
482
506
|
*
|
|
483
|
-
* @returns URL search string
|
|
507
|
+
* @returns The URL search string, for example `"?tab=security"`, or `undefined` when it is not available.
|
|
484
508
|
*/
|
|
485
509
|
getInitialRouterSearch() {
|
|
486
510
|
return undefined;
|