@xmachines/play-router 3.0.0 → 5.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/README.md +111 -116
- package/dist/base-route-map.d.ts +24 -13
- package/dist/base-route-map.d.ts.map +1 -1
- package/dist/base-route-map.js +116 -27
- package/dist/base-route-map.js.map +1 -1
- package/dist/build-tree.d.ts +16 -1
- package/dist/build-tree.d.ts.map +1 -1
- package/dist/build-tree.js +22 -4
- package/dist/build-tree.js.map +1 -1
- package/dist/create-route-map-from-tree.d.ts +13 -7
- package/dist/create-route-map-from-tree.d.ts.map +1 -1
- package/dist/create-route-map-from-tree.js +14 -6
- package/dist/create-route-map-from-tree.js.map +1 -1
- package/dist/errors.d.ts +2 -134
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +11 -151
- package/dist/errors.js.map +1 -1
- package/dist/find-route.d.ts +13 -1
- package/dist/find-route.d.ts.map +1 -1
- package/dist/find-route.js +33 -26
- package/dist/find-route.js.map +1 -1
- package/dist/index.d.ts +8 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -12
- package/dist/index.js.map +1 -1
- package/dist/provider-lifecycle.d.ts +18 -6
- package/dist/provider-lifecycle.d.ts.map +1 -1
- package/dist/provider-lifecycle.js +10 -5
- package/dist/provider-lifecycle.js.map +1 -1
- package/dist/router-bridge-base.d.ts +73 -38
- package/dist/router-bridge-base.d.ts.map +1 -1
- package/dist/router-bridge-base.js +304 -124
- package/dist/router-bridge-base.js.map +1 -1
- package/dist/router-sync.d.ts +3 -27
- package/dist/router-sync.d.ts.map +1 -1
- package/dist/router-sync.js +27 -45
- package/dist/router-sync.js.map +1 -1
- package/dist/types.d.ts +206 -102
- package/dist/types.d.ts.map +1 -1
- package/dist/xstate/create-route-map.d.ts +30 -0
- package/dist/xstate/create-route-map.d.ts.map +1 -0
- package/dist/{create-route-map.js → xstate/create-route-map.js} +11 -6
- package/dist/xstate/create-route-map.js.map +1 -0
- package/dist/{extract-routes.d.ts → xstate/extract-routes.d.ts} +4 -2
- package/dist/xstate/extract-routes.d.ts.map +1 -0
- package/dist/{extract-routes.js → xstate/extract-routes.js} +11 -6
- package/dist/xstate/extract-routes.js.map +1 -0
- package/dist/xstate/index.d.ts +35 -0
- package/dist/xstate/index.d.ts.map +1 -0
- package/dist/xstate/index.js +33 -0
- package/dist/xstate/index.js.map +1 -0
- package/dist/{machine-to-graph.d.ts → xstate/machine-to-graph.d.ts} +1 -1
- package/dist/xstate/machine-to-graph.d.ts.map +1 -0
- package/dist/{machine-to-graph.js → xstate/machine-to-graph.js} +2 -2
- package/dist/xstate/machine-to-graph.js.map +1 -0
- package/dist/xstate/machine-types.d.ts +42 -0
- package/dist/xstate/machine-types.d.ts.map +1 -0
- package/dist/xstate/machine-types.js +2 -0
- package/dist/xstate/machine-types.js.map +1 -0
- package/dist/{query.d.ts → xstate/query.d.ts} +6 -4
- package/dist/xstate/query.d.ts.map +1 -0
- package/dist/{query.js → xstate/query.js} +3 -2
- package/dist/xstate/query.js.map +1 -0
- package/package.json +30 -21
- package/dist/base-path.d.ts +0 -209
- package/dist/base-path.d.ts.map +0 -1
- package/dist/base-path.js +0 -418
- package/dist/base-path.js.map +0 -1
- package/dist/create-route-map.d.ts +0 -44
- package/dist/create-route-map.d.ts.map +0 -1
- package/dist/create-route-map.js.map +0 -1
- package/dist/extract-routes.d.ts.map +0 -1
- package/dist/extract-routes.js.map +0 -1
- package/dist/framework-params.d.ts +0 -144
- package/dist/framework-params.d.ts.map +0 -1
- package/dist/framework-params.js +0 -291
- package/dist/framework-params.js.map +0 -1
- package/dist/machine-to-graph.d.ts.map +0 -1
- package/dist/machine-to-graph.js.map +0 -1
- package/dist/query.d.ts.map +0 -1
- package/dist/query.js.map +0 -1
- package/dist/url-pattern-utils.d.ts +0 -93
- package/dist/url-pattern-utils.d.ts.map +0 -1
- package/dist/url-pattern-utils.js +0 -233
- package/dist/url-pattern-utils.js.map +0 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The class holds the logic that each router bridge shares, and that is 90% of the
|
|
5
5
|
* code of every bridge:
|
|
6
|
-
* - The
|
|
6
|
+
* - The atom watcher of the direction from the actor to the router
|
|
7
7
|
* - `lastSyncedPath`, for the echo suppression in the direction from the actor to the router
|
|
8
8
|
* - The `isProcessingNavigation` flag, which stops a loop of a guard redirect in `syncActorFromRouter` only
|
|
9
9
|
* - `syncRouterFromActor` and `syncActorFromRouter`, with the read of each URL parameter
|
|
@@ -42,11 +42,10 @@
|
|
|
42
42
|
*
|
|
43
43
|
* @see [Play RFC](../../docs/rfc/play.md) - invariant INV-04
|
|
44
44
|
*/
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
49
|
-
import { NO_BASE_PATH, firstMarkIndex, joinBasePath, resolveBasePath, stripBasePath, } from "./base-path.js";
|
|
45
|
+
import { NO_BASE_PATH, sanitizePathname, splitLocation, holdsUnsubstitutedParam, joinBasePath, parsePattern, resolveBasePath, stripBasePath, } from "@xmachines/play-url";
|
|
46
|
+
import { watchAtom } from "@xmachines/play-atom";
|
|
47
|
+
import { DuplicateBridgeError, RouterSyncError, InvalidRoutePatternError } from "./errors.js";
|
|
48
|
+
import { buildPlayRouteEvent, extractQuery, extractRouteParams, matchRouteMap, } from "./router-sync.js";
|
|
50
49
|
/**
|
|
51
50
|
* The registry of the actors with an active bridge connection, at the module level.
|
|
52
51
|
*
|
|
@@ -85,19 +84,19 @@ export class RouterBridgeBase {
|
|
|
85
84
|
/**
|
|
86
85
|
* The raw value of `actor.currentRoute` that this bridge accounted for already.
|
|
87
86
|
*
|
|
88
|
-
* The field answers ONE question: "did the bridge see this value of the
|
|
87
|
+
* The field answers ONE question: "did the bridge see this value of the atom
|
|
89
88
|
* before?". It is therefore a string, and not a location — a route that resolves to
|
|
90
89
|
* no URL, such as an unknown stateId or a parameterized pattern, still stops a
|
|
91
90
|
* second attempt at the same value.
|
|
92
91
|
*
|
|
93
92
|
* The constructor and `connect()` seed it, so that the first fire of the watcher of
|
|
94
|
-
* the
|
|
93
|
+
* the atom pushes nothing for a route that moved nowhere.
|
|
95
94
|
*/
|
|
96
95
|
lastActorRoute = null;
|
|
97
96
|
/**
|
|
98
97
|
* The flag guards `syncActorFromRouter` against a re-entrant call from a guard
|
|
99
98
|
* redirect of the actor itself. Such a call has this sequence: the bridge sends to
|
|
100
|
-
* the actor, the
|
|
99
|
+
* the actor, the atom fires, the bridge pushes to the router, and a second
|
|
101
100
|
* `syncActorFromRouter` call starts before the first one returns.
|
|
102
101
|
*
|
|
103
102
|
* The flag is NOT the echo suppression of the direction from the actor to the
|
|
@@ -169,6 +168,13 @@ export class RouterBridgeBase {
|
|
|
169
168
|
* the echo suppression therefore keeps comparing like with like.
|
|
170
169
|
*/
|
|
171
170
|
mount = NO_BASE_PATH;
|
|
171
|
+
/**
|
|
172
|
+
* The caches that every parse and every compilation of this bridge reads.
|
|
173
|
+
*
|
|
174
|
+
* `resolveNavigationPath` and `isActorAtPath` parse a CONCRETE location, and an
|
|
175
|
+
* application writes one for each value that it puts in a param.
|
|
176
|
+
*/
|
|
177
|
+
compileOptions;
|
|
172
178
|
/**
|
|
173
179
|
* @param actor - A `RoutableActor`, with `currentRoute`, `initialRoute`, and `send`.
|
|
174
180
|
* @param routeMap - The route map of both directions, for the resolution between a
|
|
@@ -181,11 +187,19 @@ export class RouterBridgeBase {
|
|
|
181
187
|
constructor(actor, routeMap, options) {
|
|
182
188
|
this.actor = actor;
|
|
183
189
|
this.routeMap = routeMap;
|
|
190
|
+
// The caches of this bridge, or the shared defaults. `resolveNavigationPath` and
|
|
191
|
+
// `isActorAtPath` parse a CONCRETE location — one for each value that the
|
|
192
|
+
// application puts in a param — so a server that holds a bridge for each request
|
|
193
|
+
// passes caches that the request owns and drops.
|
|
194
|
+
this.compileOptions = {
|
|
195
|
+
patternCache: options?.patternCache,
|
|
196
|
+
compiledCache: options?.compiledCache,
|
|
197
|
+
};
|
|
184
198
|
// Resolve the mount BEFORE the seed of lastSyncedPath: an invalid basePath must
|
|
185
199
|
// fail here, at the construction, and not on the first navigation.
|
|
186
200
|
this.mount = resolveBasePath(options?.basePath, options?.basePathParams);
|
|
187
201
|
// Record the route of the actor as accounted for. This stops a loop in the first
|
|
188
|
-
// synchronization: the watcher of the
|
|
202
|
+
// synchronization: the watcher of the atom fires once when `connect()` arms it,
|
|
189
203
|
// and a route that moved nowhere must push nothing.
|
|
190
204
|
//
|
|
191
205
|
// The seed goes to `lastActorRoute`, and NOT to `lastSyncedPath`. The route of the
|
|
@@ -420,7 +434,7 @@ export class RouterBridgeBase {
|
|
|
420
434
|
/**
|
|
421
435
|
* Connects the router bridge to the Actor.
|
|
422
436
|
*
|
|
423
|
-
* The method installs the
|
|
437
|
+
* The method installs the atom watcher of the direction from the actor to the
|
|
424
438
|
* router. It then starts the watch of the router changes, which each framework does
|
|
425
439
|
* in its own way.
|
|
426
440
|
*
|
|
@@ -467,14 +481,14 @@ export class RouterBridgeBase {
|
|
|
467
481
|
this.hasConnectedOnce = true;
|
|
468
482
|
// From here on, connect() gives all or nothing. THREE steps below can throw, and
|
|
469
483
|
// the rollback has to cover each of them:
|
|
470
|
-
// - `createRouteWatcher`, on
|
|
484
|
+
// - `createRouteWatcher`, on an atom that refuses a watcher
|
|
471
485
|
// - `watchRouterChanges()`, which belongs to the adapter: `afterNavigate()` of
|
|
472
486
|
// SvelteKit raises `lifecycle_outside_component` outside the initialization of
|
|
473
487
|
// a component, and a host reaches that by calling `connectRouter` from a module
|
|
474
488
|
// scope or a store
|
|
475
|
-
// - `performInitialSync()`, with
|
|
476
|
-
//
|
|
477
|
-
//
|
|
489
|
+
// - `performInitialSync()`, with an InvalidRoutePatternError for a route that
|
|
490
|
+
// URLPattern refuses, and a RouterSyncError for each other failure of the send
|
|
491
|
+
// to the actor
|
|
478
492
|
//
|
|
479
493
|
// The throw needs an action of the caller, and it therefore goes to the caller
|
|
480
494
|
// without a change.
|
|
@@ -483,12 +497,13 @@ export class RouterBridgeBase {
|
|
|
483
497
|
// cleanup. Every part above must go away again, or it leaks:
|
|
484
498
|
// - the router listener keeps firing, and it drives the actor through a bridge
|
|
485
499
|
// that the caller believes to be dead
|
|
486
|
-
// - the
|
|
500
|
+
// - the atom watcher keeps pushing each actor route to the router
|
|
487
501
|
// - the activeBridges entry keeps the actor, so the NEXT connect() call of the
|
|
488
502
|
// caller for this actor throws a DuplicateBridgeError. A repair of the real
|
|
489
|
-
// cause, for example a
|
|
503
|
+
// cause, for example a correction of the route pattern, rescues that actor
|
|
504
|
+
// never.
|
|
490
505
|
try {
|
|
491
|
-
// The
|
|
506
|
+
// The atom watcher of the direction from the actor to the router.
|
|
492
507
|
this.routeWatcher = createRouteWatcher(this.actor.currentRoute, (route) => {
|
|
493
508
|
this.syncRouterFromActor(route);
|
|
494
509
|
});
|
|
@@ -501,7 +516,8 @@ export class RouterBridgeBase {
|
|
|
501
516
|
// guard of its own, because `unwatchRouterChanges()` belongs to the adapter and
|
|
502
517
|
// it can throw: a Vue scope that stops, a Solid disposer, a SvelteKit teardown.
|
|
503
518
|
// That error must NOT replace the one the rollback exists to surface — a caller
|
|
504
|
-
// that receives it
|
|
519
|
+
// that receives it learns neither which route pattern URLPattern refused nor
|
|
520
|
+
// which event the actor refused.
|
|
505
521
|
try {
|
|
506
522
|
this.disconnect();
|
|
507
523
|
}
|
|
@@ -541,6 +557,18 @@ export class RouterBridgeBase {
|
|
|
541
557
|
*/
|
|
542
558
|
performInitialSync(initialRouterPath) {
|
|
543
559
|
const initialActorRoute = this.actor.currentRoute.get();
|
|
560
|
+
// ONE read of the query of the router, and a LAZY one. The read belongs to the
|
|
561
|
+
// adapter and it can walk the state of a router, so a second call can answer
|
|
562
|
+
// differently — and most branches below reach no query at all.
|
|
563
|
+
let search;
|
|
564
|
+
let searchRead = false;
|
|
565
|
+
const routerSearch = () => {
|
|
566
|
+
if (!searchRead) {
|
|
567
|
+
search = this.getInitialRouterSearch();
|
|
568
|
+
searchRead = true;
|
|
569
|
+
}
|
|
570
|
+
return search;
|
|
571
|
+
};
|
|
544
572
|
if (typeof initialRouterPath === "string") {
|
|
545
573
|
// `sanitizePathname` refuses a path of more than 2048 characters, and the RAW
|
|
546
574
|
// value is no substitute for the clean one: it still carries a query, a
|
|
@@ -575,8 +603,15 @@ export class RouterBridgeBase {
|
|
|
575
603
|
// The decision below IS the first synchronization, and it runs against a location
|
|
576
604
|
// under the mount, so nothing is owed any more.
|
|
577
605
|
this.hasSynchronizedUnderMount = true;
|
|
578
|
-
// Test if the actor is at the current location of the router already.
|
|
579
|
-
|
|
606
|
+
// Test if the actor is at the current location of the router already. The PATH
|
|
607
|
+
// and the QUERY are two answers of ONE lookup: the path decides whether a restore
|
|
608
|
+
// is possible at all, and the query decides whether the two sides are in step
|
|
609
|
+
// there. `routerSearch` runs behind the `&&`, so a branch that answers on the
|
|
610
|
+
// path alone reads no query.
|
|
611
|
+
const actorAtRouterPath = this.isActorAtPath(machinePath, initialActorRoute);
|
|
612
|
+
const actorAlreadyAtRouterLocation = actorAtRouterPath &&
|
|
613
|
+
initialActorRoute != null &&
|
|
614
|
+
sameQuery(initialActorRoute, routerSearch());
|
|
580
615
|
// The two are in step already, so nothing is synchronized — but the location has
|
|
581
616
|
// to be RECORDED, or the echo suppression starts disarmed. `setBasePath` clears
|
|
582
617
|
// `lastSyncedPath` on a move of the prefix, and this branch left it null: the
|
|
@@ -590,7 +625,7 @@ export class RouterBridgeBase {
|
|
|
590
625
|
// of one connection, and the record of the old prefix says nothing about this
|
|
591
626
|
// location.
|
|
592
627
|
if (actorAlreadyAtRouterLocation) {
|
|
593
|
-
this.lastSyncedPath = withSearch(machinePath,
|
|
628
|
+
this.lastSyncedPath = withSearch(machinePath, routerSearch());
|
|
594
629
|
}
|
|
595
630
|
else {
|
|
596
631
|
// The path of the router is different from the actor route. Is this a deep link, or
|
|
@@ -606,16 +641,33 @@ export class RouterBridgeBase {
|
|
|
606
641
|
//
|
|
607
642
|
// The detection: the URL of the router is the initial route of the machine, AND the
|
|
608
643
|
// actor is at a different route. This is then a restore.
|
|
644
|
+
//
|
|
645
|
+
// The branch reads the LOCATION test above, through the `else` that carries it,
|
|
646
|
+
// and not the path test alone: a fragment that the actor carries and the router
|
|
647
|
+
// reports never — `sanitizePathname` cuts it off — is a difference that only
|
|
648
|
+
// this push can place in the address bar. A query of the ACTOR is such a
|
|
649
|
+
// difference too, and issue #18 carries the decision about it.
|
|
650
|
+
//
|
|
651
|
+
// `carriesNoOwnSuffix` is what keeps the two apart. An actor that stands on the
|
|
652
|
+
// PATH of the router and carries no query and no fragment of its own restores
|
|
653
|
+
// NOTHING: the address bar already shows its place, and a push only rewrites the
|
|
654
|
+
// location that the user opened. The raw comparison against `initialRoute` cannot
|
|
655
|
+
// see that, because a stateId is never string-equal to a path — an actor at
|
|
656
|
+
// `"#home"` against a router at `"/?ref=nav"` read as a move, so the bridge
|
|
657
|
+
// pushed `"/"` over the query the user opened and sent no `play.route` that
|
|
658
|
+
// carried it. The router keeps the priority for such a location, which is the
|
|
659
|
+
// answer that the same bridge gives for the actor route `"/"`.
|
|
609
660
|
if (initialActorRoute &&
|
|
610
661
|
machinePath === this.actor.initialRoute &&
|
|
611
|
-
initialActorRoute !== this.actor.initialRoute
|
|
662
|
+
initialActorRoute !== this.actor.initialRoute &&
|
|
663
|
+
!(actorAtRouterPath && carriesNoOwnSuffix(initialActorRoute))) {
|
|
612
664
|
this.pushHoldingNavigation(initialActorRoute);
|
|
613
665
|
}
|
|
614
666
|
else {
|
|
615
|
-
// The search is read
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
this.syncActorFromRouter(initialRouterPath,
|
|
667
|
+
// The search is read through `routerSearch`, which reads it one time and only
|
|
668
|
+
// when a branch needs it: the read belongs to the adapter, and it can walk
|
|
669
|
+
// the state of a router.
|
|
670
|
+
this.syncActorFromRouter(initialRouterPath, routerSearch());
|
|
619
671
|
}
|
|
620
672
|
}
|
|
621
673
|
}
|
|
@@ -641,7 +693,7 @@ export class RouterBridgeBase {
|
|
|
641
693
|
/**
|
|
642
694
|
* Disconnects the router bridge from the Actor.
|
|
643
695
|
*
|
|
644
|
-
* The method stops the watch of the
|
|
696
|
+
* The method stops the watch of the atom, and it removes the router listener of the
|
|
645
697
|
* framework.
|
|
646
698
|
*/
|
|
647
699
|
disconnect() {
|
|
@@ -692,7 +744,7 @@ export class RouterBridgeBase {
|
|
|
692
744
|
}
|
|
693
745
|
// ── The sync methods. They are protected, and a subclass overrides one for its own behavior ──
|
|
694
746
|
/**
|
|
695
|
-
* Writes the location of the router when the route
|
|
747
|
+
* Writes the location of the router when the route atom of the actor changes.
|
|
696
748
|
*
|
|
697
749
|
* The method resolves the actor route to a concrete URL path, then it calls
|
|
698
750
|
* navigateRouter() for the navigation of the framework. When it cannot resolve the
|
|
@@ -791,7 +843,7 @@ export class RouterBridgeBase {
|
|
|
791
843
|
const owesFirstSync = !this.hasSynchronizedUnderMount;
|
|
792
844
|
this.hasSynchronizedUnderMount = true;
|
|
793
845
|
const restoredRoute = this.actor.currentRoute.get();
|
|
794
|
-
// `
|
|
846
|
+
// `isActorAtPath` is the test that `performInitialSync` makes BEFORE it looks
|
|
795
847
|
// for a restore, and both branches of the owed synchronization below need it. The
|
|
796
848
|
// actor route can be a stateId, and a stateId is never string-equal to
|
|
797
849
|
// `initialRoute`, which is a path: an actor that sits at "#home" of the mount point
|
|
@@ -801,13 +853,23 @@ export class RouterBridgeBase {
|
|
|
801
853
|
//
|
|
802
854
|
// ONE call, because `getStateIdByPath` and `getPathByStateId` belong to the route
|
|
803
855
|
// map: a map of a consumer can answer differently on a second call, and the two
|
|
804
|
-
// branches would then disagree about the same location.
|
|
805
|
-
|
|
856
|
+
// branches would then disagree about the same location. The PATH and the QUERY are
|
|
857
|
+
// two answers of that one lookup, exactly as `performInitialSync` reads them.
|
|
858
|
+
const actorAtPath = owesFirstSync && this.isActorAtPath(machinePath, restoredRoute);
|
|
859
|
+
const actorAtLocation = actorAtPath && restoredRoute != null && sameQuery(restoredRoute, search);
|
|
860
|
+
// The RESTORE reads the LOCATION test, exactly as `performInitialSync` does, and it
|
|
861
|
+
// refuses the same empty restore: an actor that stands on the PATH of the location
|
|
862
|
+
// and carries no query and no fragment of its own has nothing to put in the address
|
|
863
|
+
// bar. Without that test an actor at `"#home"` met a host that came back to the
|
|
864
|
+
// mount point with a query, and the bridge pushed the mount point over it and
|
|
865
|
+
// returned `true` — so no `play.route` carried that location to the machine either,
|
|
866
|
+
// which is the fault that the comment above this block names.
|
|
806
867
|
if (owesFirstSync &&
|
|
807
868
|
restoredRoute &&
|
|
808
869
|
machinePath === this.actor.initialRoute &&
|
|
809
870
|
restoredRoute !== this.actor.initialRoute &&
|
|
810
|
-
!actorAtLocation
|
|
871
|
+
!actorAtLocation &&
|
|
872
|
+
!(actorAtPath && carriesNoOwnSuffix(restoredRoute))) {
|
|
811
873
|
return this.pushUnderMount(restoredRoute);
|
|
812
874
|
}
|
|
813
875
|
if (owesFirstSync) {
|
|
@@ -879,12 +941,12 @@ export class RouterBridgeBase {
|
|
|
879
941
|
if (this.isProcessingNavigation)
|
|
880
942
|
return true;
|
|
881
943
|
// The route has to resolve to a concrete path BEFORE the method reports that it
|
|
882
|
-
// handled the location. `pushResolvedRoute`
|
|
883
|
-
// and for a parameterized pattern, and it
|
|
884
|
-
//
|
|
885
|
-
//
|
|
886
|
-
//
|
|
887
|
-
// recognized
|
|
944
|
+
// handled the location. `pushResolvedRoute` navigates nowhere for an unknown stateId
|
|
945
|
+
// and for a parameterized pattern, and it records the RAW value in `lastActorRoute`
|
|
946
|
+
// alone: a `true` return then left the location in neither side — the URL kept the
|
|
947
|
+
// page of the host, and the actor heard no `play.route`. The record of the location
|
|
948
|
+
// that the address bar holds says nothing about it either, so no later return could
|
|
949
|
+
// be recognized. `reconcileLocationWithMount` guards the same case.
|
|
888
950
|
//
|
|
889
951
|
// The resolution travels with the route, so `resolveNavigationPath` runs one time:
|
|
890
952
|
// the method is `protected`, a subclass can override it, and a second call can
|
|
@@ -920,7 +982,7 @@ export class RouterBridgeBase {
|
|
|
920
982
|
* reports, and `sanitizePathname` has removed the fragment from that already.
|
|
921
983
|
*/
|
|
922
984
|
testEchoOfLastPush(route) {
|
|
923
|
-
// The FIRST question: did the bridge account for this value of the
|
|
985
|
+
// The FIRST question: did the bridge account for this value of the atom already?
|
|
924
986
|
// The test is an exact string compare, because it asks about the VALUE. A route
|
|
925
987
|
// that resolves to no URL — an unknown stateId, a parameterized pattern — is
|
|
926
988
|
// therefore covered too, and the bridge tries it one time only.
|
|
@@ -937,7 +999,7 @@ export class RouterBridgeBase {
|
|
|
937
999
|
if (sameLocation(route, location))
|
|
938
1000
|
return { isEcho: true };
|
|
939
1001
|
// A route that is a stateId answers it only after the resolution. The machine that
|
|
940
|
-
// mirrors an inbound `play.route` back into its route
|
|
1002
|
+
// mirrors an inbound `play.route` back into its route atom reaches this line:
|
|
941
1003
|
// the bridge wrote "/about" and the actor answers "#about", which is ONE location.
|
|
942
1004
|
// A compare of the raw value alone missed, and the bridge pushed a location that
|
|
943
1005
|
// the router already held — one history entry that BACK cannot escape.
|
|
@@ -963,8 +1025,9 @@ export class RouterBridgeBase {
|
|
|
963
1025
|
* pattern, for a wildcard pattern, and for an unknown id, the method skips the push
|
|
964
1026
|
* completely: a push of the raw value writes a stateId, a `:param` pattern, or a
|
|
965
1027
|
* literal `*` into the browser URL. The method still writes the raw route to
|
|
966
|
-
*
|
|
967
|
-
*
|
|
1028
|
+
* lastActorRoute, and it leaves lastSyncedPath as it stands, because that field
|
|
1029
|
+
* records a LOCATION and no location changed. Therefore the dedup guard fires
|
|
1030
|
+
* correctly on the next identical value of the atom.
|
|
968
1031
|
*
|
|
969
1032
|
* @param route - The raw value of the actor route.
|
|
970
1033
|
* @param resolved - The concrete path of that route. The default resolves it here. A
|
|
@@ -973,7 +1036,7 @@ export class RouterBridgeBase {
|
|
|
973
1036
|
*/
|
|
974
1037
|
pushResolvedRoute(route, resolved = this.resolveNavigationPath(route)) {
|
|
975
1038
|
// The VALUE is accounted for either way, so the dedup stops the next identical fire
|
|
976
|
-
// of the
|
|
1039
|
+
// of the atom even when the route resolves to no URL.
|
|
977
1040
|
this.lastActorRoute = route;
|
|
978
1041
|
// The LOCATION record takes the concrete path only. A raw stateId here described no
|
|
979
1042
|
// location that a router ever showed, and `isEchoOfLastSync` compares this field
|
|
@@ -1071,7 +1134,7 @@ export class RouterBridgeBase {
|
|
|
1071
1134
|
}
|
|
1072
1135
|
// Record the location BEFORE the send, and record it with its query.
|
|
1073
1136
|
//
|
|
1074
|
-
// The route
|
|
1137
|
+
// The route atom of the actor notifies the watcher of the other direction
|
|
1075
1138
|
// for this same move, and `syncRouterFromActor` then tests the route of the
|
|
1076
1139
|
// actor against this field. Two details decide that test:
|
|
1077
1140
|
//
|
|
@@ -1086,7 +1149,7 @@ export class RouterBridgeBase {
|
|
|
1086
1149
|
this.lastSyncedPath = withSearch(nextRoute.pathname, search);
|
|
1087
1150
|
// The router moved, and the actor has not answered yet, so the value that the
|
|
1088
1151
|
// bridge accounted for describes the agreement of the two sides NO MORE. A value
|
|
1089
|
-
// that stayed read the answer of the actor as a repeat of the
|
|
1152
|
+
// that stayed read the answer of the actor as a repeat of the atom: a guard
|
|
1090
1153
|
// redirect BACK to the route the actor came from — the route that the seed of
|
|
1091
1154
|
// `connect()` recorded — stopped at the dedup, the bridge pushed nothing, and the
|
|
1092
1155
|
// address bar kept the location that the guard refused.
|
|
@@ -1094,10 +1157,11 @@ export class RouterBridgeBase {
|
|
|
1094
1157
|
this.actor.send(nextRoute.event);
|
|
1095
1158
|
}
|
|
1096
1159
|
catch (error) {
|
|
1097
|
-
//
|
|
1098
|
-
//
|
|
1099
|
-
//
|
|
1100
|
-
|
|
1160
|
+
// An InvalidRoutePatternError needs an action of the DEVELOPER: a route of the
|
|
1161
|
+
// machine declares a pattern that URLPattern refuses, and no location can fix it.
|
|
1162
|
+
// It goes to the caller without a change, so that the caller can find it and read
|
|
1163
|
+
// the pattern that it names.
|
|
1164
|
+
if (error instanceof InvalidRoutePatternError) {
|
|
1101
1165
|
throw error;
|
|
1102
1166
|
}
|
|
1103
1167
|
throw new RouterSyncError("Failed to sync actor state from router location.", {
|
|
@@ -1112,22 +1176,20 @@ export class RouterBridgeBase {
|
|
|
1112
1176
|
/**
|
|
1113
1177
|
* Reads the path parameters of a URL, with the URLPattern API.
|
|
1114
1178
|
*
|
|
1115
|
-
* The
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
1118
|
-
* must therefore give a polyfill in an environment without the native URLPattern:
|
|
1119
|
-
* Node.js < 24, and an older browser.
|
|
1179
|
+
* The API is always present: `@xmachines/play-router` uses the native URLPattern of the
|
|
1180
|
+
* runtime, and it falls back to `urlpattern-polyfill`, which it carries as an ordinary
|
|
1181
|
+
* dependency. A consumer therefore loads nothing, whatever the runtime.
|
|
1120
1182
|
*
|
|
1121
1183
|
* @param pathname - The real URL path, for example '/profile/john'
|
|
1122
1184
|
* @param stateId - The stateId of the match, for the lookup of the route pattern
|
|
1123
1185
|
* @returns The path parameters of the read, or an empty object when nothing matches
|
|
1124
|
-
* @throws {
|
|
1186
|
+
* @throws {InvalidRoutePatternError} When URLPattern cannot compile the route pattern
|
|
1125
1187
|
*/
|
|
1126
1188
|
extractParams(pathname, stateId) {
|
|
1127
1189
|
const pattern = this.routeMap.getPathByStateId(stateId);
|
|
1128
1190
|
if (!pattern)
|
|
1129
1191
|
return {};
|
|
1130
|
-
return extractRouteParams(pathname, pattern);
|
|
1192
|
+
return extractRouteParams(pathname, pattern, this.compileOptions);
|
|
1131
1193
|
}
|
|
1132
1194
|
/**
|
|
1133
1195
|
* Resolves a value of an actor route to a concrete URL path for a navigation.
|
|
@@ -1140,6 +1202,9 @@ export class RouterBridgeBase {
|
|
|
1140
1202
|
* `/docs/*`, because no concrete value is available for a real URL
|
|
1141
1203
|
* - a string that is no path and no known stateId
|
|
1142
1204
|
*
|
|
1205
|
+
* A LITERAL route of the map comes back with each escape resolved: `/tags/c\+\+` gives
|
|
1206
|
+
* the path `/tags/c++`, which is the path that the route matches.
|
|
1207
|
+
*
|
|
1143
1208
|
* A lookup of a stateId tries the form `"#stateId"` and the bare form `"stateId"`,
|
|
1144
1209
|
* through `lookupPathByStateId`. Therefore a route map of your own that holds one
|
|
1145
1210
|
* form only still resolves the path.
|
|
@@ -1157,19 +1222,70 @@ export class RouterBridgeBase {
|
|
|
1157
1222
|
const path = mapped ?? (route.startsWith("/") ? route : null);
|
|
1158
1223
|
if (path === null)
|
|
1159
1224
|
return null;
|
|
1160
|
-
//
|
|
1161
|
-
//
|
|
1162
|
-
//
|
|
1163
|
-
//
|
|
1164
|
-
//
|
|
1165
|
-
//
|
|
1166
|
-
//
|
|
1167
|
-
|
|
1225
|
+
// A LITERAL route resolves to the one path that it matches, and the source of a
|
|
1226
|
+
// pattern is not always that path: `/tags/c\+\+` matches `/tags/c++`, and
|
|
1227
|
+
// `/i18n/{en}` matches `/i18n/en`. A bridge that pushed the source wrote the
|
|
1228
|
+
// backslashes and the braces into the address bar, where `getStateIdByPath` answered
|
|
1229
|
+
// that location never — so the URL stopped following the actor, and its next move to
|
|
1230
|
+
// the same route was suppressed as an echo of that push.
|
|
1231
|
+
//
|
|
1232
|
+
// The two branches above resolve DIFFERENT amounts, and the difference is what the
|
|
1233
|
+
// string is:
|
|
1234
|
+
//
|
|
1235
|
+
// - A route of the MAP is a pattern source, so every construct in it is grammar and
|
|
1236
|
+
// the parse resolves all of it.
|
|
1237
|
+
// - A route that the ACTOR built is a path already. `buildRouteUrl` of
|
|
1238
|
+
// `@xmachines/play-xstate` reads the whole grammar and resolves every escape, so
|
|
1239
|
+
// a route that IT built carries no backslash. An actor of another kind can still
|
|
1240
|
+
// write one, and a BACKSLASH is the one construct that means grammar in such a
|
|
1241
|
+
// path — and nothing else does. A brace in such a path is a real brace, and
|
|
1242
|
+
// `/i18n/{en}` from an actor is the location `/i18n/{en}`.
|
|
1243
|
+
//
|
|
1244
|
+
// A route of the map is a pattern source WHOLE, and its `?` is a modifier and never
|
|
1245
|
+
// a query. The parse therefore reads all of it, and its answer is complete: a `null`
|
|
1246
|
+
// literal path says that the pattern describes a SET of paths, so the bridge pushes
|
|
1247
|
+
// nothing. A fall to the concrete-path test below answered that question with the
|
|
1248
|
+
// characters of the string instead, and it let every pattern whose grammar carries
|
|
1249
|
+
// no ":" and no "*" through — `/docs/rfc(2119)`, `/books{/v2}?` and `/files/(\d+)`
|
|
1250
|
+
// all reached the address bar as they stand, where `getStateIdByPath` answers them
|
|
1251
|
+
// never.
|
|
1252
|
+
if (mapped != null)
|
|
1253
|
+
return parsePattern(path, this.compileOptions).literalPath;
|
|
1254
|
+
// A route of the ACTOR carries its query, because `context.query` travels in
|
|
1255
|
+
// `currentRoute`, and the PATHNAME half alone reaches the grammar for it. A query
|
|
1256
|
+
// holds a ":" and a "*" legally — a time in "?at=10:30", a URL in "?next=https://x",
|
|
1257
|
+
// a glob in "?q=a*" — and it holds the "?" that the grammar reads as a modifier. A
|
|
1258
|
+
// parse of the whole string called such a route malformed and resolved nothing of
|
|
1259
|
+
// it, so `/tags/c\+\+?ref=nav` went to the address bar with both backslashes; a
|
|
1260
|
+
// PATTERN test of the whole string called it a pattern, so `pushResolvedRoute` wrote
|
|
1261
|
+
// `lastSyncedPath` and navigated nowhere, and the dedup guard then suppressed the
|
|
1262
|
+
// next attempt at the same route as an echo. The suffix carries no grammar, and it
|
|
1263
|
+
// travels to the router unchanged.
|
|
1264
|
+
const { pathname, suffix } = splitLocation(path);
|
|
1265
|
+
// A BACKSLASH is the one construct that reaches an actor route unresolved, and this
|
|
1266
|
+
// is where it resolves.
|
|
1267
|
+
if (pathname.includes("\\")) {
|
|
1268
|
+
const literal = parsePattern(pathname, this.compileOptions).literalPath;
|
|
1269
|
+
// The resolution FAILED, so this route resolves to no concrete path and the
|
|
1270
|
+
// bridge pushes nothing. A fall to the test below refused none of it — a backslash
|
|
1271
|
+
// is neither a ":" nor a "*" — so the SOURCE went to the address bar with its
|
|
1272
|
+
// backslashes, where `getStateIdByPath` answers that location never, and
|
|
1273
|
+
// `pushResolvedRoute` then recorded it and suppressed the next attempt at the same
|
|
1274
|
+
// route as an echo. `encodeURIComponent` leaves "(", ")", "!", "'" and "~" as they
|
|
1275
|
+
// stand, so a param value of `f(x)` under a template that carries an escape —
|
|
1276
|
+
// `/files/c\+\+/:name` — reaches this line and derives no literal path.
|
|
1277
|
+
return literal === null ? null : `${literal}${suffix}`;
|
|
1278
|
+
}
|
|
1279
|
+
// The test is `holdsUnsubstitutedParam` and NOT `isParameterizedPattern`, because
|
|
1280
|
+
// this string is a concrete path and not a pattern. Its own documentation gives the
|
|
1281
|
+
// reason: a real param value carries a character of the grammar often enough, and
|
|
1282
|
+
// the grammar reads a modifier in "/tags/c++" and a group in "/i18n/{en}".
|
|
1283
|
+
if (holdsUnsubstitutedParam(pathname))
|
|
1168
1284
|
return null;
|
|
1169
1285
|
return path;
|
|
1170
1286
|
}
|
|
1171
1287
|
/**
|
|
1172
|
-
* Tells you whether the actor already stands
|
|
1288
|
+
* Tells you whether the actor already stands on the PATH of a location of the router.
|
|
1173
1289
|
*
|
|
1174
1290
|
* The `currentRoute` of the actor can be a stateId, for example `"#app.home"`, while a
|
|
1175
1291
|
* location of the router is a URL path, for example `"/home"`. Therefore the method
|
|
@@ -1178,33 +1294,69 @@ export class RouterBridgeBase {
|
|
|
1178
1294
|
* `takeReturnUnderMount`, it reads a location that the HOST chose as a restore of the
|
|
1179
1295
|
* actor and writes over it.
|
|
1180
1296
|
*
|
|
1181
|
-
* The actor is
|
|
1297
|
+
* The actor is on the path in three cases: the stateId of the match IS the actor
|
|
1182
1298
|
* route, the path of the match IS the actor route, or the actor route is a stateId
|
|
1183
1299
|
* that resolves to the same registered path as the stateId of the match. Each lookup
|
|
1184
1300
|
* goes through {@link lookupPathByStateId}, which tries the form `"#stateId"` and also
|
|
1185
1301
|
* the bare form `"stateId"`. Therefore a route map with `"#about"` recognizes an actor
|
|
1186
1302
|
* route of `"about"`, and the opposite also works, and this is correct for a
|
|
1187
|
-
* structural map of your own that holds one form.
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
1194
|
-
*
|
|
1195
|
-
*
|
|
1196
|
-
*
|
|
1303
|
+
* structural map of your own that holds one form.
|
|
1304
|
+
*
|
|
1305
|
+
* An actor route with a concrete path, which starts with `"/"`, goes to a direct
|
|
1306
|
+
* comparison of the pathname against `machinePath`. That comparison runs BEFORE the
|
|
1307
|
+
* map, because a location the map does not hold is still a location the actor can
|
|
1308
|
+
* stand on.
|
|
1309
|
+
*
|
|
1310
|
+
* The QUERY is a question of its own, and {@link sameQuery} answers it. Each caller
|
|
1311
|
+
* asks BOTH, and the two answers do different work. "The two sides are in step" needs
|
|
1312
|
+
* the path AND the query, so a router that carries the only query is a deep link that
|
|
1313
|
+
* the actor has to follow — for a stateId route as much as for a path route. The
|
|
1314
|
+
* RESTORE needs the path, beside `carriesNoOwnSuffix`: an actor that stands here and
|
|
1315
|
+
* carries no query and no fragment of its own has nothing to put in the address bar,
|
|
1316
|
+
* and a push would only rewrite the location the user opened.
|
|
1317
|
+
*
|
|
1318
|
+
* A query that the ACTOR carries is a difference that this branch still reads as a
|
|
1319
|
+
* restore, so an actor at `/?ref=nav` wins against a router at `/?other=1`. `main`
|
|
1320
|
+
* answers the same way, and whether that is a deep link or a restore is a decision
|
|
1321
|
+
* about the rule: issue #18 carries it, and a test pins the behaviour.
|
|
1322
|
+
*
|
|
1323
|
+
* Each lookup runs ONE time, and a caller that needs both answers calls this method
|
|
1324
|
+
* one time too. `getPathByStateId` belongs to the route map: a map of a consumer can
|
|
1325
|
+
* answer differently on a second call, and `RouteMap` pays a cache lookup for each of
|
|
1326
|
+
* them.
|
|
1327
|
+
*
|
|
1328
|
+
* @param machinePath - The machine half of the location of the router, with no query.
|
|
1329
|
+
* @param actorRoute - The raw value of the actor route. It CAN carry a query.
|
|
1197
1330
|
*/
|
|
1198
|
-
|
|
1331
|
+
isActorAtPath(machinePath, actorRoute) {
|
|
1199
1332
|
if (actorRoute == null)
|
|
1200
1333
|
return false;
|
|
1201
|
-
// The
|
|
1202
|
-
//
|
|
1203
|
-
//
|
|
1204
|
-
//
|
|
1205
|
-
//
|
|
1206
|
-
//
|
|
1207
|
-
|
|
1334
|
+
// The PATHNAME half of the actor route.
|
|
1335
|
+
//
|
|
1336
|
+
// An actor route carries its QUERY, because `context.query` travels in
|
|
1337
|
+
// `currentRoute`, and `machinePath` carries none — `sanitizePathname` cut it off
|
|
1338
|
+
// before the caller stripped the prefix. The two are therefore never the identical
|
|
1339
|
+
// string for a route with a query, and a comparison of the whole strings reported an
|
|
1340
|
+
// actor standing exactly at this location as "somewhere else": `performInitialSync`
|
|
1341
|
+
// read that as a deep link and sent a `play.route` for the state the actor was in
|
|
1342
|
+
// already, so every entry action of that state ran a second time.
|
|
1343
|
+
const { pathname: actorPath } = splitLocation(actorRoute);
|
|
1344
|
+
// An ESCAPED literal route resolves on that same half, and its two forms are never
|
|
1345
|
+
// the identical string: the actor carries `/tags/c\+\+`, which is what the template
|
|
1346
|
+
// writes, and the router stands at `/tags/c++`, which is the path that the route
|
|
1347
|
+
// matches and the path that `resolveNavigationPath` pushes. The resolution reads the
|
|
1348
|
+
// PATHNAME, and not the whole string, for two reasons: a `?` is a modifier of the
|
|
1349
|
+
// grammar that makes `/tags/c\+\+?ref=nav` malformed, and a query would key the
|
|
1350
|
+
// bounded parse cache on a string that no route map holds.
|
|
1351
|
+
const literalPath = actorPath.includes("\\")
|
|
1352
|
+
? parsePattern(actorPath, this.compileOptions).literalPath
|
|
1353
|
+
: null;
|
|
1354
|
+
// The pathname test answers with no map at all, and it has to run FIRST: the lookup
|
|
1355
|
+
// below returns nothing for a location that the map does not hold, and the early
|
|
1356
|
+
// return then reported an actor standing exactly at that location as "somewhere
|
|
1357
|
+
// else". The seed of the constructor hid this for as long as one field carried the
|
|
1358
|
+
// route of the actor AND the location.
|
|
1359
|
+
if (machinePath === (literalPath ?? actorPath))
|
|
1208
1360
|
return true;
|
|
1209
1361
|
const resolvedStateId = this.routeMap.getStateIdByPath(machinePath);
|
|
1210
1362
|
if (resolvedStateId === null || resolvedStateId === undefined)
|
|
@@ -1256,10 +1408,10 @@ export class RouterBridgeBase {
|
|
|
1256
1408
|
* callback reports the pathname and the search SEPARATELY, and `sanitizePathname`
|
|
1257
1409
|
* removes the query. A comparison of the pathname alone therefore misses for every
|
|
1258
1410
|
* route with a query, and the push of this bridge comes back as a `play.route` event
|
|
1259
|
-
* of its own navigation. That echo is not only wasted work:
|
|
1260
|
-
*
|
|
1261
|
-
*
|
|
1262
|
-
*
|
|
1411
|
+
* of its own navigation. That echo is not only wasted work: the bridge sends a
|
|
1412
|
+
* `play.route` for a location that the actor stands on already, so every entry action
|
|
1413
|
+
* of that state runs a second time, and a guard that refuses the repeat moves the
|
|
1414
|
+
* actor to a state that no user asked for.
|
|
1263
1415
|
*
|
|
1264
1416
|
* The query is compared as a MAP, and not as a string: a framework can re-serialize
|
|
1265
1417
|
* a search string in another order, and `URLSearchParams` also accepts the form with
|
|
@@ -1279,7 +1431,7 @@ export class RouterBridgeBase {
|
|
|
1279
1431
|
// move that keeps the path and changes the query, which a filter link does, and
|
|
1280
1432
|
// which a `validateSearch` of a route does when it injects a default. The actor
|
|
1281
1433
|
// then never hears of the move, and the URL and the actor diverge.
|
|
1282
|
-
const
|
|
1434
|
+
const { pathname: lastPath, query: lastQuery } = splitLocation(last);
|
|
1283
1435
|
if (lastPath !== sanitized)
|
|
1284
1436
|
return false;
|
|
1285
1437
|
return sortedQuery(lastQuery) === sortedQuery(search ?? "");
|
|
@@ -1344,41 +1496,52 @@ export class RouterBridgeBase {
|
|
|
1344
1496
|
}
|
|
1345
1497
|
}
|
|
1346
1498
|
/**
|
|
1347
|
-
*
|
|
1499
|
+
* True when the query of an actor route and the query of the router are the same query.
|
|
1500
|
+
*
|
|
1501
|
+
* `isActorAtPath` answers where the actor stands, and this function answers whether the
|
|
1502
|
+
* two sides agree about the query there. The order of the keys is not significant, which
|
|
1503
|
+
* is what `isEchoOfLastSync` says for the same pair of values.
|
|
1348
1504
|
*
|
|
1349
|
-
* A
|
|
1350
|
-
*
|
|
1505
|
+
* A query that DIFFERS answers `false`, and it must: the first synchronization gives the
|
|
1506
|
+
* router the priority, so a location with another query is a deep link that the actor has
|
|
1507
|
+
* to follow. An ABSENT query of the actor against a query of the router is such a
|
|
1508
|
+
* difference — without this, a deep link of `/settings?ref=nav` reached an actor that
|
|
1509
|
+
* stood at `/settings` never, and `context.query` stayed empty for the session while
|
|
1510
|
+
* `lastSyncedPath` recorded the query and suppressed every later report of it. A stateId
|
|
1511
|
+
* carries no query of its own, so it stands at a location with one never.
|
|
1512
|
+
*
|
|
1513
|
+
* @param actorRoute - The raw value of the actor route. It CAN carry a query.
|
|
1514
|
+
* @param search - The query of the router, or `undefined` when it has none.
|
|
1351
1515
|
*/
|
|
1352
|
-
function
|
|
1353
|
-
const
|
|
1354
|
-
|
|
1516
|
+
function sameQuery(actorRoute, search) {
|
|
1517
|
+
const { query: actorQuery, fragment } = splitLocation(actorRoute);
|
|
1518
|
+
// A FRAGMENT is a difference, always. `sanitizePathname` cuts it off, so the router
|
|
1519
|
+
// reports one never and this comparison can confirm one never — and "unknown" must not
|
|
1520
|
+
// read as "the same". An actor that carries `/#intro` against a router at `/` is the
|
|
1521
|
+
// restore that puts the fragment in the address bar, and a `true` here swallowed it:
|
|
1522
|
+
// the bridge recorded the location, sent nothing, and the fragment reached the URL on
|
|
1523
|
+
// no later navigation either.
|
|
1524
|
+
if (fragment !== "")
|
|
1525
|
+
return false;
|
|
1526
|
+
return sortedQuery(actorQuery) === sortedQuery(search ?? "");
|
|
1355
1527
|
}
|
|
1356
1528
|
/**
|
|
1357
|
-
*
|
|
1358
|
-
* with its leading `#`.
|
|
1529
|
+
* True when an actor route carries no query and no fragment of its own.
|
|
1359
1530
|
*
|
|
1360
|
-
*
|
|
1361
|
-
*
|
|
1362
|
-
*
|
|
1363
|
-
*
|
|
1364
|
-
* that BACK cannot escape.
|
|
1531
|
+
* The RESTORE branch asks this beside {@link RouterBridgeBase.isActorAtPath}. An actor
|
|
1532
|
+
* that stands on the path of the router and answers `true` here restores nothing: the
|
|
1533
|
+
* address bar already shows its place, and a push writes the same path back and drops
|
|
1534
|
+
* whatever the router carried beside it.
|
|
1365
1535
|
*
|
|
1366
|
-
*
|
|
1367
|
-
*
|
|
1368
|
-
*
|
|
1536
|
+
* A stateId answers `true`, because a stateId carries no query and no fragment. The
|
|
1537
|
+
* leading `#` of `"#home"` is no fragment, which is what {@link splitLocation} says at
|
|
1538
|
+
* index 0.
|
|
1369
1539
|
*
|
|
1370
|
-
*
|
|
1371
|
-
* stateId such as "#home", which `pushResolvedRoute` stores when it can resolve no
|
|
1372
|
-
* path. Cutting there would turn that value into the empty string.
|
|
1540
|
+
* @param actorRoute - The raw value of the actor route.
|
|
1373
1541
|
*/
|
|
1374
|
-
function
|
|
1375
|
-
const
|
|
1376
|
-
|
|
1377
|
-
const fragment = fragmentAt === -1 ? "" : location.slice(fragmentAt);
|
|
1378
|
-
const mark = withoutFragment.indexOf("?");
|
|
1379
|
-
if (mark === -1)
|
|
1380
|
-
return [withoutFragment, "", fragment];
|
|
1381
|
-
return [withoutFragment.slice(0, mark), withoutFragment.slice(mark + 1), fragment];
|
|
1542
|
+
function carriesNoOwnSuffix(actorRoute) {
|
|
1543
|
+
const { query, fragment } = splitLocation(actorRoute);
|
|
1544
|
+
return query === "" && fragment === "";
|
|
1382
1545
|
}
|
|
1383
1546
|
/** Joins a pathname and a query into one location value. An empty query adds nothing. */
|
|
1384
1547
|
function withSearch(pathname, search) {
|
|
@@ -1408,17 +1571,34 @@ function sortedQuery(search) {
|
|
|
1408
1571
|
return pairs.toSorted().join("&");
|
|
1409
1572
|
}
|
|
1410
1573
|
function noopCleanup() { }
|
|
1411
|
-
|
|
1574
|
+
/**
|
|
1575
|
+
* Watches the route atom of the actor.
|
|
1576
|
+
*
|
|
1577
|
+
* The bridge wraps the read in NOTHING. A subscription evaluates a computed atom inside
|
|
1578
|
+
* the `set` of the actor state, so a throw from the derivation would unwind into
|
|
1579
|
+
* `actor.send` and break the transition for every other observer — and `currentRoute`
|
|
1580
|
+
* therefore contains its own failures, reports them through `onError` of the actor, and
|
|
1581
|
+
* keeps its last route. Every subscriber benefits from that rule, and not this bridge
|
|
1582
|
+
* alone.
|
|
1583
|
+
*/
|
|
1584
|
+
function createRouteWatcher(atom, onRoute) {
|
|
1412
1585
|
let cleanup = noopCleanup;
|
|
1413
1586
|
const watcher = {
|
|
1414
|
-
watch(
|
|
1415
|
-
|
|
1587
|
+
watch(nextAtom) {
|
|
1588
|
+
// Release the watch that this handle holds already. `RouteWatcherHandle` is a
|
|
1589
|
+
// published type, so a bridge of a consumer can repoint the handle at the atom of
|
|
1590
|
+
// a new actor — and an assignment alone dropped the reference to the first
|
|
1591
|
+
// cleanup, which left the first subscription live for the life of the process.
|
|
1592
|
+
watcher.unwatch();
|
|
1593
|
+
cleanup = watchAtom(nextAtom, onRoute);
|
|
1416
1594
|
},
|
|
1417
1595
|
unwatch() {
|
|
1418
|
-
cleanup
|
|
1596
|
+
const release = cleanup;
|
|
1597
|
+
cleanup = noopCleanup;
|
|
1598
|
+
release();
|
|
1419
1599
|
},
|
|
1420
1600
|
};
|
|
1421
|
-
watcher.watch(
|
|
1601
|
+
watcher.watch(atom);
|
|
1422
1602
|
return watcher;
|
|
1423
1603
|
}
|
|
1424
1604
|
/**
|
|
@@ -1431,8 +1611,8 @@ function createRouteWatcher(signal, onRoute) {
|
|
|
1431
1611
|
* address bar for ever.
|
|
1432
1612
|
*/
|
|
1433
1613
|
function sameLocation(a, b) {
|
|
1434
|
-
const
|
|
1435
|
-
const
|
|
1614
|
+
const { pathname: aPath, query: aQuery, fragment: aFragment } = splitLocation(a);
|
|
1615
|
+
const { pathname: bPath, query: bQuery, fragment: bFragment } = splitLocation(b);
|
|
1436
1616
|
if (aPath !== bPath)
|
|
1437
1617
|
return false;
|
|
1438
1618
|
if (aFragment !== bFragment)
|