@native-router/core 1.4.1 → 1.6.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 +14 -1
- package/dist/index.cjs +114 -21
- package/dist/index.mjs +114 -22
- package/dist/types/router.d.ts +42 -5
- package/dist/types/types.d.ts +60 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,18 @@ const unlisten = listen(router, (view) => {
|
|
|
27
27
|
});
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
`viewStack` is the SPA-navigation counterpart of the browser's [bfcache](https://web.dev/articles/bfcache). The browser snapshots whole documents so cross-document back/forward restores instantly; the router snapshots resolved views so same-document back/forward (`pushState`/POP) does too. The two layers are complementary and never overlap: a same-document navigation never enters the bfcache, and a bfcache restore does not fire `popstate`. Together with your data layer they stack as **bfcache > viewStack > queryCache**, outermost first — any restore short-circuits every inner layer with zero requests, so freshness is compensated at the edges (e.g. refetch-on-focus in the query layer).
|
|
31
|
+
|
|
32
|
+
Snapshots can outlive their validity — after a logout or an account switch, the previous account's resolved views are exactly what a back POP must not restore. `invalidate(router)` drops every snapshot at once: the currently rendered view is untouched (no re-resolve, no re-render), and the next back/forward re-runs the guards and loaders of the landed entry through the same lazy path as out-of-window entries.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import {invalidate} from '@native-router/core';
|
|
36
|
+
|
|
37
|
+
// After the session identity changed: keep rendering the current view,
|
|
38
|
+
// but never restore a snapshot of the previous account on back/forward.
|
|
39
|
+
invalidate(router);
|
|
40
|
+
```
|
|
41
|
+
|
|
30
42
|
### Survives a refresh
|
|
31
43
|
|
|
32
44
|
The session stack is serialized into `history.state` as a bounded tail window (`maxStackDepth`, default 100) and restored on `create`. Warm the window once after a refresh with `initHistoryStack`, and every in-window back/forward renders from cache with zero requests. Entries outside the window fall back to a single lazy re-resolve.
|
|
@@ -57,8 +69,9 @@ commit(router, entry.task, entry.location); // commit like a click
|
|
|
57
69
|
- Framework-agnostic: bring your own `resolveView`, the view type (`V`) is yours — a string, a vdom, anything
|
|
58
70
|
- Route matching via path-to-regexp: declaration order, layout routes without `path`, index/fallback children with `path: ''`, strict trailing slashes, case-sensitive, nested params merged deep over shallow
|
|
59
71
|
- Route guards: static `redirect` and async `beforeLoad` on every route level, run shallow → deep; more than 10 chained redirects reject with `RedirectLoopError`
|
|
60
|
-
- Cancelable async navigation: a new resolve supersedes the in-flight one (`currentGuard`); `cancel()` aborts it; a history POP cancels it too. A superseded or cancelled `navigate()` promise **never settles** — don't `await` a navigation that might be superseded
|
|
72
|
+
- Cancelable async navigation: a new resolve supersedes the in-flight one (`currentGuard`); `cancel()` aborts it; a history POP cancels it too. A superseded or cancelled `navigate()` promise **never settles** — don't `await` a navigation that might be superseded. Superseding or cancelling also aborts the chain's `AbortSignal`: guards (`beforeLoad` ctx) and view loaders (`ResolveViewContext`) receive it as `ctx.signal`, so their in-flight requests stop instead of only having results dropped; `preload` resolutions are shared and therefore never aborted
|
|
61
73
|
- Navigation API: `navigate`, `refresh`, `go`/`forward`/`back`, `commit`/`commitReplace`, `createHref`, `getParams`, `match`, `toLocation`, `resolve`, `resolveTo`
|
|
74
|
+
- `invalidate(router)`: drop the session view snapshots in one call — the current view stays rendered (no re-resolve, no re-render) and the next back/forward re-resolves through the guards; the typical call site is right after a logout/account switch, so a POP cannot render the previous account's data or bypass guards that already ran
|
|
62
75
|
- Search validation via [Standard Schema](https://standardschema.dev): a `search` schema on any route level (zod/valibot/arktype, no hard dependency), parsed with `parseSearch`/`parseSearchSync`; failures throw `SearchError`
|
|
63
76
|
- `preload(router, to, {ttl})`: resolve a target through the guards ahead of time, sharing one task across concurrent callers (in-flight dedup) with a TTL, default 30s; consumed entries are dropped on commit
|
|
64
77
|
- `errorHandler` hook turns resolve failures into fallback views
|
package/dist/index.cjs
CHANGED
|
@@ -75,6 +75,10 @@ const DEFAULT_PRELOAD_TTL = 30_000;
|
|
|
75
75
|
* `history index - baseIndex`; entries whose slot falls outside the
|
|
76
76
|
* memory window re-resolve lazily when landed on.
|
|
77
77
|
* - `preloadCache`: router-level cache of {@link preload} results.
|
|
78
|
+
* - `resolvingController`: the in-flight chain's AbortController. It is
|
|
79
|
+
* aborted(supersede/cancel) only while the chain is in flight; a
|
|
80
|
+
* settled chain's controller is left alone so its contexts never
|
|
81
|
+
* report `aborted` for a navigation that actually committed.
|
|
78
82
|
*/
|
|
79
83
|
|
|
80
84
|
/**
|
|
@@ -108,8 +112,15 @@ function create(routes, history, resolveView, options) {
|
|
|
108
112
|
resolveView,
|
|
109
113
|
history: instanceHistory,
|
|
110
114
|
locationStack,
|
|
111
|
-
// The view stack is
|
|
112
|
-
//
|
|
115
|
+
// The view stack is the SPA-navigation counterpart of the browser's
|
|
116
|
+
// bfcache — a resolved-view snapshot per history entry, restored with
|
|
117
|
+
// zero requests on POP. It is window-relative, so it is exactly as
|
|
118
|
+
// long as the location window and stays bounded by maxStackDepth
|
|
119
|
+
// with it; invalidate() drops these snapshots.
|
|
120
|
+
//
|
|
121
|
+
// viewStack 是 SPA 内导航对应的 bfcache——每个 history 条目一份已解析
|
|
122
|
+
// 视图快照,POP 时零请求还原。它按窗口相对位置存放,与 location 窗口
|
|
123
|
+
// 等长、随 maxStackDepth 一同封顶;invalidate() 丢弃这些快照。
|
|
113
124
|
viewStack: new Array(locationStack.length).fill(null),
|
|
114
125
|
baseIndex,
|
|
115
126
|
preloadCache: new Map(),
|
|
@@ -274,7 +285,10 @@ function resolve(router, location) {
|
|
|
274
285
|
} = router;
|
|
275
286
|
return (matched ? resolveView(matched, {
|
|
276
287
|
router,
|
|
277
|
-
location
|
|
288
|
+
location,
|
|
289
|
+
// One-shot resolves(warm-up, direct calls) are never superseded
|
|
290
|
+
// or cancelled: their loaders get a signal that never aborts.
|
|
291
|
+
signal: new AbortController().signal
|
|
278
292
|
}) : Promise.reject(new NotFoundError(location.pathname))).catch(errorHandler);
|
|
279
293
|
}
|
|
280
294
|
|
|
@@ -301,22 +315,37 @@ function resolveTo(router, to, state) {
|
|
|
301
315
|
* levels re-run on every hop(keep side-effectful guards idempotent) —
|
|
302
316
|
* carrying the original user state; at most
|
|
303
317
|
* {@link MAX_REDIRECTS 10} redirects are followed before a
|
|
304
|
-
*
|
|
318
|
+
* An unmatched pathname keeps the
|
|
305
319
|
* {@link resolve resolve} behavior: the task rejects with a
|
|
306
320
|
* {@link NotFoundError} and is routed through `router.errorHandler`.
|
|
307
321
|
*
|
|
322
|
+
* `opts.signal` is the abort signal of the whole chain: guards see it in
|
|
323
|
+
* their {@link GuardContext contexts}, the view task's
|
|
324
|
+
* {@link ResolveViewContext context} carries it on, and it is aborted
|
|
325
|
+
* once the navigation is superseded or cancelled. Standalone callers
|
|
326
|
+
* that pass nothing(e.g. {@link preload}) get a signal that never
|
|
327
|
+
* aborts — their resolution may be shared, so cancelling it on behalf of
|
|
328
|
+
* one consumer is not sound yet.
|
|
329
|
+
*
|
|
308
330
|
* @group Methods
|
|
309
331
|
* @category Router
|
|
310
332
|
* @param router router instance
|
|
311
333
|
* @param location the location to resolve; the object itself is never
|
|
312
334
|
* mutated — a redirect rebinds the resolution to a new location
|
|
335
|
+
* @param opts options; `signal` is the chain's abort signal
|
|
313
336
|
* @returns the terminal location and its resolve task
|
|
314
337
|
*/
|
|
315
|
-
async function resolveEntry(router, location) {
|
|
338
|
+
async function resolveEntry(router, location, opts) {
|
|
316
339
|
const {
|
|
317
340
|
resolveView,
|
|
318
341
|
errorHandler
|
|
319
342
|
} = router;
|
|
343
|
+
// The chain owner(navigate/refresh) passes its controller's signal;
|
|
344
|
+
// standalone resolutions get one more controller whose signal never
|
|
345
|
+
// aborts, so downstream consumers always observe a real signal.
|
|
346
|
+
const {
|
|
347
|
+
signal = new AbortController().signal
|
|
348
|
+
} = opts ?? {};
|
|
320
349
|
for (let redirects = 0;; redirects++) {
|
|
321
350
|
if (redirects > MAX_REDIRECTS) {
|
|
322
351
|
throw new RedirectLoopError(location.pathname);
|
|
@@ -340,7 +369,8 @@ async function resolveEntry(router, location) {
|
|
|
340
369
|
await route.beforeLoad?.({
|
|
341
370
|
router,
|
|
342
371
|
location,
|
|
343
|
-
params: mergeMatchedParams(matched, i)
|
|
372
|
+
params: mergeMatchedParams(matched, i),
|
|
373
|
+
signal
|
|
344
374
|
}));
|
|
345
375
|
if (target) {
|
|
346
376
|
location = toLocation(router, target, location.state);
|
|
@@ -354,7 +384,8 @@ async function resolveEntry(router, location) {
|
|
|
354
384
|
location,
|
|
355
385
|
task: resolveView(matched, {
|
|
356
386
|
router,
|
|
357
|
-
location
|
|
387
|
+
location,
|
|
388
|
+
signal
|
|
358
389
|
}).catch(errorHandler)
|
|
359
390
|
};
|
|
360
391
|
}
|
|
@@ -460,12 +491,12 @@ function commit(router, resolvePromise, location) {
|
|
|
460
491
|
task: resolvePromise
|
|
461
492
|
}), location);
|
|
462
493
|
}
|
|
463
|
-
function pushEntry(router, entryPromise, fromLocation) {
|
|
494
|
+
function pushEntry(router, entryPromise, fromLocation, ac) {
|
|
464
495
|
const {
|
|
465
496
|
history
|
|
466
497
|
} = router;
|
|
467
498
|
const nextIndex = getHistoryState(router).index + 1;
|
|
468
|
-
return commitBase(router, entryPromise, fromLocation, (resolvedView, entry) => {
|
|
499
|
+
return commitBase(router, entryPromise, fromLocation, ac, (resolvedView, entry) => {
|
|
469
500
|
const {
|
|
470
501
|
location
|
|
471
502
|
} = entry;
|
|
@@ -512,14 +543,14 @@ function commitReplace(router, resolvePromise, location) {
|
|
|
512
543
|
task: resolvePromise
|
|
513
544
|
}), location);
|
|
514
545
|
}
|
|
515
|
-
function replaceEntry(router, entryPromise, fromLocation) {
|
|
546
|
+
function replaceEntry(router, entryPromise, fromLocation, ac) {
|
|
516
547
|
const {
|
|
517
548
|
history
|
|
518
549
|
} = router;
|
|
519
550
|
const {
|
|
520
551
|
index
|
|
521
552
|
} = getHistoryState(router);
|
|
522
|
-
return commitBase(router, entryPromise, fromLocation, (resolvedView, entry) => {
|
|
553
|
+
return commitBase(router, entryPromise, fromLocation, ac, (resolvedView, entry) => {
|
|
523
554
|
const {
|
|
524
555
|
location
|
|
525
556
|
} = entry;
|
|
@@ -545,7 +576,8 @@ function replaceEntry(router, entryPromise, fromLocation) {
|
|
|
545
576
|
});
|
|
546
577
|
});
|
|
547
578
|
}
|
|
548
|
-
function commitBase(router, entryPromise, location, onResolved) {
|
|
579
|
+
function commitBase(router, entryPromise, location, ac, onResolved) {
|
|
580
|
+
const core = router;
|
|
549
581
|
const {
|
|
550
582
|
currentGuard,
|
|
551
583
|
onLoadingChange = util.noop
|
|
@@ -553,8 +585,16 @@ function commitBase(router, entryPromise, location, onResolved) {
|
|
|
553
585
|
if (router.resolving) {
|
|
554
586
|
// Cancel current resolve
|
|
555
587
|
onLoadingChange();
|
|
588
|
+
// ...and stop its requests: the guard below discards the superseded
|
|
589
|
+
// chain's result, so its in-flight guards/loaders must not keep
|
|
590
|
+
// consuming the network until they settle on their own.
|
|
591
|
+
core.resolvingController?.abort();
|
|
556
592
|
}
|
|
557
593
|
router.resolving = location;
|
|
594
|
+
// External commits(plain tasks from resolveTo/preload) carry no
|
|
595
|
+
// controller; clearing the slot keeps a stale one from being aborted
|
|
596
|
+
// by a later supersede.
|
|
597
|
+
core.resolvingController = ac;
|
|
558
598
|
onLoadingChange('pending');
|
|
559
599
|
return (
|
|
560
600
|
// The whole chain — route guards AND the view task — is guarded from
|
|
@@ -568,15 +608,19 @@ function commitBase(router, entryPromise, location, onResolved) {
|
|
|
568
608
|
entry,
|
|
569
609
|
resolvedView
|
|
570
610
|
}) => {
|
|
611
|
+
// This chain settled: it is no longer in flight. Clearing the
|
|
612
|
+
// mark BEFORE onResolved matters because onResolved commits
|
|
613
|
+
// history, which synchronously re-enters cancel() through the
|
|
614
|
+
// router's own listen() handler — an already-settled chain must
|
|
615
|
+
// not be aborted(or fire a cancel signal) as if it were still
|
|
616
|
+
// running. Superseded chains park forever, so the mark is always
|
|
617
|
+
// ours to clear.
|
|
618
|
+
router.resolving = undefined;
|
|
571
619
|
onResolved(resolvedView, entry);
|
|
572
620
|
// The navigation consumed this resolution: drop its preload
|
|
573
621
|
// cache slots so a later preload re-resolves fresh state.
|
|
574
622
|
evictPreloadCache(router, entry.location);
|
|
575
623
|
}).then(() => {
|
|
576
|
-
// This chain settled, so it is no longer in flight. Superseded
|
|
577
|
-
// chains park forever, so only the latest chain can reach here:
|
|
578
|
-
// the mark is always ours to clear.
|
|
579
|
-
router.resolving = undefined;
|
|
580
624
|
onLoadingChange('resolved');
|
|
581
625
|
}).catch(e => {
|
|
582
626
|
router.resolving = undefined;
|
|
@@ -591,7 +635,10 @@ function commitBase(router, entryPromise, location, onResolved) {
|
|
|
591
635
|
* the view resolves; the history entry is committed on the terminal
|
|
592
636
|
* location when guards redirected. The guard phase is part of the
|
|
593
637
|
* cancelable navigation: a superseding navigate or a `cancel()` while
|
|
594
|
-
* guards are still running discards this navigation
|
|
638
|
+
* guards are still running discards this navigation — and aborts the
|
|
639
|
+
* chain's `signal`, so guards and loaders observing it({@link
|
|
640
|
+
* GuardContext.signal}, {@link ResolveViewContext.signal}) stop their
|
|
641
|
+
* requests instead of only having their results dropped.
|
|
595
642
|
* @group Methods
|
|
596
643
|
* @category Router
|
|
597
644
|
* @param router router instance
|
|
@@ -600,19 +647,29 @@ function commitBase(router, entryPromise, location, onResolved) {
|
|
|
600
647
|
*/
|
|
601
648
|
function navigate(router, to, state) {
|
|
602
649
|
const location = toLocation(router, to, state);
|
|
603
|
-
|
|
650
|
+
// One controller per navigation round: guards and view loaders of the
|
|
651
|
+
// whole chain(including redirect hops) share its signal.
|
|
652
|
+
const ac = new AbortController();
|
|
653
|
+
return pushEntry(router, resolveEntry(router, location, {
|
|
654
|
+
signal: ac.signal
|
|
655
|
+
}), location, ac);
|
|
604
656
|
}
|
|
605
657
|
|
|
606
658
|
/**
|
|
607
659
|
* Refresh the page. Route guards run before the view resolves; a redirect
|
|
608
|
-
* replaces the current entry with the terminal location.
|
|
660
|
+
* replaces the current entry with the terminal location. The refresh is a
|
|
661
|
+
* cancelable navigation chain like {@link navigate}: superseding it or
|
|
662
|
+
* `cancel()` aborts its signal.
|
|
609
663
|
* @group Methods
|
|
610
664
|
* @category Router
|
|
611
665
|
* @param router router instance
|
|
612
666
|
*/
|
|
613
667
|
function refresh(router) {
|
|
614
668
|
const location = getLocation(router);
|
|
615
|
-
|
|
669
|
+
const ac = new AbortController();
|
|
670
|
+
return replaceEntry(router, resolveEntry(router, location, {
|
|
671
|
+
signal: ac.signal
|
|
672
|
+
}), location, ac);
|
|
616
673
|
}
|
|
617
674
|
|
|
618
675
|
/**
|
|
@@ -662,12 +719,22 @@ function createHref({
|
|
|
662
719
|
}
|
|
663
720
|
|
|
664
721
|
/**
|
|
665
|
-
* Cancel the current navigate.
|
|
722
|
+
* Cancel the current navigate. The in-flight chain's guards/loaders are
|
|
723
|
+
* aborted through their signal, not merely discarded.
|
|
666
724
|
* @group Methods
|
|
667
725
|
* @category Router
|
|
668
726
|
* @param router router instance
|
|
669
727
|
*/
|
|
670
728
|
function cancel(router) {
|
|
729
|
+
const core = router;
|
|
730
|
+
// Aborting is reserved for chains that are still running: a chain that
|
|
731
|
+
// just committed re-enters cancel() synchronously through listen()'s
|
|
732
|
+
// history handler and must not have its(possibly still-rendered) view
|
|
733
|
+
// contexts aborted after the fact.
|
|
734
|
+
if (router.resolving) {
|
|
735
|
+
core.resolvingController?.abort();
|
|
736
|
+
core.resolvingController = undefined;
|
|
737
|
+
}
|
|
671
738
|
// The cancelled chain parks forever, so nothing else will clear the
|
|
672
739
|
// in-flight mark: drop it here, or a later navigation would fire a
|
|
673
740
|
// spurious cancel signal(`onLoadingChange()`) for a dead resolve.
|
|
@@ -693,6 +760,31 @@ function initHistoryStack(router) {
|
|
|
693
760
|
});
|
|
694
761
|
}
|
|
695
762
|
|
|
763
|
+
/**
|
|
764
|
+
* Drop every view snapshot of the session window. The already rendered
|
|
765
|
+
* view is untouched — no re-resolve, no re-render; only future POPs
|
|
766
|
+
* change: with no snapshot to hit, {@link listen} falls back to the same
|
|
767
|
+
* lazy re-resolve path as out-of-window entries, so the landed entry's
|
|
768
|
+
* guards(`redirect`/`beforeLoad`) and loaders run again. Call it when
|
|
769
|
+
* the snapshots stop being valid — e.g. right after a logout or an
|
|
770
|
+
* account switch, so a back POP cannot render the previous account's
|
|
771
|
+
* view or bypass guards that already ran in the session.
|
|
772
|
+
*
|
|
773
|
+
* 丢弃会话窗口内的全部视图快照。已渲染的当前视图不受影响——不重解析、
|
|
774
|
+
* 不重渲染;变化的只有后续 POP:无快照可命中时,listen 落入与窗口外条目
|
|
775
|
+
* 相同的惰性重解析路径,落点条目的守卫与加载器重新执行。快照失效时调用
|
|
776
|
+
* ——例如登出/切换账号后,后退 POP 不再渲染上一账号的视图、也不再绕过
|
|
777
|
+
* 会话内已执行过的守卫。
|
|
778
|
+
* @group Methods
|
|
779
|
+
* @category Router
|
|
780
|
+
* @param router router instance
|
|
781
|
+
*/
|
|
782
|
+
function invalidate(router) {
|
|
783
|
+
// Keep the window shape: locationStack stays untouched, so getParams
|
|
784
|
+
// and the serialized window keep working — only the snapshots go.
|
|
785
|
+
router.viewStack = new Array(router.locationStack.length).fill(null);
|
|
786
|
+
}
|
|
787
|
+
|
|
696
788
|
/**
|
|
697
789
|
* Listen the history change.
|
|
698
790
|
* @group Methods
|
|
@@ -874,6 +966,7 @@ exports.getLocation = getLocation;
|
|
|
874
966
|
exports.getParams = getParams;
|
|
875
967
|
exports.go = go;
|
|
876
968
|
exports.initHistoryStack = initHistoryStack;
|
|
969
|
+
exports.invalidate = invalidate;
|
|
877
970
|
exports.listen = listen;
|
|
878
971
|
exports.match = match;
|
|
879
972
|
exports.mergeMatchedParams = mergeMatchedParams;
|
package/dist/index.mjs
CHANGED
|
@@ -73,6 +73,10 @@ const DEFAULT_PRELOAD_TTL = 30_000;
|
|
|
73
73
|
* `history index - baseIndex`; entries whose slot falls outside the
|
|
74
74
|
* memory window re-resolve lazily when landed on.
|
|
75
75
|
* - `preloadCache`: router-level cache of {@link preload} results.
|
|
76
|
+
* - `resolvingController`: the in-flight chain's AbortController. It is
|
|
77
|
+
* aborted(supersede/cancel) only while the chain is in flight; a
|
|
78
|
+
* settled chain's controller is left alone so its contexts never
|
|
79
|
+
* report `aborted` for a navigation that actually committed.
|
|
76
80
|
*/
|
|
77
81
|
|
|
78
82
|
/**
|
|
@@ -106,8 +110,15 @@ function create(routes, history, resolveView, options) {
|
|
|
106
110
|
resolveView,
|
|
107
111
|
history: instanceHistory,
|
|
108
112
|
locationStack,
|
|
109
|
-
// The view stack is
|
|
110
|
-
//
|
|
113
|
+
// The view stack is the SPA-navigation counterpart of the browser's
|
|
114
|
+
// bfcache — a resolved-view snapshot per history entry, restored with
|
|
115
|
+
// zero requests on POP. It is window-relative, so it is exactly as
|
|
116
|
+
// long as the location window and stays bounded by maxStackDepth
|
|
117
|
+
// with it; invalidate() drops these snapshots.
|
|
118
|
+
//
|
|
119
|
+
// viewStack 是 SPA 内导航对应的 bfcache——每个 history 条目一份已解析
|
|
120
|
+
// 视图快照,POP 时零请求还原。它按窗口相对位置存放,与 location 窗口
|
|
121
|
+
// 等长、随 maxStackDepth 一同封顶;invalidate() 丢弃这些快照。
|
|
111
122
|
viewStack: new Array(locationStack.length).fill(null),
|
|
112
123
|
baseIndex,
|
|
113
124
|
preloadCache: new Map(),
|
|
@@ -272,7 +283,10 @@ function resolve(router, location) {
|
|
|
272
283
|
} = router;
|
|
273
284
|
return (matched ? resolveView(matched, {
|
|
274
285
|
router,
|
|
275
|
-
location
|
|
286
|
+
location,
|
|
287
|
+
// One-shot resolves(warm-up, direct calls) are never superseded
|
|
288
|
+
// or cancelled: their loaders get a signal that never aborts.
|
|
289
|
+
signal: new AbortController().signal
|
|
276
290
|
}) : Promise.reject(new NotFoundError(location.pathname))).catch(errorHandler);
|
|
277
291
|
}
|
|
278
292
|
|
|
@@ -299,22 +313,37 @@ function resolveTo(router, to, state) {
|
|
|
299
313
|
* levels re-run on every hop(keep side-effectful guards idempotent) —
|
|
300
314
|
* carrying the original user state; at most
|
|
301
315
|
* {@link MAX_REDIRECTS 10} redirects are followed before a
|
|
302
|
-
*
|
|
316
|
+
* An unmatched pathname keeps the
|
|
303
317
|
* {@link resolve resolve} behavior: the task rejects with a
|
|
304
318
|
* {@link NotFoundError} and is routed through `router.errorHandler`.
|
|
305
319
|
*
|
|
320
|
+
* `opts.signal` is the abort signal of the whole chain: guards see it in
|
|
321
|
+
* their {@link GuardContext contexts}, the view task's
|
|
322
|
+
* {@link ResolveViewContext context} carries it on, and it is aborted
|
|
323
|
+
* once the navigation is superseded or cancelled. Standalone callers
|
|
324
|
+
* that pass nothing(e.g. {@link preload}) get a signal that never
|
|
325
|
+
* aborts — their resolution may be shared, so cancelling it on behalf of
|
|
326
|
+
* one consumer is not sound yet.
|
|
327
|
+
*
|
|
306
328
|
* @group Methods
|
|
307
329
|
* @category Router
|
|
308
330
|
* @param router router instance
|
|
309
331
|
* @param location the location to resolve; the object itself is never
|
|
310
332
|
* mutated — a redirect rebinds the resolution to a new location
|
|
333
|
+
* @param opts options; `signal` is the chain's abort signal
|
|
311
334
|
* @returns the terminal location and its resolve task
|
|
312
335
|
*/
|
|
313
|
-
async function resolveEntry(router, location) {
|
|
336
|
+
async function resolveEntry(router, location, opts) {
|
|
314
337
|
const {
|
|
315
338
|
resolveView,
|
|
316
339
|
errorHandler
|
|
317
340
|
} = router;
|
|
341
|
+
// The chain owner(navigate/refresh) passes its controller's signal;
|
|
342
|
+
// standalone resolutions get one more controller whose signal never
|
|
343
|
+
// aborts, so downstream consumers always observe a real signal.
|
|
344
|
+
const {
|
|
345
|
+
signal = new AbortController().signal
|
|
346
|
+
} = opts ?? {};
|
|
318
347
|
for (let redirects = 0;; redirects++) {
|
|
319
348
|
if (redirects > MAX_REDIRECTS) {
|
|
320
349
|
throw new RedirectLoopError(location.pathname);
|
|
@@ -338,7 +367,8 @@ async function resolveEntry(router, location) {
|
|
|
338
367
|
await route.beforeLoad?.({
|
|
339
368
|
router,
|
|
340
369
|
location,
|
|
341
|
-
params: mergeMatchedParams(matched, i)
|
|
370
|
+
params: mergeMatchedParams(matched, i),
|
|
371
|
+
signal
|
|
342
372
|
}));
|
|
343
373
|
if (target) {
|
|
344
374
|
location = toLocation(router, target, location.state);
|
|
@@ -352,7 +382,8 @@ async function resolveEntry(router, location) {
|
|
|
352
382
|
location,
|
|
353
383
|
task: resolveView(matched, {
|
|
354
384
|
router,
|
|
355
|
-
location
|
|
385
|
+
location,
|
|
386
|
+
signal
|
|
356
387
|
}).catch(errorHandler)
|
|
357
388
|
};
|
|
358
389
|
}
|
|
@@ -458,12 +489,12 @@ function commit(router, resolvePromise, location) {
|
|
|
458
489
|
task: resolvePromise
|
|
459
490
|
}), location);
|
|
460
491
|
}
|
|
461
|
-
function pushEntry(router, entryPromise, fromLocation) {
|
|
492
|
+
function pushEntry(router, entryPromise, fromLocation, ac) {
|
|
462
493
|
const {
|
|
463
494
|
history
|
|
464
495
|
} = router;
|
|
465
496
|
const nextIndex = getHistoryState(router).index + 1;
|
|
466
|
-
return commitBase(router, entryPromise, fromLocation, (resolvedView, entry) => {
|
|
497
|
+
return commitBase(router, entryPromise, fromLocation, ac, (resolvedView, entry) => {
|
|
467
498
|
const {
|
|
468
499
|
location
|
|
469
500
|
} = entry;
|
|
@@ -510,14 +541,14 @@ function commitReplace(router, resolvePromise, location) {
|
|
|
510
541
|
task: resolvePromise
|
|
511
542
|
}), location);
|
|
512
543
|
}
|
|
513
|
-
function replaceEntry(router, entryPromise, fromLocation) {
|
|
544
|
+
function replaceEntry(router, entryPromise, fromLocation, ac) {
|
|
514
545
|
const {
|
|
515
546
|
history
|
|
516
547
|
} = router;
|
|
517
548
|
const {
|
|
518
549
|
index
|
|
519
550
|
} = getHistoryState(router);
|
|
520
|
-
return commitBase(router, entryPromise, fromLocation, (resolvedView, entry) => {
|
|
551
|
+
return commitBase(router, entryPromise, fromLocation, ac, (resolvedView, entry) => {
|
|
521
552
|
const {
|
|
522
553
|
location
|
|
523
554
|
} = entry;
|
|
@@ -543,7 +574,8 @@ function replaceEntry(router, entryPromise, fromLocation) {
|
|
|
543
574
|
});
|
|
544
575
|
});
|
|
545
576
|
}
|
|
546
|
-
function commitBase(router, entryPromise, location, onResolved) {
|
|
577
|
+
function commitBase(router, entryPromise, location, ac, onResolved) {
|
|
578
|
+
const core = router;
|
|
547
579
|
const {
|
|
548
580
|
currentGuard,
|
|
549
581
|
onLoadingChange = noop
|
|
@@ -551,8 +583,16 @@ function commitBase(router, entryPromise, location, onResolved) {
|
|
|
551
583
|
if (router.resolving) {
|
|
552
584
|
// Cancel current resolve
|
|
553
585
|
onLoadingChange();
|
|
586
|
+
// ...and stop its requests: the guard below discards the superseded
|
|
587
|
+
// chain's result, so its in-flight guards/loaders must not keep
|
|
588
|
+
// consuming the network until they settle on their own.
|
|
589
|
+
core.resolvingController?.abort();
|
|
554
590
|
}
|
|
555
591
|
router.resolving = location;
|
|
592
|
+
// External commits(plain tasks from resolveTo/preload) carry no
|
|
593
|
+
// controller; clearing the slot keeps a stale one from being aborted
|
|
594
|
+
// by a later supersede.
|
|
595
|
+
core.resolvingController = ac;
|
|
556
596
|
onLoadingChange('pending');
|
|
557
597
|
return (
|
|
558
598
|
// The whole chain — route guards AND the view task — is guarded from
|
|
@@ -566,15 +606,19 @@ function commitBase(router, entryPromise, location, onResolved) {
|
|
|
566
606
|
entry,
|
|
567
607
|
resolvedView
|
|
568
608
|
}) => {
|
|
609
|
+
// This chain settled: it is no longer in flight. Clearing the
|
|
610
|
+
// mark BEFORE onResolved matters because onResolved commits
|
|
611
|
+
// history, which synchronously re-enters cancel() through the
|
|
612
|
+
// router's own listen() handler — an already-settled chain must
|
|
613
|
+
// not be aborted(or fire a cancel signal) as if it were still
|
|
614
|
+
// running. Superseded chains park forever, so the mark is always
|
|
615
|
+
// ours to clear.
|
|
616
|
+
router.resolving = undefined;
|
|
569
617
|
onResolved(resolvedView, entry);
|
|
570
618
|
// The navigation consumed this resolution: drop its preload
|
|
571
619
|
// cache slots so a later preload re-resolves fresh state.
|
|
572
620
|
evictPreloadCache(router, entry.location);
|
|
573
621
|
}).then(() => {
|
|
574
|
-
// This chain settled, so it is no longer in flight. Superseded
|
|
575
|
-
// chains park forever, so only the latest chain can reach here:
|
|
576
|
-
// the mark is always ours to clear.
|
|
577
|
-
router.resolving = undefined;
|
|
578
622
|
onLoadingChange('resolved');
|
|
579
623
|
}).catch(e => {
|
|
580
624
|
router.resolving = undefined;
|
|
@@ -589,7 +633,10 @@ function commitBase(router, entryPromise, location, onResolved) {
|
|
|
589
633
|
* the view resolves; the history entry is committed on the terminal
|
|
590
634
|
* location when guards redirected. The guard phase is part of the
|
|
591
635
|
* cancelable navigation: a superseding navigate or a `cancel()` while
|
|
592
|
-
* guards are still running discards this navigation
|
|
636
|
+
* guards are still running discards this navigation — and aborts the
|
|
637
|
+
* chain's `signal`, so guards and loaders observing it({@link
|
|
638
|
+
* GuardContext.signal}, {@link ResolveViewContext.signal}) stop their
|
|
639
|
+
* requests instead of only having their results dropped.
|
|
593
640
|
* @group Methods
|
|
594
641
|
* @category Router
|
|
595
642
|
* @param router router instance
|
|
@@ -598,19 +645,29 @@ function commitBase(router, entryPromise, location, onResolved) {
|
|
|
598
645
|
*/
|
|
599
646
|
function navigate(router, to, state) {
|
|
600
647
|
const location = toLocation(router, to, state);
|
|
601
|
-
|
|
648
|
+
// One controller per navigation round: guards and view loaders of the
|
|
649
|
+
// whole chain(including redirect hops) share its signal.
|
|
650
|
+
const ac = new AbortController();
|
|
651
|
+
return pushEntry(router, resolveEntry(router, location, {
|
|
652
|
+
signal: ac.signal
|
|
653
|
+
}), location, ac);
|
|
602
654
|
}
|
|
603
655
|
|
|
604
656
|
/**
|
|
605
657
|
* Refresh the page. Route guards run before the view resolves; a redirect
|
|
606
|
-
* replaces the current entry with the terminal location.
|
|
658
|
+
* replaces the current entry with the terminal location. The refresh is a
|
|
659
|
+
* cancelable navigation chain like {@link navigate}: superseding it or
|
|
660
|
+
* `cancel()` aborts its signal.
|
|
607
661
|
* @group Methods
|
|
608
662
|
* @category Router
|
|
609
663
|
* @param router router instance
|
|
610
664
|
*/
|
|
611
665
|
function refresh(router) {
|
|
612
666
|
const location = getLocation(router);
|
|
613
|
-
|
|
667
|
+
const ac = new AbortController();
|
|
668
|
+
return replaceEntry(router, resolveEntry(router, location, {
|
|
669
|
+
signal: ac.signal
|
|
670
|
+
}), location, ac);
|
|
614
671
|
}
|
|
615
672
|
|
|
616
673
|
/**
|
|
@@ -660,12 +717,22 @@ function createHref({
|
|
|
660
717
|
}
|
|
661
718
|
|
|
662
719
|
/**
|
|
663
|
-
* Cancel the current navigate.
|
|
720
|
+
* Cancel the current navigate. The in-flight chain's guards/loaders are
|
|
721
|
+
* aborted through their signal, not merely discarded.
|
|
664
722
|
* @group Methods
|
|
665
723
|
* @category Router
|
|
666
724
|
* @param router router instance
|
|
667
725
|
*/
|
|
668
726
|
function cancel(router) {
|
|
727
|
+
const core = router;
|
|
728
|
+
// Aborting is reserved for chains that are still running: a chain that
|
|
729
|
+
// just committed re-enters cancel() synchronously through listen()'s
|
|
730
|
+
// history handler and must not have its(possibly still-rendered) view
|
|
731
|
+
// contexts aborted after the fact.
|
|
732
|
+
if (router.resolving) {
|
|
733
|
+
core.resolvingController?.abort();
|
|
734
|
+
core.resolvingController = undefined;
|
|
735
|
+
}
|
|
669
736
|
// The cancelled chain parks forever, so nothing else will clear the
|
|
670
737
|
// in-flight mark: drop it here, or a later navigation would fire a
|
|
671
738
|
// spurious cancel signal(`onLoadingChange()`) for a dead resolve.
|
|
@@ -691,6 +758,31 @@ function initHistoryStack(router) {
|
|
|
691
758
|
});
|
|
692
759
|
}
|
|
693
760
|
|
|
761
|
+
/**
|
|
762
|
+
* Drop every view snapshot of the session window. The already rendered
|
|
763
|
+
* view is untouched — no re-resolve, no re-render; only future POPs
|
|
764
|
+
* change: with no snapshot to hit, {@link listen} falls back to the same
|
|
765
|
+
* lazy re-resolve path as out-of-window entries, so the landed entry's
|
|
766
|
+
* guards(`redirect`/`beforeLoad`) and loaders run again. Call it when
|
|
767
|
+
* the snapshots stop being valid — e.g. right after a logout or an
|
|
768
|
+
* account switch, so a back POP cannot render the previous account's
|
|
769
|
+
* view or bypass guards that already ran in the session.
|
|
770
|
+
*
|
|
771
|
+
* 丢弃会话窗口内的全部视图快照。已渲染的当前视图不受影响——不重解析、
|
|
772
|
+
* 不重渲染;变化的只有后续 POP:无快照可命中时,listen 落入与窗口外条目
|
|
773
|
+
* 相同的惰性重解析路径,落点条目的守卫与加载器重新执行。快照失效时调用
|
|
774
|
+
* ——例如登出/切换账号后,后退 POP 不再渲染上一账号的视图、也不再绕过
|
|
775
|
+
* 会话内已执行过的守卫。
|
|
776
|
+
* @group Methods
|
|
777
|
+
* @category Router
|
|
778
|
+
* @param router router instance
|
|
779
|
+
*/
|
|
780
|
+
function invalidate(router) {
|
|
781
|
+
// Keep the window shape: locationStack stays untouched, so getParams
|
|
782
|
+
// and the serialized window keep working — only the snapshots go.
|
|
783
|
+
router.viewStack = new Array(router.locationStack.length).fill(null);
|
|
784
|
+
}
|
|
785
|
+
|
|
694
786
|
/**
|
|
695
787
|
* Listen the history change.
|
|
696
788
|
* @group Methods
|
|
@@ -856,4 +948,4 @@ function isThenable(value) {
|
|
|
856
948
|
return typeof value?.then === 'function';
|
|
857
949
|
}
|
|
858
950
|
|
|
859
|
-
export { NativeRouterError, NotFoundError, RedirectLoopError, SearchError, back, cancel, commit, commitReplace, create, createHref, forward, getCurrentView, getLocation, getParams, go, initHistoryStack, listen, match, mergeMatchedParams, navigate, parseSearch, parseSearchInput, parseSearchSync, preload, refresh, resolve, resolveEntry, resolveTo, setOptions, toLocation };
|
|
951
|
+
export { NativeRouterError, NotFoundError, RedirectLoopError, SearchError, back, cancel, commit, commitReplace, create, createHref, forward, getCurrentView, getLocation, getParams, go, initHistoryStack, invalidate, listen, match, mergeMatchedParams, navigate, parseSearch, parseSearchInput, parseSearchSync, preload, refresh, resolve, resolveEntry, resolveTo, setOptions, toLocation };
|
package/dist/types/router.d.ts
CHANGED
|
@@ -88,18 +88,29 @@ export declare function resolveTo<R extends BaseRoute = BaseRoute, V = any>(rout
|
|
|
88
88
|
* levels re-run on every hop(keep side-effectful guards idempotent) —
|
|
89
89
|
* carrying the original user state; at most
|
|
90
90
|
* {@link MAX_REDIRECTS 10} redirects are followed before a
|
|
91
|
-
*
|
|
91
|
+
* An unmatched pathname keeps the
|
|
92
92
|
* {@link resolve resolve} behavior: the task rejects with a
|
|
93
93
|
* {@link NotFoundError} and is routed through `router.errorHandler`.
|
|
94
94
|
*
|
|
95
|
+
* `opts.signal` is the abort signal of the whole chain: guards see it in
|
|
96
|
+
* their {@link GuardContext contexts}, the view task's
|
|
97
|
+
* {@link ResolveViewContext context} carries it on, and it is aborted
|
|
98
|
+
* once the navigation is superseded or cancelled. Standalone callers
|
|
99
|
+
* that pass nothing(e.g. {@link preload}) get a signal that never
|
|
100
|
+
* aborts — their resolution may be shared, so cancelling it on behalf of
|
|
101
|
+
* one consumer is not sound yet.
|
|
102
|
+
*
|
|
95
103
|
* @group Methods
|
|
96
104
|
* @category Router
|
|
97
105
|
* @param router router instance
|
|
98
106
|
* @param location the location to resolve; the object itself is never
|
|
99
107
|
* mutated — a redirect rebinds the resolution to a new location
|
|
108
|
+
* @param opts options; `signal` is the chain's abort signal
|
|
100
109
|
* @returns the terminal location and its resolve task
|
|
101
110
|
*/
|
|
102
|
-
export declare function resolveEntry<R extends BaseRoute = BaseRoute, V = any>(router: RouterInstance<R, V>, location: Location
|
|
111
|
+
export declare function resolveEntry<R extends BaseRoute = BaseRoute, V = any>(router: RouterInstance<R, V>, location: Location, opts?: {
|
|
112
|
+
signal?: AbortSignal;
|
|
113
|
+
}): Promise<ResolvedEntry<V>>;
|
|
103
114
|
/**
|
|
104
115
|
* Resolve a target through the route guards(`redirect`/`beforeLoad`) and
|
|
105
116
|
* cache the result at the router level, keyed by `pathname + search`.
|
|
@@ -147,7 +158,10 @@ export declare function commitReplace<R extends BaseRoute = BaseRoute, V = any>(
|
|
|
147
158
|
* the view resolves; the history entry is committed on the terminal
|
|
148
159
|
* location when guards redirected. The guard phase is part of the
|
|
149
160
|
* cancelable navigation: a superseding navigate or a `cancel()` while
|
|
150
|
-
* guards are still running discards this navigation
|
|
161
|
+
* guards are still running discards this navigation — and aborts the
|
|
162
|
+
* chain's `signal`, so guards and loaders observing it({@link
|
|
163
|
+
* GuardContext.signal}, {@link ResolveViewContext.signal}) stop their
|
|
164
|
+
* requests instead of only having their results dropped.
|
|
151
165
|
* @group Methods
|
|
152
166
|
* @category Router
|
|
153
167
|
* @param router router instance
|
|
@@ -157,7 +171,9 @@ export declare function commitReplace<R extends BaseRoute = BaseRoute, V = any>(
|
|
|
157
171
|
export declare function navigate<R extends BaseRoute = BaseRoute, V = any>(router: RouterInstance<R, V>, to: string, state?: any): Promise<void>;
|
|
158
172
|
/**
|
|
159
173
|
* Refresh the page. Route guards run before the view resolves; a redirect
|
|
160
|
-
* replaces the current entry with the terminal location.
|
|
174
|
+
* replaces the current entry with the terminal location. The refresh is a
|
|
175
|
+
* cancelable navigation chain like {@link navigate}: superseding it or
|
|
176
|
+
* `cancel()` aborts its signal.
|
|
161
177
|
* @group Methods
|
|
162
178
|
* @category Router
|
|
163
179
|
* @param router router instance
|
|
@@ -195,7 +211,8 @@ export declare function back<R extends BaseRoute = BaseRoute, V = any>(router: R
|
|
|
195
211
|
*/
|
|
196
212
|
export declare function createHref<R extends BaseRoute = BaseRoute, V = any>({ baseUrl, history }: RouterInstance<R, V>, to: string): string;
|
|
197
213
|
/**
|
|
198
|
-
* Cancel the current navigate.
|
|
214
|
+
* Cancel the current navigate. The in-flight chain's guards/loaders are
|
|
215
|
+
* aborted through their signal, not merely discarded.
|
|
199
216
|
* @group Methods
|
|
200
217
|
* @category Router
|
|
201
218
|
* @param router router instance
|
|
@@ -213,6 +230,26 @@ export declare function cancel<R extends BaseRoute = BaseRoute, V = any>(router:
|
|
|
213
230
|
* @param router router instance
|
|
214
231
|
*/
|
|
215
232
|
export declare function initHistoryStack<R extends BaseRoute = BaseRoute, V = any>(router: RouterInstance<R, V>): Promise<void>;
|
|
233
|
+
/**
|
|
234
|
+
* Drop every view snapshot of the session window. The already rendered
|
|
235
|
+
* view is untouched — no re-resolve, no re-render; only future POPs
|
|
236
|
+
* change: with no snapshot to hit, {@link listen} falls back to the same
|
|
237
|
+
* lazy re-resolve path as out-of-window entries, so the landed entry's
|
|
238
|
+
* guards(`redirect`/`beforeLoad`) and loaders run again. Call it when
|
|
239
|
+
* the snapshots stop being valid — e.g. right after a logout or an
|
|
240
|
+
* account switch, so a back POP cannot render the previous account's
|
|
241
|
+
* view or bypass guards that already ran in the session.
|
|
242
|
+
*
|
|
243
|
+
* 丢弃会话窗口内的全部视图快照。已渲染的当前视图不受影响——不重解析、
|
|
244
|
+
* 不重渲染;变化的只有后续 POP:无快照可命中时,listen 落入与窗口外条目
|
|
245
|
+
* 相同的惰性重解析路径,落点条目的守卫与加载器重新执行。快照失效时调用
|
|
246
|
+
* ——例如登出/切换账号后,后退 POP 不再渲染上一账号的视图、也不再绕过
|
|
247
|
+
* 会话内已执行过的守卫。
|
|
248
|
+
* @group Methods
|
|
249
|
+
* @category Router
|
|
250
|
+
* @param router router instance
|
|
251
|
+
*/
|
|
252
|
+
export declare function invalidate<R extends BaseRoute = BaseRoute, V = any>(router: RouterInstance<R, V>): void;
|
|
216
253
|
/**
|
|
217
254
|
* Listen the history change.
|
|
218
255
|
* @group Methods
|
package/dist/types/types.d.ts
CHANGED
|
@@ -79,32 +79,61 @@ export type SearchInput = Record<string, string | string[]>;
|
|
|
79
79
|
* @category Route
|
|
80
80
|
*/
|
|
81
81
|
export type SearchOutputOf<S> = S extends StandardSchemaV1<any, infer Output> ? Output : never;
|
|
82
|
+
/** ASCII approximation of path-to-regexp's `ID_Start`. */
|
|
83
|
+
type ParamStartChar = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' | '_' | '$';
|
|
84
|
+
/** ASCII approximation of path-to-regexp's `ID_Continue`. */
|
|
85
|
+
type ParamContinueChar = ParamStartChar | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
|
|
86
|
+
type ParamValueOf<Name extends string, Mode extends 'param' | 'wildcard'> = Mode extends 'wildcard' ? {
|
|
87
|
+
[K in Name]: string[];
|
|
88
|
+
} : {
|
|
89
|
+
[K in Name]: string;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Scan a segment char by char: `\\x` escapes the next char, `:name`
|
|
93
|
+
* starts a param, `*name` starts a wildcard, anything else is static
|
|
94
|
+
* text.
|
|
95
|
+
*/
|
|
96
|
+
type SegmentParamsOf<Seg extends string> = Seg extends `${infer Char}${infer Rest}` ? Char extends '\\' ? Rest extends `${string}${infer Tail}` ? SegmentParamsOf<Tail> : {} : Char extends ':' | '*' ? Rest extends `${infer First}${infer Rest2}` ? First extends ParamStartChar ? ParamNameOf<Rest2, First, Char extends '*' ? 'wildcard' : 'param'> : {} : {} : SegmentParamsOf<Rest> : {};
|
|
97
|
+
/** Consume the identifier run started by a `ParamStartChar`. */
|
|
98
|
+
type ParamNameOf<Rest extends string, Name extends string, Mode extends 'param' | 'wildcard'> = Rest extends `${infer Char}${infer Tail}` ? Char extends ParamContinueChar ? ParamNameOf<Tail, `${Name}${Char}`, Mode> : Char extends '?' | '(' | ')' | '[' | ']' | '+' | '!' | '*' ? {} : ParamValueOf<Name, Mode> & SegmentParamsOf<Rest> : ParamValueOf<Name, Mode>;
|
|
82
99
|
/**
|
|
83
|
-
* Params contributed by a single path segment
|
|
84
|
-
*
|
|
85
|
-
*
|
|
100
|
+
* Params contributed by a single path segment, modeled after the
|
|
101
|
+
* path-to-regexp **8.4.2** string grammar(the version this package
|
|
102
|
+
* locks):
|
|
103
|
+
*
|
|
104
|
+
* - `:name` contributes a required `string` param wherever it appears
|
|
105
|
+
* in the segment — `:id`, `page-:id`, `:from-:to` all work at
|
|
106
|
+
* runtime and are modeled;
|
|
107
|
+
* - `*name` contributes a `string[]` wildcard param(the runtime
|
|
108
|
+
* matcher splits a wildcard value by `/`).
|
|
109
|
+
*
|
|
110
|
+
* Everything else contributes nothing:
|
|
86
111
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
112
|
+
* - the v6-era suffixes `:id?`, `:id+`, `:id*`, `:id(\\d+)` are **not**
|
|
113
|
+
* runtime syntax in 8.4.2 — the matcher throws a `PathError` when the
|
|
114
|
+
* path is compiled, so they are deliberately left unmodeled instead
|
|
115
|
+
* of endorsing a path that crashes;
|
|
116
|
+
* - quoted names(`:"x y"`) and non-ASCII identifier chars are not
|
|
117
|
+
* modeled(the scanner only knows ASCII identifiers).
|
|
118
|
+
*
|
|
119
|
+
* Note: wildcard params surface as `string[]` at runtime while the
|
|
120
|
+
* router-level types(`Matched.params`,
|
|
121
|
+
* {@link GuardContext.params}) are `Record<string, string>` — the
|
|
122
|
+
* router does not model wildcard params.
|
|
91
123
|
* @group Types
|
|
92
124
|
* @category Route
|
|
93
125
|
*/
|
|
94
|
-
export type PathParamsOf<Seg extends string> = Seg
|
|
95
|
-
[K in Name & string]?: string;
|
|
96
|
-
} : Seg extends `:${infer Name}` ? {
|
|
97
|
-
[K in Name & string]: string;
|
|
98
|
-
} : {};
|
|
126
|
+
export type PathParamsOf<Seg extends string> = SegmentParamsOf<Seg>;
|
|
99
127
|
/**
|
|
100
128
|
* Extract the params shape of a route path pattern. Splits the pattern
|
|
101
129
|
* into `/`-separated segments and intersects the params of each, e.g.
|
|
102
|
-
* `ExtractPathParams<'/users/:id/
|
|
103
|
-
* `{id: string} & {
|
|
130
|
+
* `ExtractPathParams<'/users/:id/files/*rest'>` is
|
|
131
|
+
* `{id: string} & {rest: string[]}`.
|
|
104
132
|
*
|
|
105
|
-
* Within the modeled path-to-regexp
|
|
106
|
-
* {@link PathParamsOf});
|
|
107
|
-
*
|
|
133
|
+
* Within the modeled path-to-regexp 8.4.2 syntax scope(see
|
|
134
|
+
* {@link PathParamsOf}); static segments are ignored and v6-era
|
|
135
|
+
* suffixes(`:id?`, `:id(\\d+)`, …) contribute nothing because the
|
|
136
|
+
* runtime matcher rejects them. Distributes over unions of patterns.
|
|
108
137
|
* @group Types
|
|
109
138
|
* @category Route
|
|
110
139
|
*/
|
|
@@ -118,6 +147,13 @@ export type GuardContext<R extends BaseRoute = BaseRoute> = {
|
|
|
118
147
|
router: RouterInstance<R>;
|
|
119
148
|
location: Location;
|
|
120
149
|
params: Record<string, string>;
|
|
150
|
+
/**
|
|
151
|
+
* Aborted when this navigation is superseded by a newer one or
|
|
152
|
+
* cancelled(see {@link RouterInstance.cancelAll cancel}); pass it to
|
|
153
|
+
* the guard's requests(e.g. `fetch(url, {signal})`) so a discarded
|
|
154
|
+
* navigation stops consuming the network.
|
|
155
|
+
*/
|
|
156
|
+
signal: AbortSignal;
|
|
121
157
|
};
|
|
122
158
|
export type BaseRoute<T = any> = {
|
|
123
159
|
path?: Path;
|
|
@@ -147,6 +183,12 @@ export type Matched<R extends BaseRoute = BaseRoute> = {
|
|
|
147
183
|
export type ResolveViewContext<R extends BaseRoute> = {
|
|
148
184
|
router: RouterInstance<R>;
|
|
149
185
|
location: Location;
|
|
186
|
+
/**
|
|
187
|
+
* The navigation chain's abort signal: aborted when this navigation is
|
|
188
|
+
* superseded by a newer one or cancelled. Frameworks forward it into
|
|
189
|
+
* their data contexts so loaders can abort their requests.
|
|
190
|
+
*/
|
|
191
|
+
signal: AbortSignal;
|
|
150
192
|
};
|
|
151
193
|
export type ResolveView<R extends BaseRoute, V> = (matched: Matched<R>[], ctx: ResolveViewContext<R>) => Promise<V>;
|
|
152
194
|
export type Options<V> = {
|
|
@@ -191,3 +233,4 @@ export type RouterInstance<R extends BaseRoute, V = any> = {
|
|
|
191
233
|
cancelAll(): void;
|
|
192
234
|
resolving?: Location;
|
|
193
235
|
} & RequiredOf<Options<V>, 'baseUrl' | 'maxStackDepth'>;
|
|
236
|
+
export {};
|