@native-router/core 1.5.0 → 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 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.
@@ -59,6 +71,7 @@ commit(router, entry.task, entry.location); // commit like a click
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
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
@@ -112,8 +112,15 @@ function create(routes, history, resolveView, options) {
112
112
  resolveView,
113
113
  history: instanceHistory,
114
114
  locationStack,
115
- // The view stack is window-relative, so it is exactly as long as the
116
- // location window and stays bounded by maxStackDepth with it.
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() 丢弃这些快照。
117
124
  viewStack: new Array(locationStack.length).fill(null),
118
125
  baseIndex,
119
126
  preloadCache: new Map(),
@@ -753,6 +760,31 @@ function initHistoryStack(router) {
753
760
  });
754
761
  }
755
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
+
756
788
  /**
757
789
  * Listen the history change.
758
790
  * @group Methods
@@ -934,6 +966,7 @@ exports.getLocation = getLocation;
934
966
  exports.getParams = getParams;
935
967
  exports.go = go;
936
968
  exports.initHistoryStack = initHistoryStack;
969
+ exports.invalidate = invalidate;
937
970
  exports.listen = listen;
938
971
  exports.match = match;
939
972
  exports.mergeMatchedParams = mergeMatchedParams;
package/dist/index.mjs CHANGED
@@ -110,8 +110,15 @@ function create(routes, history, resolveView, options) {
110
110
  resolveView,
111
111
  history: instanceHistory,
112
112
  locationStack,
113
- // The view stack is window-relative, so it is exactly as long as the
114
- // location window and stays bounded by maxStackDepth with it.
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() 丢弃这些快照。
115
122
  viewStack: new Array(locationStack.length).fill(null),
116
123
  baseIndex,
117
124
  preloadCache: new Map(),
@@ -751,6 +758,31 @@ function initHistoryStack(router) {
751
758
  });
752
759
  }
753
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
+
754
786
  /**
755
787
  * Listen the history change.
756
788
  * @group Methods
@@ -916,4 +948,4 @@ function isThenable(value) {
916
948
  return typeof value?.then === 'function';
917
949
  }
918
950
 
919
- 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 };
@@ -230,6 +230,26 @@ export declare function cancel<R extends BaseRoute = BaseRoute, V = any>(router:
230
230
  * @param router router instance
231
231
  */
232
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;
233
253
  /**
234
254
  * Listen the history change.
235
255
  * @group Methods
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@native-router/core",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "exports": {
5
5
  ".": {
6
6
  "types": "./dist/types/index.d.ts",