@real-router/sources 0.7.2 → 0.8.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/dist/cjs/index.d.ts +26 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.mts +26 -2
- package/dist/esm/index.d.mts.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/createActiveRouteSource.ts +83 -6
- package/src/normalizeActiveOptions.ts +19 -7
- package/src/stabilizeState.ts +26 -6
- package/src/types.ts +14 -0
package/dist/cjs/index.d.ts
CHANGED
|
@@ -17,6 +17,20 @@ interface RouterSource<T> {
|
|
|
17
17
|
interface ActiveRouteSourceOptions {
|
|
18
18
|
strict?: boolean;
|
|
19
19
|
ignoreQueryParams?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* URL fragment match (#532). When defined, the source is active iff the
|
|
22
|
+
* route matches AND `state.context.url.hash` (decoded, populated by
|
|
23
|
+
* browser/navigation URL plugins) equals this value:
|
|
24
|
+
*
|
|
25
|
+
* - `undefined` (default): hash is ignored — legacy route-only matching.
|
|
26
|
+
* - `""`: active only when the current URL has no fragment (or empty).
|
|
27
|
+
* - `"value"`: active only when the current fragment equals `"value"`.
|
|
28
|
+
*
|
|
29
|
+
* Hash-plugin runtimes leave `state.context.url` undefined, so any non-
|
|
30
|
+
* undefined `hash` option will produce `false` there — consistent with the
|
|
31
|
+
* documented limitation that hash-plugin doesn't support URL fragments.
|
|
32
|
+
*/
|
|
33
|
+
hash?: string;
|
|
20
34
|
}
|
|
21
35
|
interface RouterTransitionSnapshot {
|
|
22
36
|
isTransitioning: boolean;
|
|
@@ -193,19 +207,29 @@ interface ActiveNameSelector {
|
|
|
193
207
|
declare function createActiveNameSelector(router: Router): ActiveNameSelector;
|
|
194
208
|
//#endregion
|
|
195
209
|
//#region src/normalizeActiveOptions.d.ts
|
|
210
|
+
/**
|
|
211
|
+
* Normalized options shape — booleans are required (filled with defaults),
|
|
212
|
+
* `hash` stays `string | undefined` because `undefined` is the meaningful
|
|
213
|
+
* "ignore hash" sentinel that callers pass intentionally.
|
|
214
|
+
*/
|
|
215
|
+
interface NormalizedActiveOptions {
|
|
216
|
+
strict: boolean;
|
|
217
|
+
ignoreQueryParams: boolean;
|
|
218
|
+
hash: string | undefined;
|
|
219
|
+
}
|
|
196
220
|
/**
|
|
197
221
|
* Default options for `createActiveRouteSource` and adapter-level helpers.
|
|
198
222
|
*
|
|
199
223
|
* Frozen to prevent accidental mutation by consumers.
|
|
200
224
|
*/
|
|
201
|
-
declare const DEFAULT_ACTIVE_OPTIONS: Readonly<
|
|
225
|
+
declare const DEFAULT_ACTIVE_OPTIONS: Readonly<NormalizedActiveOptions>;
|
|
202
226
|
/**
|
|
203
227
|
* Normalizes partial `ActiveRouteSourceOptions` into a fully-defaulted object.
|
|
204
228
|
*
|
|
205
229
|
* Use this to produce a stable options record for comparison, caching, or
|
|
206
230
|
* downstream consumers that require all fields present.
|
|
207
231
|
*/
|
|
208
|
-
declare function normalizeActiveOptions(options?: ActiveRouteSourceOptions):
|
|
232
|
+
declare function normalizeActiveOptions(options?: ActiveRouteSourceOptions): NormalizedActiveOptions;
|
|
209
233
|
//#endregion
|
|
210
234
|
//#region src/canonicalJson.d.ts
|
|
211
235
|
/**
|
package/dist/cjs/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/types.ts","../../src/createRouteSource.ts","../../src/createRouteNodeSource.ts","../../src/createActiveRouteSource.ts","../../src/createTransitionSource.ts","../../src/createErrorSource.ts","../../src/createDismissableError.ts","../../src/createActiveNameSelector.ts","../../src/normalizeActiveOptions.ts","../../src/canonicalJson.ts"],"mappings":";;;UAEiB,aAAA,WAAwB,MAAA,GAAS,MAAA;EAChD,KAAA,EAAO,KAAA,CAAM,CAAA;EACb,aAAA,EAAe,KAAA;AAAA;AAAA,UAGA,iBAAA,WAA4B,MAAA,GAAS,MAAA;EACpD,KAAA,EAAO,KAAA,CAAM,CAAA;EACb,aAAA,EAAe,KAAA;AAAA;AAAA,UAGA,YAAA;EACf,SAAA,GAAY,QAAA;EACZ,WAAA,QAAmB,CAAA;EACnB,OAAA;AAAA;AAAA,UAGe,wBAAA;EACf,MAAA;EACA,iBAAA;AAAA;AAAA,UAGe,wBAAA;EACf,eAAA;EACA,eAAA;EACA,OAAA,EAAS,KAAA;EACT,SAAA,EAAW,KAAA;AAAA;AAAA,UAGI,mBAAA;EACf,KAAA,EAAO,WAAA;EACP,OAAA,EAAS,KAAA;EACT,SAAA,EAAW,KAAA;EACX,OAAA;AAAA;AAAA,UAGe,wBAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/types.ts","../../src/createRouteSource.ts","../../src/createRouteNodeSource.ts","../../src/createActiveRouteSource.ts","../../src/createTransitionSource.ts","../../src/createErrorSource.ts","../../src/createDismissableError.ts","../../src/createActiveNameSelector.ts","../../src/normalizeActiveOptions.ts","../../src/canonicalJson.ts"],"mappings":";;;UAEiB,aAAA,WAAwB,MAAA,GAAS,MAAA;EAChD,KAAA,EAAO,KAAA,CAAM,CAAA;EACb,aAAA,EAAe,KAAA;AAAA;AAAA,UAGA,iBAAA,WAA4B,MAAA,GAAS,MAAA;EACpD,KAAA,EAAO,KAAA,CAAM,CAAA;EACb,aAAA,EAAe,KAAA;AAAA;AAAA,UAGA,YAAA;EACf,SAAA,GAAY,QAAA;EACZ,WAAA,QAAmB,CAAA;EACnB,OAAA;AAAA;AAAA,UAGe,wBAAA;EACf,MAAA;EACA,iBAAA;EAjBO;;;;;;AAIT;;;;;;;EA2BE,IAAA;AAAA;AAAA,UAGe,wBAAA;EACf,eAAA;EACA,eAAA;EACA,OAAA,EAAS,KAAA;EACT,SAAA,EAAW,KAAA;AAAA;AAAA,UAGI,mBAAA;EACf,KAAA,EAAO,WAAA;EACP,OAAA,EAAS,KAAA;EACT,SAAA,EAAW,KAAA;EACX,OAAA;AAAA;AAAA,UAGe,wBAAA;EAvCY;EAyC3B,KAAA,EAAO,WAAA;EAzCqB;EA2C5B,OAAA,EAAS,KAAA;EA1CG;EA4CZ,SAAA,EAAW,KAAA;EA3CQ;EA6CnB,OAAA;EA5CO;EA8CP,UAAA;AAAA;;;;AA3DF;;;;;;iBCWgB,iBAAA,CAAkB,MAAA,EAAQ,MAAA,GAAS,YAAA,CAAa,aAAA;;;;ADXhE;;;;;;;;;;;;;;;;iBE0BgB,qBAAA,CACd,MAAA,EAAQ,MAAA,EACR,QAAA,WACC,YAAA,CAAa,iBAAA;;;;AF7BhB;;;;;;;;;;;;;;;;iBG6BgB,uBAAA,CACd,MAAA,EAAQ,MAAA,EACR,SAAA,UACA,MAAA,GAAS,MAAA,EACT,OAAA,GAAU,wBAAA,GACT,YAAA;;;iBCfa,sBAAA,CACd,MAAA,EAAQ,MAAA,GACP,YAAA,CAAa,wBAAA;AJrBhB;;;;;;;;;;;;;;AAAA,iBI8FgB,mBAAA,CACd,MAAA,EAAQ,MAAA,GACP,YAAA,CAAa,wBAAA;;;iBC9EA,iBAAA,CACd,MAAA,EAAQ,MAAA,GACP,YAAA,CAAa,mBAAA;ALpBhB;;;;;;;;;;;;;;AAAA,iBKoFgB,cAAA,CACd,MAAA,EAAQ,MAAA,GACP,YAAA,CAAa,mBAAA;;;;ALtFhB;;;;;;;;;;;;;;;;;;iBM4BgB,sBAAA,CACd,MAAA,EAAQ,MAAA,GACP,YAAA,CAAa,wBAAA;;;UC5BC,kBAAA;;APFjB;;;;EOQE,SAAA,GAAY,SAAA,UAAmB,QAAA;EPPlB;;;;EOYb,QAAA,GAAW,SAAA;EPbkB;EOe7B,OAAA;AAAA;;;;;;;;APVF;;;;;;;;;;;;;;;;iBOsCgB,wBAAA,CAAyB,MAAA,EAAQ,MAAA,GAAS,kBAAA;;;;;AP3C1D;;;UQKiB,uBAAA;EACf,MAAA;EACA,iBAAA;EACA,IAAA;AAAA;;;;;;cAQW,sBAAA,EAAwB,QAAA,CAAS,uBAAA;;;;;;;iBAa9B,sBAAA,CACd,OAAA,GAAU,wBAAA,GACT,uBAAA;;;;;;AR/BH;;;;;;;;;;;;;iBScgB,aAAA,CAAc,KAAA"}
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@real-router/route-utils`),t=require(`@real-router/core`),n=require(`@real-router/core/api`);var r=class{#e;#t=!1;#n=new Set;#r;#i;#a;constructor(e,t){this.#e=e,this.#r=t?.onFirstSubscribe,this.#i=t?.onLastUnsubscribe,this.#a=t?.onDestroy,this.subscribe=this.subscribe.bind(this),this.getSnapshot=this.getSnapshot.bind(this),this.destroy=this.destroy.bind(this)}subscribe(e){if(this.#t)return()=>{};let t=this.#n.size===0;return this.#n.add(e),t&&this.#r&&this.#r(),()=>{this.#n.delete(e),!this.#t&&this.#n.size===0&&this.#i&&this.#i()}}getSnapshot(){return this.#e}updateSnapshot(e){this.#t||(this.#e=e,this.#n.forEach(e=>{e()}))}destroy(){this.#t||(this.#t=!0,this.#a?.(),this.#n.clear())}};function i(e,t){return e===t
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@real-router/route-utils`),t=require(`@real-router/core`),n=require(`@real-router/core/api`);var r=class{#e;#t=!1;#n=new Set;#r;#i;#a;constructor(e,t){this.#e=e,this.#r=t?.onFirstSubscribe,this.#i=t?.onLastUnsubscribe,this.#a=t?.onDestroy,this.subscribe=this.subscribe.bind(this),this.getSnapshot=this.getSnapshot.bind(this),this.destroy=this.destroy.bind(this)}subscribe(e){if(this.#t)return()=>{};let t=this.#n.size===0;return this.#n.add(e),t&&this.#r&&this.#r(),()=>{this.#n.delete(e),!this.#t&&this.#n.size===0&&this.#i&&this.#i()}}getSnapshot(){return this.#e}updateSnapshot(e){this.#t||(this.#e=e,this.#n.forEach(e=>{e()}))}destroy(){this.#t||(this.#t=!0,this.#a?.(),this.#n.clear())}};function i(e,t){return e===t?e:e?.path!==t?.path||a(e)!==a(t)?t:e}function a(e){return(e?.context)?.url?.hash}function o(e){let t=null,n=()=>{let e=t;t=null,e?.()},a=new r({route:e.getState(),previousRoute:void 0},{onFirstSubscribe:()=>{t=e.subscribe(e=>{let t=a.getSnapshot(),n=i(t.route,e.route),r=i(t.previousRoute,e.previousRoute);(n!==t.route||r!==t.previousRoute)&&a.updateSnapshot({route:n,previousRoute:r})})},onLastUnsubscribe:n,onDestroy:n});return a}function s(e,t,n,r){let a=r?.route??t.getState(),o=r?.previousRoute,s=n===``||a!==void 0&&(a.name===n||a.name.startsWith(`${n}.`))?a:void 0;if(s===e.route&&o===e.previousRoute)return e;let c=i(e.route,s),l=i(e.previousRoute,o);return c===e.route&&l===e.previousRoute?e:{route:c,previousRoute:l}}const c=new WeakMap;function l(e,t){let n=c.get(e);n||(n=new Map,c.set(e,n));let r=n.get(t);if(!r){let i=u(e,t);r={subscribe:i.subscribe,getSnapshot:i.getSnapshot,destroy:d},n.set(t,r)}return r}function u(e,t){let n=null,i=e.shouldUpdateNode(t),a=new r(s({route:void 0,previousRoute:void 0},e,t),{onFirstSubscribe:()=>{let r=s(a.getSnapshot(),e,t);Object.is(r,a.getSnapshot())||a.updateSnapshot(r),n=e.subscribe(n=>{if(!i(n.route,n.previousRoute))return;let r=s(a.getSnapshot(),e,t,n);Object.is(a.getSnapshot(),r)||a.updateSnapshot(r)})},onLastUnsubscribe:()=>{let e=n;n=null,e?.()}});return a}function d(){}function f(e){return JSON.stringify(e,p)}function p(e,t){if(typeof t==`object`&&t&&!Array.isArray(t)){let e={},n=Object.keys(t).toSorted((e,t)=>e.localeCompare(t));for(let r of n)e[r]=t[r];return e}return t}const m=Object.freeze({strict:!1,ignoreQueryParams:!0,hash:void 0});function h(e){return{strict:e?.strict??m.strict,ignoreQueryParams:e?.ignoreQueryParams??m.ignoreQueryParams,hash:e?.hash}}const g=new WeakMap;function _(e,t,n,r){let{strict:i,ignoreQueryParams:a,hash:o}=h(r),s;try{let e=o===void 0?``:`#${o}`;s=`${t}|${f(n)}|${String(i)}|${String(a)}|${e}`}catch{s=void 0}if(s===void 0){let r=b(e,t,n,i,a,o);return{subscribe:r.subscribe,getSnapshot:r.getSnapshot,destroy:x}}let c=g.get(e);c||(c=new Map,g.set(e,c));let l=c.get(s);if(!l){let r=b(e,t,n,i,a,o);l={subscribe:r.subscribe,getSnapshot:r.getSnapshot,destroy:x},c.set(s,l)}return l}function v(e){return(e.getState()?.context)?.url?.hash??``}function y(e,t,n,r,i,a){return e.isActiveRoute(t,n,r,i)?a===void 0?!0:v(e)===a:!1}function b(t,n,i,a,o,s){let c=new r(y(t,n,i,a,o,s));return t.subscribe(r=>{let l=(0,e.areRoutesRelated)(n,r.route.name),u=r.previousRoute&&(0,e.areRoutesRelated)(n,r.previousRoute.name),d=s!==void 0&&(r.route.context?.url?.hashChanged??!1);if(!l&&!u&&!d)return;let f=l?y(t,n,i,a,o,s):!1;Object.is(c.getSnapshot(),f)||c.updateSnapshot(f)}),c}function x(){}const S={isTransitioning:!1,isLeaveApproved:!1,toRoute:null,fromRoute:null},C=new WeakMap;function w(e){let a=new r(S,{onDestroy:()=>{c.forEach(e=>{e()})}}),o=(0,n.getPluginApi)(e),s=()=>{a.updateSnapshot(S)},c=[o.addEventListener(t.events.TRANSITION_START,(e,t)=>{let n=a.getSnapshot(),r=i(n.toRoute,e),o=i(n.fromRoute,t??null);(!n.isTransitioning||r!==n.toRoute||o!==n.fromRoute)&&a.updateSnapshot({isTransitioning:!0,isLeaveApproved:!1,toRoute:r,fromRoute:o})}),o.addEventListener(t.events.TRANSITION_LEAVE_APPROVE,(e,t)=>{let n=a.getSnapshot();a.updateSnapshot({isTransitioning:!0,isLeaveApproved:!0,toRoute:i(n.toRoute,e),fromRoute:i(n.fromRoute,t??null)})}),o.addEventListener(t.events.TRANSITION_SUCCESS,s),o.addEventListener(t.events.TRANSITION_ERROR,s),o.addEventListener(t.events.TRANSITION_CANCEL,s)];return a}function T(e){let t=C.get(e);if(!t){let n=w(e);t={subscribe:n.subscribe,getSnapshot:n.getSnapshot,destroy:E},C.set(e,t)}return t}function E(){}const D={error:null,toRoute:null,fromRoute:null,version:0},O=new WeakMap;function k(e){let i=0,a=new r(D,{onDestroy:()=>{s.forEach(e=>{e()})}}),o=(0,n.getPluginApi)(e),s=[o.addEventListener(t.events.TRANSITION_ERROR,(e,t,n)=>{i++,a.updateSnapshot({error:n,toRoute:e??null,fromRoute:t??null,version:i})}),o.addEventListener(t.events.TRANSITION_SUCCESS,()=>{a.getSnapshot().error!==null&&a.updateSnapshot({error:null,toRoute:null,fromRoute:null,version:i})})];return a}function A(e){let t=O.get(e);if(!t){let n=k(e);t={subscribe:n.subscribe,getSnapshot:n.getSnapshot,destroy:j},O.set(e,t)}return t}function j(){}const M=new WeakMap;function N(e){let t=M.get(e);if(t)return t;let n=A(e),i=-1,a=()=>{let e=n.getSnapshot(),t=e.version<=i;return{error:t?null:e.error,toRoute:t?null:e.toRoute,fromRoute:t?null:e.fromRoute,version:e.version,resetError:c}},o=new r(a(),{onFirstSubscribe:()=>{s=n.subscribe(()=>{o.updateSnapshot(a())})},onLastUnsubscribe:()=>{l()}}),s=null;function c(){i=n.getSnapshot().version,o.updateSnapshot(a())}function l(){let e=s;s=null,e?.()}let u={subscribe:o.subscribe,getSnapshot:o.getSnapshot,destroy:P};return M.set(e,u),u}function P(){}const F=new WeakMap;function I(t){let n=F.get(t);if(n)return n;let r=new Map,i=new Map,a=null,o=e=>{let n=t.getState();return n?n.name===e||n.name.startsWith(`${e}.`):!1},s=()=>{a=t.subscribe(t=>{for(let[n,a]of r){let r=(0,e.areRoutesRelated)(n,t.route.name),s=t.previousRoute&&(0,e.areRoutesRelated)(n,t.previousRoute.name);if(!r&&!s)continue;let c=i.get(n)===!0,l=o(n);if(c!==l){i.set(n,l);for(let e of a)e()}}})},c=()=>{let e=a;a=null,e?.()},l={subscribe:(e,t)=>{let n=r.get(e);n||(n=new Set,r.set(e,n),i.set(e,o(e))),n.add(t),a||s();let l=!1;return()=>{l||(l=!0,n.delete(t),n.size===0&&(r.delete(e),i.delete(e)),r.size===0&&c())}},isActive:e=>{let t=i.get(e);return t===void 0?o(e):t},destroy:L};return F.set(t,l),l}function L(){}exports.DEFAULT_ACTIVE_OPTIONS=m,exports.canonicalJson=f,exports.createActiveNameSelector=I,exports.createActiveRouteSource=_,exports.createDismissableError=N,exports.createErrorSource=k,exports.createRouteNodeSource=l,exports.createRouteSource=o,exports.createTransitionSource=w,exports.getErrorSource=A,exports.getTransitionSource=T,exports.normalizeActiveOptions=h;
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/cjs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["#listeners","#onFirstSubscribe","#onLastUnsubscribe","#onDestroy","#currentSnapshot","#destroyed","noopDestroy","noopDestroy","events","noopDestroy","events","noopDestroy","noopDestroy"],"sources":["../../src/BaseSource.ts","../../src/stabilizeState.ts","../../src/createRouteSource.ts","../../src/computeSnapshot.ts","../../src/createRouteNodeSource.ts","../../src/canonicalJson.ts","../../src/normalizeActiveOptions.ts","../../src/createActiveRouteSource.ts","../../src/createTransitionSource.ts","../../src/createErrorSource.ts","../../src/createDismissableError.ts","../../src/createActiveNameSelector.ts"],"sourcesContent":["export interface BaseSourceOptions {\n onFirstSubscribe?: () => void;\n onLastUnsubscribe?: () => void;\n onDestroy?: () => void;\n}\n\nexport class BaseSource<T> {\n #currentSnapshot: T;\n #destroyed = false;\n\n readonly #listeners = new Set<() => void>();\n readonly #onFirstSubscribe: (() => void) | undefined;\n readonly #onLastUnsubscribe: (() => void) | undefined;\n readonly #onDestroy: (() => void) | undefined;\n\n constructor(initialSnapshot: T, options?: BaseSourceOptions) {\n this.#currentSnapshot = initialSnapshot;\n this.#onFirstSubscribe = options?.onFirstSubscribe;\n this.#onLastUnsubscribe = options?.onLastUnsubscribe;\n this.#onDestroy = options?.onDestroy;\n\n this.subscribe = this.subscribe.bind(this);\n this.getSnapshot = this.getSnapshot.bind(this);\n this.destroy = this.destroy.bind(this);\n }\n\n subscribe(listener: () => void): () => void {\n if (this.#destroyed) {\n return () => {};\n }\n\n const wasFirst = this.#listeners.size === 0;\n\n // Add listener BEFORE onFirstSubscribe so that if the reconciliation in\n // onFirstSubscribe calls updateSnapshot(), this listener receives the\n // notification. Critical for useSyncExternalStore in adapters — without\n // this the post-reconnection snapshot is missed and consumers render\n // stale data. (See Preact RouteView nested remount test.)\n this.#listeners.add(listener);\n\n if (wasFirst && this.#onFirstSubscribe) {\n this.#onFirstSubscribe();\n }\n\n return () => {\n this.#listeners.delete(listener);\n\n if (\n !this.#destroyed &&\n this.#listeners.size === 0 &&\n this.#onLastUnsubscribe\n ) {\n this.#onLastUnsubscribe();\n }\n };\n }\n\n getSnapshot(): T {\n return this.#currentSnapshot;\n }\n\n updateSnapshot(snapshot: T): void {\n /* v8 ignore next 2 -- @preserve: defensive guard unreachable via public API (destroy() removes router subscription first) */\n if (this.#destroyed) {\n return;\n }\n\n this.#currentSnapshot = snapshot;\n this.#listeners.forEach((listener) => {\n listener();\n });\n }\n\n destroy(): void {\n if (this.#destroyed) {\n return;\n }\n\n this.#destroyed = true;\n this.#onDestroy?.();\n this.#listeners.clear();\n }\n}\n","import type { State } from \"@real-router/core\";\n\n/**\n * State-aware stabilization for route snapshots.\n *\n * Compares `path` — the canonical representation of rendering-relevant\n * State fields (name + params). When path matches, returns `prev`\n * (preserving reference), enabling frameworks to skip re-renders.\n *\n * Ignores `meta` (internal: auto-increment id) and `transition`\n * (reference data: from, segments, reload) — they don't affect\n * what is rendered.\n *\n * Accepts `null` for compatibility with `RouterTransitionSnapshot`\n * (toRoute/fromRoute are `State | null`).\n *\n * @internal Not exported from package public API.\n */\nexport function stabilizeState<T extends State | null | undefined>(\n prev: T,\n next: T,\n): T {\n if (prev === next) {\n return prev;\n }\n if (prev?.path !== next?.path) {\n return next;\n }\n\n return prev;\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { stabilizeState } from \"./stabilizeState.js\";\n\nimport type { RouteSnapshot, RouterSource } from \"./types.js\";\nimport type { Router } from \"@real-router/core\";\n\n/**\n * Creates a source for the full route state.\n *\n * Uses a lazy-connection pattern: the router subscription is created when the\n * first listener subscribes and removed when the last listener unsubscribes.\n * This is compatible with React's useSyncExternalStore and Strict Mode.\n */\nexport function createRouteSource(router: Router): RouterSource<RouteSnapshot> {\n let routerUnsubscribe: (() => void) | null = null;\n\n const disconnect = (): void => {\n const unsub = routerUnsubscribe;\n\n routerUnsubscribe = null;\n unsub?.();\n };\n\n const source = new BaseSource<RouteSnapshot>(\n {\n route: router.getState(),\n previousRoute: undefined,\n },\n {\n onFirstSubscribe: () => {\n routerUnsubscribe = router.subscribe((next) => {\n const prev = source.getSnapshot();\n const newRoute = stabilizeState(prev.route, next.route);\n const newPreviousRoute = stabilizeState(\n prev.previousRoute,\n next.previousRoute,\n );\n\n if (\n newRoute !== prev.route ||\n newPreviousRoute !== prev.previousRoute\n ) {\n source.updateSnapshot({\n route: newRoute,\n previousRoute: newPreviousRoute,\n });\n }\n });\n },\n onLastUnsubscribe: disconnect,\n onDestroy: disconnect,\n },\n );\n\n return source;\n}\n","import { stabilizeState } from \"./stabilizeState.js\";\n\nimport type { RouteNodeSnapshot } from \"./types.js\";\nimport type { Router, SubscribeState } from \"@real-router/core\";\n\nexport function computeSnapshot(\n currentSnapshot: RouteNodeSnapshot,\n router: Router,\n nodeName: string,\n next?: SubscribeState,\n): RouteNodeSnapshot {\n const currentRoute = next?.route ?? router.getState();\n const previousRoute = next?.previousRoute;\n\n const isNodeActive =\n nodeName === \"\" ||\n (currentRoute !== undefined &&\n (currentRoute.name === nodeName ||\n currentRoute.name.startsWith(`${nodeName}.`)));\n\n const route = isNodeActive ? currentRoute : undefined;\n\n if (\n route === currentSnapshot.route &&\n previousRoute === currentSnapshot.previousRoute\n ) {\n return currentSnapshot;\n }\n\n const newRoute = stabilizeState(currentSnapshot.route, route);\n const newPreviousRoute = stabilizeState(\n currentSnapshot.previousRoute,\n previousRoute,\n );\n\n if (\n newRoute === currentSnapshot.route &&\n newPreviousRoute === currentSnapshot.previousRoute\n ) {\n return currentSnapshot;\n }\n\n return { route: newRoute, previousRoute: newPreviousRoute };\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { computeSnapshot } from \"./computeSnapshot.js\";\n\nimport type { RouteNodeSnapshot, RouterSource } from \"./types.js\";\nimport type { Router } from \"@real-router/core\";\n\nconst nodeSourceCache = new WeakMap<\n Router,\n Map<string, RouterSource<RouteNodeSnapshot>>\n>();\n\n/**\n * Creates a source scoped to a specific route node.\n *\n * **Per-router + per-nodeName cache:** repeated calls with the same\n * `(router, nodeName)` return the same shared instance. `N` consumers\n * calling `createRouteNodeSource(r, \"users\")` produce one router subscription\n * shared across all of them.\n *\n * Uses a lazy-connection pattern: the router subscription is created when the\n * first listener subscribes and removed when the last listener unsubscribes.\n * This is compatible with React's useSyncExternalStore and Strict Mode.\n *\n * `destroy()` on the returned source is a no-op — the shared instance lives\n * as long as the router itself (the WeakMap entry releases automatically on\n * router GC). Callers that need an isolated instance with working teardown\n * can use `buildRouteNodeSource` internally (not exported).\n */\nexport function createRouteNodeSource(\n router: Router,\n nodeName: string,\n): RouterSource<RouteNodeSnapshot> {\n let perRouter = nodeSourceCache.get(router);\n\n if (!perRouter) {\n perRouter = new Map();\n nodeSourceCache.set(router, perRouter);\n }\n\n let cached = perRouter.get(nodeName);\n\n if (!cached) {\n const source = buildRouteNodeSource(router, nodeName);\n\n // Wrap with no-op destroy. The shared source lives with the router.\n cached = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n perRouter.set(nodeName, cached);\n }\n\n return cached;\n}\n\nfunction buildRouteNodeSource(\n router: Router,\n nodeName: string,\n): RouterSource<RouteNodeSnapshot> {\n let routerUnsubscribe: (() => void) | null = null;\n\n // Built once per cached source instance; safe — createRouteNodeSource is\n // itself per-(router, nodeName) cached, so shouldUpdate is called once.\n const shouldUpdate = router.shouldUpdateNode(nodeName);\n\n const initialSnapshot: RouteNodeSnapshot = {\n route: undefined,\n previousRoute: undefined,\n };\n\n const disconnect = (): void => {\n const unsub = routerUnsubscribe;\n\n routerUnsubscribe = null;\n unsub?.();\n };\n\n const source = new BaseSource<RouteNodeSnapshot>(\n computeSnapshot(initialSnapshot, router, nodeName),\n {\n onFirstSubscribe: () => {\n // Reconcile snapshot with current router state before connecting.\n // Covers reconnection after Activity hide/show cycles where the\n // source was disconnected and missed navigation events.\n const reconciled = computeSnapshot(\n source.getSnapshot(),\n router,\n nodeName,\n );\n\n if (!Object.is(reconciled, source.getSnapshot())) {\n source.updateSnapshot(reconciled);\n }\n\n // Connect to router on first subscription\n routerUnsubscribe = router.subscribe((next) => {\n if (!shouldUpdate(next.route, next.previousRoute)) {\n return;\n }\n\n const newSnapshot = computeSnapshot(\n source.getSnapshot(),\n router,\n nodeName,\n next,\n );\n\n if (!Object.is(source.getSnapshot(), newSnapshot)) {\n source.updateSnapshot(newSnapshot);\n }\n });\n },\n onLastUnsubscribe: disconnect,\n },\n );\n\n return source;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","/**\n * Serializes a value into a stable JSON string — object keys are sorted at\n * every level so that `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` produce the same\n * output.\n *\n * Used as a cache key for `createActiveRouteSource` so that equivalent params\n * objects share the same cached source regardless of key order.\n *\n * Edge cases:\n * - Arrays preserve order (canonical: index-ordered already).\n * - `undefined` values are dropped (standard JSON behaviour).\n * - `Symbol`, `BigInt`, `Date`, `Map`, `Set` etc. fall through to\n * `JSON.stringify` defaults — `Symbol` becomes `undefined`, `BigInt` throws.\n * In practice, route params carry primitives (`string | number | boolean`)\n * and such edge cases would hit a fresh source on cache miss — acceptable.\n */\nexport function canonicalJson(value: unknown): string {\n return JSON.stringify(value, replacer);\n}\n\nfunction replacer(_key: string, val: unknown): unknown {\n if (val !== null && typeof val === \"object\" && !Array.isArray(val)) {\n const sorted: Record<string, unknown> = {};\n const keys = Object.keys(val as Record<string, unknown>).toSorted(\n (left, right) => left.localeCompare(right),\n );\n\n for (const key of keys) {\n sorted[key] = (val as Record<string, unknown>)[key];\n }\n\n return sorted;\n }\n\n return val;\n}\n","import type { ActiveRouteSourceOptions } from \"./types.js\";\n\n/**\n * Default options for `createActiveRouteSource` and adapter-level helpers.\n *\n * Frozen to prevent accidental mutation by consumers.\n */\nexport const DEFAULT_ACTIVE_OPTIONS: Readonly<\n Required<ActiveRouteSourceOptions>\n> = Object.freeze({\n strict: false,\n ignoreQueryParams: true,\n});\n\n/**\n * Normalizes partial `ActiveRouteSourceOptions` into a fully-defaulted object.\n *\n * Use this to produce a stable options record for comparison, caching, or\n * downstream consumers that require all fields present.\n */\nexport function normalizeActiveOptions(\n options?: ActiveRouteSourceOptions,\n): Required<ActiveRouteSourceOptions> {\n return {\n strict: options?.strict ?? DEFAULT_ACTIVE_OPTIONS.strict,\n ignoreQueryParams:\n options?.ignoreQueryParams ?? DEFAULT_ACTIVE_OPTIONS.ignoreQueryParams,\n };\n}\n","import { areRoutesRelated } from \"@real-router/route-utils\";\n\nimport { BaseSource } from \"./BaseSource\";\nimport { canonicalJson } from \"./canonicalJson.js\";\nimport { normalizeActiveOptions } from \"./normalizeActiveOptions.js\";\n\nimport type { ActiveRouteSourceOptions, RouterSource } from \"./types.js\";\nimport type { Params, Router } from \"@real-router/core\";\n\nconst activeSourceCache = new WeakMap<\n Router,\n Map<string, RouterSource<boolean>>\n>();\n\n/**\n * Creates a source tracking whether a route (with given params/options) is active.\n *\n * **Per-router + canonical-args cache:** repeated calls with equivalent\n * arguments return the same shared instance. Param key order doesn't matter\n * (`{ a:1, b:2 }` and `{ b:2, a:1 }` hit the same cache entry via\n * `canonicalJson`).\n *\n * `destroy()` is a no-op — shared sources live with the router. The router\n * subscription stays active while any consumer subscribes; when the router\n * is garbage-collected, the WeakMap entry releases automatically.\n *\n * Edge cases: `Symbol`/`BigInt` in params bypass `canonicalJson` and produce\n * an unstable cache key — these will simply miss the cache and create a new\n * source on each call. Practical params are primitives, so this is not a\n * concern in real usage.\n */\nexport function createActiveRouteSource(\n router: Router,\n routeName: string,\n params?: Params,\n options?: ActiveRouteSourceOptions,\n): RouterSource<boolean> {\n const { strict, ignoreQueryParams } = normalizeActiveOptions(options);\n\n // BigInt/Symbol/circular refs cannot be serialized — fall back to creating\n // a fresh (non-cached) source. Callers pass these edge-case params rarely;\n // the extra allocation is acceptable.\n let key: string | undefined;\n\n try {\n key = `${routeName}|${canonicalJson(params)}|${String(strict)}|${String(ignoreQueryParams)}`;\n } catch {\n key = undefined;\n }\n\n if (key === undefined) {\n const source = buildActiveRouteSource(\n router,\n routeName,\n params,\n strict,\n ignoreQueryParams,\n );\n\n return {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n }\n\n let perRouter = activeSourceCache.get(router);\n\n if (!perRouter) {\n perRouter = new Map();\n activeSourceCache.set(router, perRouter);\n }\n\n let cached = perRouter.get(key);\n\n if (!cached) {\n const source = buildActiveRouteSource(\n router,\n routeName,\n params,\n strict,\n ignoreQueryParams,\n );\n\n cached = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n perRouter.set(key, cached);\n }\n\n return cached;\n}\n\nfunction buildActiveRouteSource(\n router: Router,\n routeName: string,\n params: Params | undefined,\n strict: boolean,\n ignoreQueryParams: boolean,\n): RouterSource<boolean> {\n const initialValue = router.isActiveRoute(\n routeName,\n params,\n strict,\n ignoreQueryParams,\n );\n\n const source = new BaseSource(initialValue);\n\n // Eager connection: subscribe to router immediately. This source is only\n // ever reached through the cached public `createActiveRouteSource`, whose\n // returned wrapper has a no-op destroy. The source lives with the router;\n // the router.subscribe handle is released on router GC.\n router.subscribe((next) => {\n const isNewRelated = areRoutesRelated(routeName, next.route.name);\n const isPrevRelated =\n next.previousRoute &&\n areRoutesRelated(routeName, next.previousRoute.name);\n\n if (!isNewRelated && !isPrevRelated) {\n return;\n }\n\n // If new route is not related, we know the route is inactive —\n // avoid calling isActiveRoute for the optimization\n const newValue = isNewRelated\n ? router.isActiveRoute(routeName, params, strict, ignoreQueryParams)\n : false;\n\n if (!Object.is(source.getSnapshot(), newValue)) {\n source.updateSnapshot(newValue);\n }\n });\n\n return source;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { events } from \"@real-router/core\";\nimport { getPluginApi } from \"@real-router/core/api\";\n\nimport { BaseSource } from \"./BaseSource\";\nimport { stabilizeState } from \"./stabilizeState.js\";\n\nimport type { RouterTransitionSnapshot, RouterSource } from \"./types.js\";\nimport type { Router, State } from \"@real-router/core\";\n\nconst IDLE_SNAPSHOT: RouterTransitionSnapshot = {\n isTransitioning: false,\n isLeaveApproved: false,\n toRoute: null,\n fromRoute: null,\n};\n\nconst transitionSourceCache = new WeakMap<\n Router,\n RouterSource<RouterTransitionSnapshot>\n>();\n\nexport function createTransitionSource(\n router: Router,\n): RouterSource<RouterTransitionSnapshot> {\n const source = new BaseSource(IDLE_SNAPSHOT, {\n onDestroy: () => {\n unsubs.forEach((unsub) => {\n unsub();\n });\n },\n });\n\n const api = getPluginApi(router);\n\n const resetToIdle = (): void => {\n source.updateSnapshot(IDLE_SNAPSHOT);\n };\n\n // Eager connection: subscribe to router events immediately\n const unsubs = [\n api.addEventListener(\n events.TRANSITION_START,\n (toState: State, fromState?: State) => {\n const prev = source.getSnapshot();\n const newToRoute = stabilizeState(prev.toRoute, toState);\n const newFromRoute = stabilizeState(prev.fromRoute, fromState ?? null);\n\n if (\n !prev.isTransitioning ||\n newToRoute !== prev.toRoute ||\n newFromRoute !== prev.fromRoute\n ) {\n source.updateSnapshot({\n isTransitioning: true,\n isLeaveApproved: false,\n toRoute: newToRoute,\n fromRoute: newFromRoute,\n });\n }\n },\n ),\n api.addEventListener(\n events.TRANSITION_LEAVE_APPROVE,\n (toState: State, fromState?: State) => {\n const prev = source.getSnapshot();\n\n source.updateSnapshot({\n isTransitioning: true,\n isLeaveApproved: true,\n toRoute: stabilizeState(prev.toRoute, toState),\n fromRoute: stabilizeState(prev.fromRoute, fromState ?? null),\n });\n },\n ),\n api.addEventListener(events.TRANSITION_SUCCESS, resetToIdle),\n api.addEventListener(events.TRANSITION_ERROR, resetToIdle),\n api.addEventListener(events.TRANSITION_CANCEL, resetToIdle),\n ];\n\n return source;\n}\n\n/**\n * Returns a per-router cached transition source shared across all consumers.\n *\n * Safe to call destroy() — the cached source ignores external destroy() calls\n * and lives until the router itself is garbage-collected (the WeakMap entry\n * releases automatically).\n *\n * Use this in framework adapters (React/Preact/Solid/Vue/Svelte/Angular) to\n * share a single TransitionSource instance across all mount/unmount cycles.\n *\n * For isolated/advanced use (ad-hoc, short-lived, per-owner teardown), call\n * `createTransitionSource(router)` directly — it returns a fresh instance with\n * a working `destroy()`.\n */\nexport function getTransitionSource(\n router: Router,\n): RouterSource<RouterTransitionSnapshot> {\n let cached = transitionSourceCache.get(router);\n\n if (!cached) {\n const source = createTransitionSource(router);\n\n // Wrap with no-op destroy. The underlying source is shared across all\n // consumers; letting any one consumer call destroy() would tear it down\n // for the rest. The source lives as long as the router (WeakMap key).\n cached = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n transitionSourceCache.set(router, cached);\n }\n\n return cached;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { events } from \"@real-router/core\";\nimport { getPluginApi } from \"@real-router/core/api\";\n\nimport { BaseSource } from \"./BaseSource\";\n\nimport type { RouterErrorSnapshot, RouterSource } from \"./types.js\";\nimport type { Router, State, RouterError } from \"@real-router/core\";\n\nconst INITIAL_SNAPSHOT: RouterErrorSnapshot = {\n error: null,\n toRoute: null,\n fromRoute: null,\n version: 0,\n};\n\nconst errorSourceCache = new WeakMap<\n Router,\n RouterSource<RouterErrorSnapshot>\n>();\n\nexport function createErrorSource(\n router: Router,\n): RouterSource<RouterErrorSnapshot> {\n let errorVersion = 0;\n\n const source = new BaseSource(INITIAL_SNAPSHOT, {\n onDestroy: () => {\n unsubs.forEach((unsub) => {\n unsub();\n });\n },\n });\n\n const api = getPluginApi(router);\n\n // Eager connection: subscribe to router events immediately\n const unsubs = [\n api.addEventListener(\n events.TRANSITION_ERROR,\n (\n toState: State | undefined,\n fromState: State | undefined,\n err: RouterError,\n ) => {\n errorVersion++;\n source.updateSnapshot({\n error: err,\n toRoute: toState ?? null,\n /* v8 ignore next -- @preserve: fromState undefined only during start() error; unreachable via navigate() */\n fromRoute: fromState ?? null,\n version: errorVersion,\n });\n },\n ),\n api.addEventListener(events.TRANSITION_SUCCESS, () => {\n // Skip if no error — avoids unnecessary re-renders.\n // BaseSource.updateSnapshot() always notifies listeners (new object = new ref),\n // and useSyncExternalStore compares via Object.is().\n if (source.getSnapshot().error !== null) {\n source.updateSnapshot({\n error: null,\n toRoute: null,\n fromRoute: null,\n version: errorVersion,\n });\n }\n }),\n ];\n\n return source;\n}\n\n/**\n * Returns a per-router cached error source shared across all consumers.\n *\n * Safe to call destroy() — the cached source ignores external destroy() calls\n * and lives until the router itself is garbage-collected (the WeakMap entry\n * releases automatically).\n *\n * Use this in framework adapters (React/Preact/Solid/Vue/Svelte/Angular) to\n * share a single ErrorSource instance across all mount/unmount cycles.\n *\n * For isolated/advanced use (ad-hoc, short-lived, per-owner teardown), call\n * `createErrorSource(router)` directly — it returns a fresh instance with a\n * working `destroy()`.\n */\nexport function getErrorSource(\n router: Router,\n): RouterSource<RouterErrorSnapshot> {\n let cached = errorSourceCache.get(router);\n\n if (!cached) {\n const source = createErrorSource(router);\n\n // Wrap with no-op destroy. The underlying source is shared across all\n // consumers; letting any one consumer call destroy() would tear it down\n // for the rest. The source lives as long as the router (WeakMap key).\n cached = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n errorSourceCache.set(router, cached);\n }\n\n return cached;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { getErrorSource } from \"./createErrorSource\";\n\nimport type { DismissableErrorSnapshot, RouterSource } from \"./types.js\";\nimport type { Router } from \"@real-router/core\";\n\nconst dismissableCache = new WeakMap<\n Router,\n RouterSource<DismissableErrorSnapshot>\n>();\n\n/**\n * Returns a per-router cached source that wraps `getErrorSource(router)` with\n * an integrated \"dismissed\" version counter, exposing a single reactive\n * snapshot `{ error, toRoute, fromRoute, version, resetError }`.\n *\n * Each `RouterErrorBoundary` in a framework adapter subscribes to this one\n * source instead of re-implementing the `dismissedVersion` state pattern\n * locally. The 6-copy duplicate across adapters collapses to this helper.\n *\n * **Semantics:**\n * - `error` is non-null only when `underlying.version > dismissedVersion`.\n * - `resetError()` sets `dismissedVersion = current underlying version`,\n * immediately clearing `error` to `null` and notifying all listeners.\n * - A subsequent `TRANSITION_ERROR` advances `version` beyond `dismissedVersion`,\n * so `error` becomes non-null again — no additional plumbing needed.\n *\n * **Cached:** one instance per router. `destroy()` on the returned source is\n * a no-op. Shared across all `RouterErrorBoundary` consumers.\n */\nexport function createDismissableError(\n router: Router,\n): RouterSource<DismissableErrorSnapshot> {\n const cached = dismissableCache.get(router);\n\n if (cached) {\n return cached;\n }\n\n const errorSource = getErrorSource(router);\n\n let dismissedVersion = -1;\n\n const computeSnapshot = (): DismissableErrorSnapshot => {\n const snap = errorSource.getSnapshot();\n const isDismissed = snap.version <= dismissedVersion;\n\n return {\n error: isDismissed ? null : snap.error,\n toRoute: isDismissed ? null : snap.toRoute,\n fromRoute: isDismissed ? null : snap.fromRoute,\n version: snap.version,\n resetError,\n };\n };\n\n const source = new BaseSource<DismissableErrorSnapshot>(computeSnapshot(), {\n onFirstSubscribe: () => {\n unsubFromError = errorSource.subscribe(() => {\n source.updateSnapshot(computeSnapshot());\n });\n },\n onLastUnsubscribe: () => {\n disconnect();\n },\n });\n\n let unsubFromError: (() => void) | null = null;\n\n function resetError(): void {\n dismissedVersion = errorSource.getSnapshot().version;\n source.updateSnapshot(computeSnapshot());\n }\n\n function disconnect(): void {\n const unsub = unsubFromError;\n\n unsubFromError = null;\n unsub?.();\n }\n\n const wrapper: RouterSource<DismissableErrorSnapshot> = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n\n dismissableCache.set(router, wrapper);\n\n return wrapper;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { areRoutesRelated } from \"@real-router/route-utils\";\n\nimport type { Router } from \"@real-router/core\";\n\nexport interface ActiveNameSelector {\n /**\n * Subscribes to active-state changes of a specific route name.\n * Listener is called only when `isActive(routeName)` for this name transitions.\n * Returns an unsubscribe function.\n */\n subscribe: (routeName: string, listener: () => void) => () => void;\n /**\n * O(1) active check for the given route name (non-strict by default —\n * matches descendants). Uses the shared underlying router subscription.\n */\n isActive: (routeName: string) => boolean;\n /** No-op on the cached wrapper. */\n destroy: () => void;\n}\n\nconst selectorCache = new WeakMap<Router, ActiveNameSelector>();\n\n/**\n * Per-router cached selector providing O(1) active-route checks with one\n * shared router subscription for any number of distinct `routeName`\n * consumers.\n *\n * **When to use:** framework `Link` components that need an active boolean\n * without custom `params` / `activeStrict` / `ignoreQueryParams` — e.g. the\n * common navigation-link case. Multiple `<Link>` components with different\n * `routeName` share ONE `router.subscribe` handle instead of creating one\n * per Link (which is what `createActiveRouteSource(router, name)` does — it\n * caches per-name, so N names = N subscriptions).\n *\n * **When NOT to use:** Link needs `activeStrict: true`, custom `routeParams`,\n * or `ignoreQueryParams: false`. Fall back to `createActiveRouteSource` —\n * its cache handles the full argument surface.\n *\n * Based on the `routeSelector` pattern pioneered by `@real-router/solid`'s\n * `RouterProvider` (`createSelector` + `areRoutesRelated`). This helper\n * ports it to framework-agnostic API so Vue / React / Preact / Svelte /\n * Angular Link components can adopt the same fast path.\n *\n * @see Solid reference implementation — `packages/solid/src/RouterProvider.tsx`\n */\nexport function createActiveNameSelector(router: Router): ActiveNameSelector {\n const cached = selectorCache.get(router);\n\n if (cached) {\n return cached;\n }\n\n // listeners per-name — re-evaluated on every router transition\n const listenersByName = new Map<string, Set<() => void>>();\n // cached active state per-name — used to diff before notifying\n const activeByName = new Map<string, boolean>();\n\n let routerUnsubscribe: (() => void) | null = null;\n\n const isActiveNonStrict = (routeName: string): boolean => {\n const current = router.getState();\n\n if (!current) {\n return false;\n }\n\n return (\n current.name === routeName || current.name.startsWith(`${routeName}.`)\n );\n };\n\n const connect = (): void => {\n routerUnsubscribe = router.subscribe((next) => {\n for (const [routeName, listeners] of listenersByName) {\n // Cheap pre-filter: if neither new nor previous route is related\n // to this name, its active state cannot have changed.\n const isNewRelated = areRoutesRelated(routeName, next.route.name);\n const isPrevRelated =\n next.previousRoute &&\n areRoutesRelated(routeName, next.previousRoute.name);\n\n if (!isNewRelated && !isPrevRelated) {\n continue;\n }\n\n // activeByName always has an entry for names present in listenersByName —\n // subscribe() seeds it, and we only iterate over listenersByName.\n const prevActive = activeByName.get(routeName) === true;\n const nextActive = isActiveNonStrict(routeName);\n\n if (prevActive === nextActive) {\n continue;\n }\n\n activeByName.set(routeName, nextActive);\n for (const listener of listeners) {\n listener();\n }\n }\n });\n };\n\n const disconnect = (): void => {\n const unsub = routerUnsubscribe;\n\n routerUnsubscribe = null;\n unsub?.();\n };\n\n const subscribe = (routeName: string, listener: () => void): (() => void) => {\n let listeners = listenersByName.get(routeName);\n\n if (!listeners) {\n listeners = new Set();\n listenersByName.set(routeName, listeners);\n activeByName.set(routeName, isActiveNonStrict(routeName));\n }\n\n listeners.add(listener);\n\n if (!routerUnsubscribe) {\n connect();\n }\n\n let unsubscribed = false;\n\n return () => {\n if (unsubscribed) {\n return;\n }\n\n unsubscribed = true;\n listeners.delete(listener);\n\n if (listeners.size === 0) {\n listenersByName.delete(routeName);\n activeByName.delete(routeName);\n }\n\n if (listenersByName.size === 0) {\n disconnect();\n }\n };\n };\n\n const isActive = (routeName: string): boolean => {\n const cachedActive = activeByName.get(routeName);\n\n if (cachedActive !== undefined) {\n return cachedActive;\n }\n\n // Not subscribed — compute on demand.\n return isActiveNonStrict(routeName);\n };\n\n const selector: ActiveNameSelector = {\n subscribe,\n isActive,\n destroy: noopDestroy,\n };\n\n selectorCache.set(router, selector);\n\n return selector;\n}\n\nfunction noopDestroy(): void {\n // Shared cached selector — external destroy() is a no-op.\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["#listeners","#onFirstSubscribe","#onLastUnsubscribe","#onDestroy","#currentSnapshot","#destroyed","readContextHash","noopDestroy","noopDestroy","events","noopDestroy","events","noopDestroy","noopDestroy"],"sources":["../../src/BaseSource.ts","../../src/stabilizeState.ts","../../src/createRouteSource.ts","../../src/computeSnapshot.ts","../../src/createRouteNodeSource.ts","../../src/canonicalJson.ts","../../src/normalizeActiveOptions.ts","../../src/createActiveRouteSource.ts","../../src/createTransitionSource.ts","../../src/createErrorSource.ts","../../src/createDismissableError.ts","../../src/createActiveNameSelector.ts"],"sourcesContent":["export interface BaseSourceOptions {\n onFirstSubscribe?: () => void;\n onLastUnsubscribe?: () => void;\n onDestroy?: () => void;\n}\n\nexport class BaseSource<T> {\n #currentSnapshot: T;\n #destroyed = false;\n\n readonly #listeners = new Set<() => void>();\n readonly #onFirstSubscribe: (() => void) | undefined;\n readonly #onLastUnsubscribe: (() => void) | undefined;\n readonly #onDestroy: (() => void) | undefined;\n\n constructor(initialSnapshot: T, options?: BaseSourceOptions) {\n this.#currentSnapshot = initialSnapshot;\n this.#onFirstSubscribe = options?.onFirstSubscribe;\n this.#onLastUnsubscribe = options?.onLastUnsubscribe;\n this.#onDestroy = options?.onDestroy;\n\n this.subscribe = this.subscribe.bind(this);\n this.getSnapshot = this.getSnapshot.bind(this);\n this.destroy = this.destroy.bind(this);\n }\n\n subscribe(listener: () => void): () => void {\n if (this.#destroyed) {\n return () => {};\n }\n\n const wasFirst = this.#listeners.size === 0;\n\n // Add listener BEFORE onFirstSubscribe so that if the reconciliation in\n // onFirstSubscribe calls updateSnapshot(), this listener receives the\n // notification. Critical for useSyncExternalStore in adapters — without\n // this the post-reconnection snapshot is missed and consumers render\n // stale data. (See Preact RouteView nested remount test.)\n this.#listeners.add(listener);\n\n if (wasFirst && this.#onFirstSubscribe) {\n this.#onFirstSubscribe();\n }\n\n return () => {\n this.#listeners.delete(listener);\n\n if (\n !this.#destroyed &&\n this.#listeners.size === 0 &&\n this.#onLastUnsubscribe\n ) {\n this.#onLastUnsubscribe();\n }\n };\n }\n\n getSnapshot(): T {\n return this.#currentSnapshot;\n }\n\n updateSnapshot(snapshot: T): void {\n /* v8 ignore next 2 -- @preserve: defensive guard unreachable via public API (destroy() removes router subscription first) */\n if (this.#destroyed) {\n return;\n }\n\n this.#currentSnapshot = snapshot;\n this.#listeners.forEach((listener) => {\n listener();\n });\n }\n\n destroy(): void {\n if (this.#destroyed) {\n return;\n }\n\n this.#destroyed = true;\n this.#onDestroy?.();\n this.#listeners.clear();\n }\n}\n","import type { State } from \"@real-router/core\";\n\n/**\n * State-aware stabilization for route snapshots.\n *\n * Compares `path` (canonical name+params) AND `state.context.url.hash`\n * (URL fragment, #532). When both match, returns `prev` (preserving\n * reference) so frameworks can skip re-renders. When hash flips on a\n * same-path navigation (tab-style UI), returns `next` so consumers\n * subscribing through `useRoute()` see the new state.\n *\n * Ignores `meta` (internal: auto-increment id), `transition` (reference\n * data: from, segments, reload), and `state.context.navigation` /\n * `state.context.browser` (transient transition metadata) — they don't\n * affect what is rendered. `state.context.url.hash` is the only context\n * field that participates in render identity, because tab-style UIs\n * subscribe to it directly.\n *\n * Accepts `null` for compatibility with `RouterTransitionSnapshot`\n * (toRoute/fromRoute are `State | null`).\n *\n * @internal Not exported from package public API.\n */\nexport function stabilizeState<T extends State | null | undefined>(\n prev: T,\n next: T,\n): T {\n if (prev === next) {\n return prev;\n }\n if (prev?.path !== next?.path) {\n return next;\n }\n\n // After the path check, both must be the same non-null State (paths\n // matched, prev !== next reference). Read context.url.hash to detect\n // same-path-different-hash navigation (#532) — render-relevant for\n // tab-style UIs that subscribe via useRoute(). Optional chaining keeps\n // the access null-safe without forbidden non-null assertions.\n if (readContextHash(prev) !== readContextHash(next)) {\n return next;\n }\n\n return prev;\n}\n\nfunction readContextHash(state: State | null | undefined): string | undefined {\n const ctx = state?.context as { url?: { hash?: string } } | undefined;\n\n return ctx?.url?.hash;\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { stabilizeState } from \"./stabilizeState.js\";\n\nimport type { RouteSnapshot, RouterSource } from \"./types.js\";\nimport type { Router } from \"@real-router/core\";\n\n/**\n * Creates a source for the full route state.\n *\n * Uses a lazy-connection pattern: the router subscription is created when the\n * first listener subscribes and removed when the last listener unsubscribes.\n * This is compatible with React's useSyncExternalStore and Strict Mode.\n */\nexport function createRouteSource(router: Router): RouterSource<RouteSnapshot> {\n let routerUnsubscribe: (() => void) | null = null;\n\n const disconnect = (): void => {\n const unsub = routerUnsubscribe;\n\n routerUnsubscribe = null;\n unsub?.();\n };\n\n const source = new BaseSource<RouteSnapshot>(\n {\n route: router.getState(),\n previousRoute: undefined,\n },\n {\n onFirstSubscribe: () => {\n routerUnsubscribe = router.subscribe((next) => {\n const prev = source.getSnapshot();\n const newRoute = stabilizeState(prev.route, next.route);\n const newPreviousRoute = stabilizeState(\n prev.previousRoute,\n next.previousRoute,\n );\n\n if (\n newRoute !== prev.route ||\n newPreviousRoute !== prev.previousRoute\n ) {\n source.updateSnapshot({\n route: newRoute,\n previousRoute: newPreviousRoute,\n });\n }\n });\n },\n onLastUnsubscribe: disconnect,\n onDestroy: disconnect,\n },\n );\n\n return source;\n}\n","import { stabilizeState } from \"./stabilizeState.js\";\n\nimport type { RouteNodeSnapshot } from \"./types.js\";\nimport type { Router, SubscribeState } from \"@real-router/core\";\n\nexport function computeSnapshot(\n currentSnapshot: RouteNodeSnapshot,\n router: Router,\n nodeName: string,\n next?: SubscribeState,\n): RouteNodeSnapshot {\n const currentRoute = next?.route ?? router.getState();\n const previousRoute = next?.previousRoute;\n\n const isNodeActive =\n nodeName === \"\" ||\n (currentRoute !== undefined &&\n (currentRoute.name === nodeName ||\n currentRoute.name.startsWith(`${nodeName}.`)));\n\n const route = isNodeActive ? currentRoute : undefined;\n\n if (\n route === currentSnapshot.route &&\n previousRoute === currentSnapshot.previousRoute\n ) {\n return currentSnapshot;\n }\n\n const newRoute = stabilizeState(currentSnapshot.route, route);\n const newPreviousRoute = stabilizeState(\n currentSnapshot.previousRoute,\n previousRoute,\n );\n\n if (\n newRoute === currentSnapshot.route &&\n newPreviousRoute === currentSnapshot.previousRoute\n ) {\n return currentSnapshot;\n }\n\n return { route: newRoute, previousRoute: newPreviousRoute };\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { computeSnapshot } from \"./computeSnapshot.js\";\n\nimport type { RouteNodeSnapshot, RouterSource } from \"./types.js\";\nimport type { Router } from \"@real-router/core\";\n\nconst nodeSourceCache = new WeakMap<\n Router,\n Map<string, RouterSource<RouteNodeSnapshot>>\n>();\n\n/**\n * Creates a source scoped to a specific route node.\n *\n * **Per-router + per-nodeName cache:** repeated calls with the same\n * `(router, nodeName)` return the same shared instance. `N` consumers\n * calling `createRouteNodeSource(r, \"users\")` produce one router subscription\n * shared across all of them.\n *\n * Uses a lazy-connection pattern: the router subscription is created when the\n * first listener subscribes and removed when the last listener unsubscribes.\n * This is compatible with React's useSyncExternalStore and Strict Mode.\n *\n * `destroy()` on the returned source is a no-op — the shared instance lives\n * as long as the router itself (the WeakMap entry releases automatically on\n * router GC). Callers that need an isolated instance with working teardown\n * can use `buildRouteNodeSource` internally (not exported).\n */\nexport function createRouteNodeSource(\n router: Router,\n nodeName: string,\n): RouterSource<RouteNodeSnapshot> {\n let perRouter = nodeSourceCache.get(router);\n\n if (!perRouter) {\n perRouter = new Map();\n nodeSourceCache.set(router, perRouter);\n }\n\n let cached = perRouter.get(nodeName);\n\n if (!cached) {\n const source = buildRouteNodeSource(router, nodeName);\n\n // Wrap with no-op destroy. The shared source lives with the router.\n cached = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n perRouter.set(nodeName, cached);\n }\n\n return cached;\n}\n\nfunction buildRouteNodeSource(\n router: Router,\n nodeName: string,\n): RouterSource<RouteNodeSnapshot> {\n let routerUnsubscribe: (() => void) | null = null;\n\n // Built once per cached source instance; safe — createRouteNodeSource is\n // itself per-(router, nodeName) cached, so shouldUpdate is called once.\n const shouldUpdate = router.shouldUpdateNode(nodeName);\n\n const initialSnapshot: RouteNodeSnapshot = {\n route: undefined,\n previousRoute: undefined,\n };\n\n const disconnect = (): void => {\n const unsub = routerUnsubscribe;\n\n routerUnsubscribe = null;\n unsub?.();\n };\n\n const source = new BaseSource<RouteNodeSnapshot>(\n computeSnapshot(initialSnapshot, router, nodeName),\n {\n onFirstSubscribe: () => {\n // Reconcile snapshot with current router state before connecting.\n // Covers reconnection after Activity hide/show cycles where the\n // source was disconnected and missed navigation events.\n const reconciled = computeSnapshot(\n source.getSnapshot(),\n router,\n nodeName,\n );\n\n if (!Object.is(reconciled, source.getSnapshot())) {\n source.updateSnapshot(reconciled);\n }\n\n // Connect to router on first subscription\n routerUnsubscribe = router.subscribe((next) => {\n if (!shouldUpdate(next.route, next.previousRoute)) {\n return;\n }\n\n const newSnapshot = computeSnapshot(\n source.getSnapshot(),\n router,\n nodeName,\n next,\n );\n\n if (!Object.is(source.getSnapshot(), newSnapshot)) {\n source.updateSnapshot(newSnapshot);\n }\n });\n },\n onLastUnsubscribe: disconnect,\n },\n );\n\n return source;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","/**\n * Serializes a value into a stable JSON string — object keys are sorted at\n * every level so that `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` produce the same\n * output.\n *\n * Used as a cache key for `createActiveRouteSource` so that equivalent params\n * objects share the same cached source regardless of key order.\n *\n * Edge cases:\n * - Arrays preserve order (canonical: index-ordered already).\n * - `undefined` values are dropped (standard JSON behaviour).\n * - `Symbol`, `BigInt`, `Date`, `Map`, `Set` etc. fall through to\n * `JSON.stringify` defaults — `Symbol` becomes `undefined`, `BigInt` throws.\n * In practice, route params carry primitives (`string | number | boolean`)\n * and such edge cases would hit a fresh source on cache miss — acceptable.\n */\nexport function canonicalJson(value: unknown): string {\n return JSON.stringify(value, replacer);\n}\n\nfunction replacer(_key: string, val: unknown): unknown {\n if (val !== null && typeof val === \"object\" && !Array.isArray(val)) {\n const sorted: Record<string, unknown> = {};\n const keys = Object.keys(val as Record<string, unknown>).toSorted(\n (left, right) => left.localeCompare(right),\n );\n\n for (const key of keys) {\n sorted[key] = (val as Record<string, unknown>)[key];\n }\n\n return sorted;\n }\n\n return val;\n}\n","import type { ActiveRouteSourceOptions } from \"./types.js\";\n\n/**\n * Normalized options shape — booleans are required (filled with defaults),\n * `hash` stays `string | undefined` because `undefined` is the meaningful\n * \"ignore hash\" sentinel that callers pass intentionally.\n */\nexport interface NormalizedActiveOptions {\n strict: boolean;\n ignoreQueryParams: boolean;\n hash: string | undefined;\n}\n\n/**\n * Default options for `createActiveRouteSource` and adapter-level helpers.\n *\n * Frozen to prevent accidental mutation by consumers.\n */\nexport const DEFAULT_ACTIVE_OPTIONS: Readonly<NormalizedActiveOptions> =\n Object.freeze({\n strict: false,\n ignoreQueryParams: true,\n hash: undefined,\n });\n\n/**\n * Normalizes partial `ActiveRouteSourceOptions` into a fully-defaulted object.\n *\n * Use this to produce a stable options record for comparison, caching, or\n * downstream consumers that require all fields present.\n */\nexport function normalizeActiveOptions(\n options?: ActiveRouteSourceOptions,\n): NormalizedActiveOptions {\n return {\n strict: options?.strict ?? DEFAULT_ACTIVE_OPTIONS.strict,\n ignoreQueryParams:\n options?.ignoreQueryParams ?? DEFAULT_ACTIVE_OPTIONS.ignoreQueryParams,\n hash: options?.hash,\n };\n}\n","import { areRoutesRelated } from \"@real-router/route-utils\";\n\nimport { BaseSource } from \"./BaseSource\";\nimport { canonicalJson } from \"./canonicalJson.js\";\nimport { normalizeActiveOptions } from \"./normalizeActiveOptions.js\";\n\nimport type { ActiveRouteSourceOptions, RouterSource } from \"./types.js\";\nimport type { Params, Router } from \"@real-router/core\";\n\nconst activeSourceCache = new WeakMap<\n Router,\n Map<string, RouterSource<boolean>>\n>();\n\n/**\n * Creates a source tracking whether a route (with given params/options) is active.\n *\n * **Per-router + canonical-args cache:** repeated calls with equivalent\n * arguments return the same shared instance. Param key order doesn't matter\n * (`{ a:1, b:2 }` and `{ b:2, a:1 }` hit the same cache entry via\n * `canonicalJson`).\n *\n * `destroy()` is a no-op — shared sources live with the router. The router\n * subscription stays active while any consumer subscribes; when the router\n * is garbage-collected, the WeakMap entry releases automatically.\n *\n * Edge cases: `Symbol`/`BigInt` in params bypass `canonicalJson` and produce\n * an unstable cache key — these will simply miss the cache and create a new\n * source on each call. Practical params are primitives, so this is not a\n * concern in real usage.\n */\nexport function createActiveRouteSource(\n router: Router,\n routeName: string,\n params?: Params,\n options?: ActiveRouteSourceOptions,\n): RouterSource<boolean> {\n const { strict, ignoreQueryParams, hash } = normalizeActiveOptions(options);\n\n // BigInt/Symbol/circular refs cannot be serialized — fall back to creating\n // a fresh (non-cached) source. Callers pass these edge-case params rarely;\n // the extra allocation is acceptable.\n let key: string | undefined;\n\n try {\n // `hash === undefined` produces \"\" via String(undefined) → \"undefined\";\n // we encode it as the empty string sentinel to keep the key short and\n // distinct from the literal \"undefined\" hash value (which is a valid,\n // if unusual, fragment).\n const hashKey = hash === undefined ? \"\" : `#${hash}`;\n\n key = `${routeName}|${canonicalJson(params)}|${String(strict)}|${String(ignoreQueryParams)}|${hashKey}`;\n } catch {\n key = undefined;\n }\n\n if (key === undefined) {\n const source = buildActiveRouteSource(\n router,\n routeName,\n params,\n strict,\n ignoreQueryParams,\n hash,\n );\n\n return {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n }\n\n let perRouter = activeSourceCache.get(router);\n\n if (!perRouter) {\n perRouter = new Map();\n activeSourceCache.set(router, perRouter);\n }\n\n let cached = perRouter.get(key);\n\n if (!cached) {\n const source = buildActiveRouteSource(\n router,\n routeName,\n params,\n strict,\n ignoreQueryParams,\n hash,\n );\n\n cached = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n perRouter.set(key, cached);\n }\n\n return cached;\n}\n\n/**\n * Reads the URL fragment published by browser/navigation plugins on the given\n * router state. Returns `\"\"` when no plugin claims the `\"url\"` namespace\n * (hash-plugin runtime, memory-plugin, SSR) — `undefined` is reserved for\n * \"no published fragment yet\" and not visible at the source layer.\n */\nfunction readContextHash(router: Router): string {\n const ctx = router.getState()?.context as\n | { url?: { hash?: string } }\n | undefined;\n\n return ctx?.url?.hash ?? \"\";\n}\n\n/**\n * Combines route-name match with optional hash match (#532).\n *\n * - Route-name match: `router.isActiveRoute(name, params, strict, ignoreQueryParams)`.\n * - Hash match (only when `hash !== undefined`): `state.context.url.hash` must\n * equal the requested fragment exactly. With hash-plugin (no `url`\n * namespace), this returns `false` — the documented limitation.\n */\nfunction computeActive(\n router: Router,\n routeName: string,\n params: Params | undefined,\n strict: boolean,\n ignoreQueryParams: boolean,\n hash: string | undefined,\n): boolean {\n const routeActive = router.isActiveRoute(\n routeName,\n params,\n strict,\n ignoreQueryParams,\n );\n\n if (!routeActive) {\n return false;\n }\n if (hash === undefined) {\n return true;\n }\n\n return readContextHash(router) === hash;\n}\n\nfunction buildActiveRouteSource(\n router: Router,\n routeName: string,\n params: Params | undefined,\n strict: boolean,\n ignoreQueryParams: boolean,\n hash: string | undefined,\n): RouterSource<boolean> {\n const initialValue = computeActive(\n router,\n routeName,\n params,\n strict,\n ignoreQueryParams,\n hash,\n );\n\n const source = new BaseSource(initialValue);\n\n // Eager connection: subscribe to router immediately. This source is only\n // ever reached through the cached public `createActiveRouteSource`, whose\n // returned wrapper has a no-op destroy. The source lives with the router;\n // the router.subscribe handle is released on router GC.\n router.subscribe((next) => {\n const isNewRelated = areRoutesRelated(routeName, next.route.name);\n const isPrevRelated =\n next.previousRoute &&\n areRoutesRelated(routeName, next.previousRoute.name);\n\n // Hash-aware sources also flip on same-path-different-hash transitions.\n // The route comparison alone misses these (route is identical), but the\n // hash claim updated, so we must re-evaluate. Detect via the `hashChanged`\n // flag published by URL plugins.\n const hashFlip =\n hash !== undefined &&\n ((next.route.context as { url?: { hashChanged?: boolean } } | undefined)\n ?.url?.hashChanged ??\n false);\n\n if (!isNewRelated && !isPrevRelated && !hashFlip) {\n return;\n }\n\n // If new route is not related, we know the route is inactive —\n // avoid calling isActiveRoute for the optimization. (Hash check would\n // also fail without route-match, so this short-circuit holds for\n // hash-aware sources too.)\n const newValue = isNewRelated\n ? computeActive(\n router,\n routeName,\n params,\n strict,\n ignoreQueryParams,\n hash,\n )\n : false;\n\n if (!Object.is(source.getSnapshot(), newValue)) {\n source.updateSnapshot(newValue);\n }\n });\n\n return source;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { events } from \"@real-router/core\";\nimport { getPluginApi } from \"@real-router/core/api\";\n\nimport { BaseSource } from \"./BaseSource\";\nimport { stabilizeState } from \"./stabilizeState.js\";\n\nimport type { RouterTransitionSnapshot, RouterSource } from \"./types.js\";\nimport type { Router, State } from \"@real-router/core\";\n\nconst IDLE_SNAPSHOT: RouterTransitionSnapshot = {\n isTransitioning: false,\n isLeaveApproved: false,\n toRoute: null,\n fromRoute: null,\n};\n\nconst transitionSourceCache = new WeakMap<\n Router,\n RouterSource<RouterTransitionSnapshot>\n>();\n\nexport function createTransitionSource(\n router: Router,\n): RouterSource<RouterTransitionSnapshot> {\n const source = new BaseSource(IDLE_SNAPSHOT, {\n onDestroy: () => {\n unsubs.forEach((unsub) => {\n unsub();\n });\n },\n });\n\n const api = getPluginApi(router);\n\n const resetToIdle = (): void => {\n source.updateSnapshot(IDLE_SNAPSHOT);\n };\n\n // Eager connection: subscribe to router events immediately\n const unsubs = [\n api.addEventListener(\n events.TRANSITION_START,\n (toState: State, fromState?: State) => {\n const prev = source.getSnapshot();\n const newToRoute = stabilizeState(prev.toRoute, toState);\n const newFromRoute = stabilizeState(prev.fromRoute, fromState ?? null);\n\n if (\n !prev.isTransitioning ||\n newToRoute !== prev.toRoute ||\n newFromRoute !== prev.fromRoute\n ) {\n source.updateSnapshot({\n isTransitioning: true,\n isLeaveApproved: false,\n toRoute: newToRoute,\n fromRoute: newFromRoute,\n });\n }\n },\n ),\n api.addEventListener(\n events.TRANSITION_LEAVE_APPROVE,\n (toState: State, fromState?: State) => {\n const prev = source.getSnapshot();\n\n source.updateSnapshot({\n isTransitioning: true,\n isLeaveApproved: true,\n toRoute: stabilizeState(prev.toRoute, toState),\n fromRoute: stabilizeState(prev.fromRoute, fromState ?? null),\n });\n },\n ),\n api.addEventListener(events.TRANSITION_SUCCESS, resetToIdle),\n api.addEventListener(events.TRANSITION_ERROR, resetToIdle),\n api.addEventListener(events.TRANSITION_CANCEL, resetToIdle),\n ];\n\n return source;\n}\n\n/**\n * Returns a per-router cached transition source shared across all consumers.\n *\n * Safe to call destroy() — the cached source ignores external destroy() calls\n * and lives until the router itself is garbage-collected (the WeakMap entry\n * releases automatically).\n *\n * Use this in framework adapters (React/Preact/Solid/Vue/Svelte/Angular) to\n * share a single TransitionSource instance across all mount/unmount cycles.\n *\n * For isolated/advanced use (ad-hoc, short-lived, per-owner teardown), call\n * `createTransitionSource(router)` directly — it returns a fresh instance with\n * a working `destroy()`.\n */\nexport function getTransitionSource(\n router: Router,\n): RouterSource<RouterTransitionSnapshot> {\n let cached = transitionSourceCache.get(router);\n\n if (!cached) {\n const source = createTransitionSource(router);\n\n // Wrap with no-op destroy. The underlying source is shared across all\n // consumers; letting any one consumer call destroy() would tear it down\n // for the rest. The source lives as long as the router (WeakMap key).\n cached = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n transitionSourceCache.set(router, cached);\n }\n\n return cached;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { events } from \"@real-router/core\";\nimport { getPluginApi } from \"@real-router/core/api\";\n\nimport { BaseSource } from \"./BaseSource\";\n\nimport type { RouterErrorSnapshot, RouterSource } from \"./types.js\";\nimport type { Router, State, RouterError } from \"@real-router/core\";\n\nconst INITIAL_SNAPSHOT: RouterErrorSnapshot = {\n error: null,\n toRoute: null,\n fromRoute: null,\n version: 0,\n};\n\nconst errorSourceCache = new WeakMap<\n Router,\n RouterSource<RouterErrorSnapshot>\n>();\n\nexport function createErrorSource(\n router: Router,\n): RouterSource<RouterErrorSnapshot> {\n let errorVersion = 0;\n\n const source = new BaseSource(INITIAL_SNAPSHOT, {\n onDestroy: () => {\n unsubs.forEach((unsub) => {\n unsub();\n });\n },\n });\n\n const api = getPluginApi(router);\n\n // Eager connection: subscribe to router events immediately\n const unsubs = [\n api.addEventListener(\n events.TRANSITION_ERROR,\n (\n toState: State | undefined,\n fromState: State | undefined,\n err: RouterError,\n ) => {\n errorVersion++;\n source.updateSnapshot({\n error: err,\n toRoute: toState ?? null,\n /* v8 ignore next -- @preserve: fromState undefined only during start() error; unreachable via navigate() */\n fromRoute: fromState ?? null,\n version: errorVersion,\n });\n },\n ),\n api.addEventListener(events.TRANSITION_SUCCESS, () => {\n // Skip if no error — avoids unnecessary re-renders.\n // BaseSource.updateSnapshot() always notifies listeners (new object = new ref),\n // and useSyncExternalStore compares via Object.is().\n if (source.getSnapshot().error !== null) {\n source.updateSnapshot({\n error: null,\n toRoute: null,\n fromRoute: null,\n version: errorVersion,\n });\n }\n }),\n ];\n\n return source;\n}\n\n/**\n * Returns a per-router cached error source shared across all consumers.\n *\n * Safe to call destroy() — the cached source ignores external destroy() calls\n * and lives until the router itself is garbage-collected (the WeakMap entry\n * releases automatically).\n *\n * Use this in framework adapters (React/Preact/Solid/Vue/Svelte/Angular) to\n * share a single ErrorSource instance across all mount/unmount cycles.\n *\n * For isolated/advanced use (ad-hoc, short-lived, per-owner teardown), call\n * `createErrorSource(router)` directly — it returns a fresh instance with a\n * working `destroy()`.\n */\nexport function getErrorSource(\n router: Router,\n): RouterSource<RouterErrorSnapshot> {\n let cached = errorSourceCache.get(router);\n\n if (!cached) {\n const source = createErrorSource(router);\n\n // Wrap with no-op destroy. The underlying source is shared across all\n // consumers; letting any one consumer call destroy() would tear it down\n // for the rest. The source lives as long as the router (WeakMap key).\n cached = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n errorSourceCache.set(router, cached);\n }\n\n return cached;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { getErrorSource } from \"./createErrorSource\";\n\nimport type { DismissableErrorSnapshot, RouterSource } from \"./types.js\";\nimport type { Router } from \"@real-router/core\";\n\nconst dismissableCache = new WeakMap<\n Router,\n RouterSource<DismissableErrorSnapshot>\n>();\n\n/**\n * Returns a per-router cached source that wraps `getErrorSource(router)` with\n * an integrated \"dismissed\" version counter, exposing a single reactive\n * snapshot `{ error, toRoute, fromRoute, version, resetError }`.\n *\n * Each `RouterErrorBoundary` in a framework adapter subscribes to this one\n * source instead of re-implementing the `dismissedVersion` state pattern\n * locally. The 6-copy duplicate across adapters collapses to this helper.\n *\n * **Semantics:**\n * - `error` is non-null only when `underlying.version > dismissedVersion`.\n * - `resetError()` sets `dismissedVersion = current underlying version`,\n * immediately clearing `error` to `null` and notifying all listeners.\n * - A subsequent `TRANSITION_ERROR` advances `version` beyond `dismissedVersion`,\n * so `error` becomes non-null again — no additional plumbing needed.\n *\n * **Cached:** one instance per router. `destroy()` on the returned source is\n * a no-op. Shared across all `RouterErrorBoundary` consumers.\n */\nexport function createDismissableError(\n router: Router,\n): RouterSource<DismissableErrorSnapshot> {\n const cached = dismissableCache.get(router);\n\n if (cached) {\n return cached;\n }\n\n const errorSource = getErrorSource(router);\n\n let dismissedVersion = -1;\n\n const computeSnapshot = (): DismissableErrorSnapshot => {\n const snap = errorSource.getSnapshot();\n const isDismissed = snap.version <= dismissedVersion;\n\n return {\n error: isDismissed ? null : snap.error,\n toRoute: isDismissed ? null : snap.toRoute,\n fromRoute: isDismissed ? null : snap.fromRoute,\n version: snap.version,\n resetError,\n };\n };\n\n const source = new BaseSource<DismissableErrorSnapshot>(computeSnapshot(), {\n onFirstSubscribe: () => {\n unsubFromError = errorSource.subscribe(() => {\n source.updateSnapshot(computeSnapshot());\n });\n },\n onLastUnsubscribe: () => {\n disconnect();\n },\n });\n\n let unsubFromError: (() => void) | null = null;\n\n function resetError(): void {\n dismissedVersion = errorSource.getSnapshot().version;\n source.updateSnapshot(computeSnapshot());\n }\n\n function disconnect(): void {\n const unsub = unsubFromError;\n\n unsubFromError = null;\n unsub?.();\n }\n\n const wrapper: RouterSource<DismissableErrorSnapshot> = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n\n dismissableCache.set(router, wrapper);\n\n return wrapper;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { areRoutesRelated } from \"@real-router/route-utils\";\n\nimport type { Router } from \"@real-router/core\";\n\nexport interface ActiveNameSelector {\n /**\n * Subscribes to active-state changes of a specific route name.\n * Listener is called only when `isActive(routeName)` for this name transitions.\n * Returns an unsubscribe function.\n */\n subscribe: (routeName: string, listener: () => void) => () => void;\n /**\n * O(1) active check for the given route name (non-strict by default —\n * matches descendants). Uses the shared underlying router subscription.\n */\n isActive: (routeName: string) => boolean;\n /** No-op on the cached wrapper. */\n destroy: () => void;\n}\n\nconst selectorCache = new WeakMap<Router, ActiveNameSelector>();\n\n/**\n * Per-router cached selector providing O(1) active-route checks with one\n * shared router subscription for any number of distinct `routeName`\n * consumers.\n *\n * **When to use:** framework `Link` components that need an active boolean\n * without custom `params` / `activeStrict` / `ignoreQueryParams` — e.g. the\n * common navigation-link case. Multiple `<Link>` components with different\n * `routeName` share ONE `router.subscribe` handle instead of creating one\n * per Link (which is what `createActiveRouteSource(router, name)` does — it\n * caches per-name, so N names = N subscriptions).\n *\n * **When NOT to use:** Link needs `activeStrict: true`, custom `routeParams`,\n * or `ignoreQueryParams: false`. Fall back to `createActiveRouteSource` —\n * its cache handles the full argument surface.\n *\n * Based on the `routeSelector` pattern pioneered by `@real-router/solid`'s\n * `RouterProvider` (`createSelector` + `areRoutesRelated`). This helper\n * ports it to framework-agnostic API so Vue / React / Preact / Svelte /\n * Angular Link components can adopt the same fast path.\n *\n * @see Solid reference implementation — `packages/solid/src/RouterProvider.tsx`\n */\nexport function createActiveNameSelector(router: Router): ActiveNameSelector {\n const cached = selectorCache.get(router);\n\n if (cached) {\n return cached;\n }\n\n // listeners per-name — re-evaluated on every router transition\n const listenersByName = new Map<string, Set<() => void>>();\n // cached active state per-name — used to diff before notifying\n const activeByName = new Map<string, boolean>();\n\n let routerUnsubscribe: (() => void) | null = null;\n\n const isActiveNonStrict = (routeName: string): boolean => {\n const current = router.getState();\n\n if (!current) {\n return false;\n }\n\n return (\n current.name === routeName || current.name.startsWith(`${routeName}.`)\n );\n };\n\n const connect = (): void => {\n routerUnsubscribe = router.subscribe((next) => {\n for (const [routeName, listeners] of listenersByName) {\n // Cheap pre-filter: if neither new nor previous route is related\n // to this name, its active state cannot have changed.\n const isNewRelated = areRoutesRelated(routeName, next.route.name);\n const isPrevRelated =\n next.previousRoute &&\n areRoutesRelated(routeName, next.previousRoute.name);\n\n if (!isNewRelated && !isPrevRelated) {\n continue;\n }\n\n // activeByName always has an entry for names present in listenersByName —\n // subscribe() seeds it, and we only iterate over listenersByName.\n const prevActive = activeByName.get(routeName) === true;\n const nextActive = isActiveNonStrict(routeName);\n\n if (prevActive === nextActive) {\n continue;\n }\n\n activeByName.set(routeName, nextActive);\n for (const listener of listeners) {\n listener();\n }\n }\n });\n };\n\n const disconnect = (): void => {\n const unsub = routerUnsubscribe;\n\n routerUnsubscribe = null;\n unsub?.();\n };\n\n const subscribe = (routeName: string, listener: () => void): (() => void) => {\n let listeners = listenersByName.get(routeName);\n\n if (!listeners) {\n listeners = new Set();\n listenersByName.set(routeName, listeners);\n activeByName.set(routeName, isActiveNonStrict(routeName));\n }\n\n listeners.add(listener);\n\n if (!routerUnsubscribe) {\n connect();\n }\n\n let unsubscribed = false;\n\n return () => {\n if (unsubscribed) {\n return;\n }\n\n unsubscribed = true;\n listeners.delete(listener);\n\n if (listeners.size === 0) {\n listenersByName.delete(routeName);\n activeByName.delete(routeName);\n }\n\n if (listenersByName.size === 0) {\n disconnect();\n }\n };\n };\n\n const isActive = (routeName: string): boolean => {\n const cachedActive = activeByName.get(routeName);\n\n if (cachedActive !== undefined) {\n return cachedActive;\n }\n\n // Not subscribed — compute on demand.\n return isActiveNonStrict(routeName);\n };\n\n const selector: ActiveNameSelector = {\n subscribe,\n isActive,\n destroy: noopDestroy,\n };\n\n selectorCache.set(router, selector);\n\n return selector;\n}\n\nfunction noopDestroy(): void {\n // Shared cached selector — external destroy() is a no-op.\n}\n"],"mappings":"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"}
|
package/dist/esm/index.d.mts
CHANGED
|
@@ -17,6 +17,20 @@ interface RouterSource<T> {
|
|
|
17
17
|
interface ActiveRouteSourceOptions {
|
|
18
18
|
strict?: boolean;
|
|
19
19
|
ignoreQueryParams?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* URL fragment match (#532). When defined, the source is active iff the
|
|
22
|
+
* route matches AND `state.context.url.hash` (decoded, populated by
|
|
23
|
+
* browser/navigation URL plugins) equals this value:
|
|
24
|
+
*
|
|
25
|
+
* - `undefined` (default): hash is ignored — legacy route-only matching.
|
|
26
|
+
* - `""`: active only when the current URL has no fragment (or empty).
|
|
27
|
+
* - `"value"`: active only when the current fragment equals `"value"`.
|
|
28
|
+
*
|
|
29
|
+
* Hash-plugin runtimes leave `state.context.url` undefined, so any non-
|
|
30
|
+
* undefined `hash` option will produce `false` there — consistent with the
|
|
31
|
+
* documented limitation that hash-plugin doesn't support URL fragments.
|
|
32
|
+
*/
|
|
33
|
+
hash?: string;
|
|
20
34
|
}
|
|
21
35
|
interface RouterTransitionSnapshot {
|
|
22
36
|
isTransitioning: boolean;
|
|
@@ -193,19 +207,29 @@ interface ActiveNameSelector {
|
|
|
193
207
|
declare function createActiveNameSelector(router: Router): ActiveNameSelector;
|
|
194
208
|
//#endregion
|
|
195
209
|
//#region src/normalizeActiveOptions.d.ts
|
|
210
|
+
/**
|
|
211
|
+
* Normalized options shape — booleans are required (filled with defaults),
|
|
212
|
+
* `hash` stays `string | undefined` because `undefined` is the meaningful
|
|
213
|
+
* "ignore hash" sentinel that callers pass intentionally.
|
|
214
|
+
*/
|
|
215
|
+
interface NormalizedActiveOptions {
|
|
216
|
+
strict: boolean;
|
|
217
|
+
ignoreQueryParams: boolean;
|
|
218
|
+
hash: string | undefined;
|
|
219
|
+
}
|
|
196
220
|
/**
|
|
197
221
|
* Default options for `createActiveRouteSource` and adapter-level helpers.
|
|
198
222
|
*
|
|
199
223
|
* Frozen to prevent accidental mutation by consumers.
|
|
200
224
|
*/
|
|
201
|
-
declare const DEFAULT_ACTIVE_OPTIONS: Readonly<
|
|
225
|
+
declare const DEFAULT_ACTIVE_OPTIONS: Readonly<NormalizedActiveOptions>;
|
|
202
226
|
/**
|
|
203
227
|
* Normalizes partial `ActiveRouteSourceOptions` into a fully-defaulted object.
|
|
204
228
|
*
|
|
205
229
|
* Use this to produce a stable options record for comparison, caching, or
|
|
206
230
|
* downstream consumers that require all fields present.
|
|
207
231
|
*/
|
|
208
|
-
declare function normalizeActiveOptions(options?: ActiveRouteSourceOptions):
|
|
232
|
+
declare function normalizeActiveOptions(options?: ActiveRouteSourceOptions): NormalizedActiveOptions;
|
|
209
233
|
//#endregion
|
|
210
234
|
//#region src/canonicalJson.d.ts
|
|
211
235
|
/**
|
package/dist/esm/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/types.ts","../../src/createRouteSource.ts","../../src/createRouteNodeSource.ts","../../src/createActiveRouteSource.ts","../../src/createTransitionSource.ts","../../src/createErrorSource.ts","../../src/createDismissableError.ts","../../src/createActiveNameSelector.ts","../../src/normalizeActiveOptions.ts","../../src/canonicalJson.ts"],"mappings":";;;UAEiB,aAAA,WAAwB,MAAA,GAAS,MAAA;EAChD,KAAA,EAAO,KAAA,CAAM,CAAA;EACb,aAAA,EAAe,KAAA;AAAA;AAAA,UAGA,iBAAA,WAA4B,MAAA,GAAS,MAAA;EACpD,KAAA,EAAO,KAAA,CAAM,CAAA;EACb,aAAA,EAAe,KAAA;AAAA;AAAA,UAGA,YAAA;EACf,SAAA,GAAY,QAAA;EACZ,WAAA,QAAmB,CAAA;EACnB,OAAA;AAAA;AAAA,UAGe,wBAAA;EACf,MAAA;EACA,iBAAA;AAAA;AAAA,UAGe,wBAAA;EACf,eAAA;EACA,eAAA;EACA,OAAA,EAAS,KAAA;EACT,SAAA,EAAW,KAAA;AAAA;AAAA,UAGI,mBAAA;EACf,KAAA,EAAO,WAAA;EACP,OAAA,EAAS,KAAA;EACT,SAAA,EAAW,KAAA;EACX,OAAA;AAAA;AAAA,UAGe,wBAAA;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/types.ts","../../src/createRouteSource.ts","../../src/createRouteNodeSource.ts","../../src/createActiveRouteSource.ts","../../src/createTransitionSource.ts","../../src/createErrorSource.ts","../../src/createDismissableError.ts","../../src/createActiveNameSelector.ts","../../src/normalizeActiveOptions.ts","../../src/canonicalJson.ts"],"mappings":";;;UAEiB,aAAA,WAAwB,MAAA,GAAS,MAAA;EAChD,KAAA,EAAO,KAAA,CAAM,CAAA;EACb,aAAA,EAAe,KAAA;AAAA;AAAA,UAGA,iBAAA,WAA4B,MAAA,GAAS,MAAA;EACpD,KAAA,EAAO,KAAA,CAAM,CAAA;EACb,aAAA,EAAe,KAAA;AAAA;AAAA,UAGA,YAAA;EACf,SAAA,GAAY,QAAA;EACZ,WAAA,QAAmB,CAAA;EACnB,OAAA;AAAA;AAAA,UAGe,wBAAA;EACf,MAAA;EACA,iBAAA;EAjBO;;;;;;AAIT;;;;;;;EA2BE,IAAA;AAAA;AAAA,UAGe,wBAAA;EACf,eAAA;EACA,eAAA;EACA,OAAA,EAAS,KAAA;EACT,SAAA,EAAW,KAAA;AAAA;AAAA,UAGI,mBAAA;EACf,KAAA,EAAO,WAAA;EACP,OAAA,EAAS,KAAA;EACT,SAAA,EAAW,KAAA;EACX,OAAA;AAAA;AAAA,UAGe,wBAAA;EAvCY;EAyC3B,KAAA,EAAO,WAAA;EAzCqB;EA2C5B,OAAA,EAAS,KAAA;EA1CG;EA4CZ,SAAA,EAAW,KAAA;EA3CQ;EA6CnB,OAAA;EA5CO;EA8CP,UAAA;AAAA;;;;AA3DF;;;;;;iBCWgB,iBAAA,CAAkB,MAAA,EAAQ,MAAA,GAAS,YAAA,CAAa,aAAA;;;;ADXhE;;;;;;;;;;;;;;;;iBE0BgB,qBAAA,CACd,MAAA,EAAQ,MAAA,EACR,QAAA,WACC,YAAA,CAAa,iBAAA;;;;AF7BhB;;;;;;;;;;;;;;;;iBG6BgB,uBAAA,CACd,MAAA,EAAQ,MAAA,EACR,SAAA,UACA,MAAA,GAAS,MAAA,EACT,OAAA,GAAU,wBAAA,GACT,YAAA;;;iBCfa,sBAAA,CACd,MAAA,EAAQ,MAAA,GACP,YAAA,CAAa,wBAAA;AJrBhB;;;;;;;;;;;;;;AAAA,iBI8FgB,mBAAA,CACd,MAAA,EAAQ,MAAA,GACP,YAAA,CAAa,wBAAA;;;iBC9EA,iBAAA,CACd,MAAA,EAAQ,MAAA,GACP,YAAA,CAAa,mBAAA;ALpBhB;;;;;;;;;;;;;;AAAA,iBKoFgB,cAAA,CACd,MAAA,EAAQ,MAAA,GACP,YAAA,CAAa,mBAAA;;;;ALtFhB;;;;;;;;;;;;;;;;;;iBM4BgB,sBAAA,CACd,MAAA,EAAQ,MAAA,GACP,YAAA,CAAa,wBAAA;;;UC5BC,kBAAA;;APFjB;;;;EOQE,SAAA,GAAY,SAAA,UAAmB,QAAA;EPPlB;;;;EOYb,QAAA,GAAW,SAAA;EPbkB;EOe7B,OAAA;AAAA;;;;;;;;APVF;;;;;;;;;;;;;;;;iBOsCgB,wBAAA,CAAyB,MAAA,EAAQ,MAAA,GAAS,kBAAA;;;;;AP3C1D;;;UQKiB,uBAAA;EACf,MAAA;EACA,iBAAA;EACA,IAAA;AAAA;;;;;;cAQW,sBAAA,EAAwB,QAAA,CAAS,uBAAA;;;;;;;iBAa9B,sBAAA,CACd,OAAA,GAAU,wBAAA,GACT,uBAAA;;;;;;AR/BH;;;;;;;;;;;;;iBScgB,aAAA,CAAc,KAAA"}
|
package/dist/esm/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{areRoutesRelated as e}from"@real-router/route-utils";import{events as t}from"@real-router/core";import{getPluginApi as n}from"@real-router/core/api";var r=class{#e;#t=!1;#n=new Set;#r;#i;#a;constructor(e,t){this.#e=e,this.#r=t?.onFirstSubscribe,this.#i=t?.onLastUnsubscribe,this.#a=t?.onDestroy,this.subscribe=this.subscribe.bind(this),this.getSnapshot=this.getSnapshot.bind(this),this.destroy=this.destroy.bind(this)}subscribe(e){if(this.#t)return()=>{};let t=this.#n.size===0;return this.#n.add(e),t&&this.#r&&this.#r(),()=>{this.#n.delete(e),!this.#t&&this.#n.size===0&&this.#i&&this.#i()}}getSnapshot(){return this.#e}updateSnapshot(e){this.#t||(this.#e=e,this.#n.forEach(e=>{e()}))}destroy(){this.#t||(this.#t=!0,this.#a?.(),this.#n.clear())}};function i(e,t){return e===t
|
|
1
|
+
import{areRoutesRelated as e}from"@real-router/route-utils";import{events as t}from"@real-router/core";import{getPluginApi as n}from"@real-router/core/api";var r=class{#e;#t=!1;#n=new Set;#r;#i;#a;constructor(e,t){this.#e=e,this.#r=t?.onFirstSubscribe,this.#i=t?.onLastUnsubscribe,this.#a=t?.onDestroy,this.subscribe=this.subscribe.bind(this),this.getSnapshot=this.getSnapshot.bind(this),this.destroy=this.destroy.bind(this)}subscribe(e){if(this.#t)return()=>{};let t=this.#n.size===0;return this.#n.add(e),t&&this.#r&&this.#r(),()=>{this.#n.delete(e),!this.#t&&this.#n.size===0&&this.#i&&this.#i()}}getSnapshot(){return this.#e}updateSnapshot(e){this.#t||(this.#e=e,this.#n.forEach(e=>{e()}))}destroy(){this.#t||(this.#t=!0,this.#a?.(),this.#n.clear())}};function i(e,t){return e===t?e:e?.path!==t?.path||a(e)!==a(t)?t:e}function a(e){return(e?.context)?.url?.hash}function o(e){let t=null,n=()=>{let e=t;t=null,e?.()},a=new r({route:e.getState(),previousRoute:void 0},{onFirstSubscribe:()=>{t=e.subscribe(e=>{let t=a.getSnapshot(),n=i(t.route,e.route),r=i(t.previousRoute,e.previousRoute);(n!==t.route||r!==t.previousRoute)&&a.updateSnapshot({route:n,previousRoute:r})})},onLastUnsubscribe:n,onDestroy:n});return a}function s(e,t,n,r){let a=r?.route??t.getState(),o=r?.previousRoute,s=n===``||a!==void 0&&(a.name===n||a.name.startsWith(`${n}.`))?a:void 0;if(s===e.route&&o===e.previousRoute)return e;let c=i(e.route,s),l=i(e.previousRoute,o);return c===e.route&&l===e.previousRoute?e:{route:c,previousRoute:l}}const c=new WeakMap;function l(e,t){let n=c.get(e);n||(n=new Map,c.set(e,n));let r=n.get(t);if(!r){let i=u(e,t);r={subscribe:i.subscribe,getSnapshot:i.getSnapshot,destroy:d},n.set(t,r)}return r}function u(e,t){let n=null,i=e.shouldUpdateNode(t),a=new r(s({route:void 0,previousRoute:void 0},e,t),{onFirstSubscribe:()=>{let r=s(a.getSnapshot(),e,t);Object.is(r,a.getSnapshot())||a.updateSnapshot(r),n=e.subscribe(n=>{if(!i(n.route,n.previousRoute))return;let r=s(a.getSnapshot(),e,t,n);Object.is(a.getSnapshot(),r)||a.updateSnapshot(r)})},onLastUnsubscribe:()=>{let e=n;n=null,e?.()}});return a}function d(){}function f(e){return JSON.stringify(e,p)}function p(e,t){if(typeof t==`object`&&t&&!Array.isArray(t)){let e={},n=Object.keys(t).toSorted((e,t)=>e.localeCompare(t));for(let r of n)e[r]=t[r];return e}return t}const m=Object.freeze({strict:!1,ignoreQueryParams:!0,hash:void 0});function h(e){return{strict:e?.strict??m.strict,ignoreQueryParams:e?.ignoreQueryParams??m.ignoreQueryParams,hash:e?.hash}}const g=new WeakMap;function _(e,t,n,r){let{strict:i,ignoreQueryParams:a,hash:o}=h(r),s;try{let e=o===void 0?``:`#${o}`;s=`${t}|${f(n)}|${String(i)}|${String(a)}|${e}`}catch{s=void 0}if(s===void 0){let r=b(e,t,n,i,a,o);return{subscribe:r.subscribe,getSnapshot:r.getSnapshot,destroy:x}}let c=g.get(e);c||(c=new Map,g.set(e,c));let l=c.get(s);if(!l){let r=b(e,t,n,i,a,o);l={subscribe:r.subscribe,getSnapshot:r.getSnapshot,destroy:x},c.set(s,l)}return l}function v(e){return(e.getState()?.context)?.url?.hash??``}function y(e,t,n,r,i,a){return e.isActiveRoute(t,n,r,i)?a===void 0?!0:v(e)===a:!1}function b(t,n,i,a,o,s){let c=new r(y(t,n,i,a,o,s));return t.subscribe(r=>{let l=e(n,r.route.name),u=r.previousRoute&&e(n,r.previousRoute.name),d=s!==void 0&&(r.route.context?.url?.hashChanged??!1);if(!l&&!u&&!d)return;let f=l?y(t,n,i,a,o,s):!1;Object.is(c.getSnapshot(),f)||c.updateSnapshot(f)}),c}function x(){}const S={isTransitioning:!1,isLeaveApproved:!1,toRoute:null,fromRoute:null},C=new WeakMap;function w(e){let a=new r(S,{onDestroy:()=>{c.forEach(e=>{e()})}}),o=n(e),s=()=>{a.updateSnapshot(S)},c=[o.addEventListener(t.TRANSITION_START,(e,t)=>{let n=a.getSnapshot(),r=i(n.toRoute,e),o=i(n.fromRoute,t??null);(!n.isTransitioning||r!==n.toRoute||o!==n.fromRoute)&&a.updateSnapshot({isTransitioning:!0,isLeaveApproved:!1,toRoute:r,fromRoute:o})}),o.addEventListener(t.TRANSITION_LEAVE_APPROVE,(e,t)=>{let n=a.getSnapshot();a.updateSnapshot({isTransitioning:!0,isLeaveApproved:!0,toRoute:i(n.toRoute,e),fromRoute:i(n.fromRoute,t??null)})}),o.addEventListener(t.TRANSITION_SUCCESS,s),o.addEventListener(t.TRANSITION_ERROR,s),o.addEventListener(t.TRANSITION_CANCEL,s)];return a}function T(e){let t=C.get(e);if(!t){let n=w(e);t={subscribe:n.subscribe,getSnapshot:n.getSnapshot,destroy:E},C.set(e,t)}return t}function E(){}const D={error:null,toRoute:null,fromRoute:null,version:0},O=new WeakMap;function k(e){let i=0,a=new r(D,{onDestroy:()=>{s.forEach(e=>{e()})}}),o=n(e),s=[o.addEventListener(t.TRANSITION_ERROR,(e,t,n)=>{i++,a.updateSnapshot({error:n,toRoute:e??null,fromRoute:t??null,version:i})}),o.addEventListener(t.TRANSITION_SUCCESS,()=>{a.getSnapshot().error!==null&&a.updateSnapshot({error:null,toRoute:null,fromRoute:null,version:i})})];return a}function A(e){let t=O.get(e);if(!t){let n=k(e);t={subscribe:n.subscribe,getSnapshot:n.getSnapshot,destroy:j},O.set(e,t)}return t}function j(){}const M=new WeakMap;function N(e){let t=M.get(e);if(t)return t;let n=A(e),i=-1,a=()=>{let e=n.getSnapshot(),t=e.version<=i;return{error:t?null:e.error,toRoute:t?null:e.toRoute,fromRoute:t?null:e.fromRoute,version:e.version,resetError:c}},o=new r(a(),{onFirstSubscribe:()=>{s=n.subscribe(()=>{o.updateSnapshot(a())})},onLastUnsubscribe:()=>{l()}}),s=null;function c(){i=n.getSnapshot().version,o.updateSnapshot(a())}function l(){let e=s;s=null,e?.()}let u={subscribe:o.subscribe,getSnapshot:o.getSnapshot,destroy:P};return M.set(e,u),u}function P(){}const F=new WeakMap;function I(t){let n=F.get(t);if(n)return n;let r=new Map,i=new Map,a=null,o=e=>{let n=t.getState();return n?n.name===e||n.name.startsWith(`${e}.`):!1},s=()=>{a=t.subscribe(t=>{for(let[n,a]of r){let r=e(n,t.route.name),s=t.previousRoute&&e(n,t.previousRoute.name);if(!r&&!s)continue;let c=i.get(n)===!0,l=o(n);if(c!==l){i.set(n,l);for(let e of a)e()}}})},c=()=>{let e=a;a=null,e?.()},l={subscribe:(e,t)=>{let n=r.get(e);n||(n=new Set,r.set(e,n),i.set(e,o(e))),n.add(t),a||s();let l=!1;return()=>{l||(l=!0,n.delete(t),n.size===0&&(r.delete(e),i.delete(e)),r.size===0&&c())}},isActive:e=>{let t=i.get(e);return t===void 0?o(e):t},destroy:L};return F.set(t,l),l}function L(){}export{m as DEFAULT_ACTIVE_OPTIONS,f as canonicalJson,I as createActiveNameSelector,_ as createActiveRouteSource,N as createDismissableError,k as createErrorSource,l as createRouteNodeSource,o as createRouteSource,w as createTransitionSource,A as getErrorSource,T as getTransitionSource,h as normalizeActiveOptions};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/esm/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["#listeners","#onFirstSubscribe","#onLastUnsubscribe","#onDestroy","#currentSnapshot","#destroyed","noopDestroy","noopDestroy","noopDestroy","noopDestroy","noopDestroy"],"sources":["../../src/BaseSource.ts","../../src/stabilizeState.ts","../../src/createRouteSource.ts","../../src/computeSnapshot.ts","../../src/createRouteNodeSource.ts","../../src/canonicalJson.ts","../../src/normalizeActiveOptions.ts","../../src/createActiveRouteSource.ts","../../src/createTransitionSource.ts","../../src/createErrorSource.ts","../../src/createDismissableError.ts","../../src/createActiveNameSelector.ts"],"sourcesContent":["export interface BaseSourceOptions {\n onFirstSubscribe?: () => void;\n onLastUnsubscribe?: () => void;\n onDestroy?: () => void;\n}\n\nexport class BaseSource<T> {\n #currentSnapshot: T;\n #destroyed = false;\n\n readonly #listeners = new Set<() => void>();\n readonly #onFirstSubscribe: (() => void) | undefined;\n readonly #onLastUnsubscribe: (() => void) | undefined;\n readonly #onDestroy: (() => void) | undefined;\n\n constructor(initialSnapshot: T, options?: BaseSourceOptions) {\n this.#currentSnapshot = initialSnapshot;\n this.#onFirstSubscribe = options?.onFirstSubscribe;\n this.#onLastUnsubscribe = options?.onLastUnsubscribe;\n this.#onDestroy = options?.onDestroy;\n\n this.subscribe = this.subscribe.bind(this);\n this.getSnapshot = this.getSnapshot.bind(this);\n this.destroy = this.destroy.bind(this);\n }\n\n subscribe(listener: () => void): () => void {\n if (this.#destroyed) {\n return () => {};\n }\n\n const wasFirst = this.#listeners.size === 0;\n\n // Add listener BEFORE onFirstSubscribe so that if the reconciliation in\n // onFirstSubscribe calls updateSnapshot(), this listener receives the\n // notification. Critical for useSyncExternalStore in adapters — without\n // this the post-reconnection snapshot is missed and consumers render\n // stale data. (See Preact RouteView nested remount test.)\n this.#listeners.add(listener);\n\n if (wasFirst && this.#onFirstSubscribe) {\n this.#onFirstSubscribe();\n }\n\n return () => {\n this.#listeners.delete(listener);\n\n if (\n !this.#destroyed &&\n this.#listeners.size === 0 &&\n this.#onLastUnsubscribe\n ) {\n this.#onLastUnsubscribe();\n }\n };\n }\n\n getSnapshot(): T {\n return this.#currentSnapshot;\n }\n\n updateSnapshot(snapshot: T): void {\n /* v8 ignore next 2 -- @preserve: defensive guard unreachable via public API (destroy() removes router subscription first) */\n if (this.#destroyed) {\n return;\n }\n\n this.#currentSnapshot = snapshot;\n this.#listeners.forEach((listener) => {\n listener();\n });\n }\n\n destroy(): void {\n if (this.#destroyed) {\n return;\n }\n\n this.#destroyed = true;\n this.#onDestroy?.();\n this.#listeners.clear();\n }\n}\n","import type { State } from \"@real-router/core\";\n\n/**\n * State-aware stabilization for route snapshots.\n *\n * Compares `path` — the canonical representation of rendering-relevant\n * State fields (name + params). When path matches, returns `prev`\n * (preserving reference), enabling frameworks to skip re-renders.\n *\n * Ignores `meta` (internal: auto-increment id) and `transition`\n * (reference data: from, segments, reload) — they don't affect\n * what is rendered.\n *\n * Accepts `null` for compatibility with `RouterTransitionSnapshot`\n * (toRoute/fromRoute are `State | null`).\n *\n * @internal Not exported from package public API.\n */\nexport function stabilizeState<T extends State | null | undefined>(\n prev: T,\n next: T,\n): T {\n if (prev === next) {\n return prev;\n }\n if (prev?.path !== next?.path) {\n return next;\n }\n\n return prev;\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { stabilizeState } from \"./stabilizeState.js\";\n\nimport type { RouteSnapshot, RouterSource } from \"./types.js\";\nimport type { Router } from \"@real-router/core\";\n\n/**\n * Creates a source for the full route state.\n *\n * Uses a lazy-connection pattern: the router subscription is created when the\n * first listener subscribes and removed when the last listener unsubscribes.\n * This is compatible with React's useSyncExternalStore and Strict Mode.\n */\nexport function createRouteSource(router: Router): RouterSource<RouteSnapshot> {\n let routerUnsubscribe: (() => void) | null = null;\n\n const disconnect = (): void => {\n const unsub = routerUnsubscribe;\n\n routerUnsubscribe = null;\n unsub?.();\n };\n\n const source = new BaseSource<RouteSnapshot>(\n {\n route: router.getState(),\n previousRoute: undefined,\n },\n {\n onFirstSubscribe: () => {\n routerUnsubscribe = router.subscribe((next) => {\n const prev = source.getSnapshot();\n const newRoute = stabilizeState(prev.route, next.route);\n const newPreviousRoute = stabilizeState(\n prev.previousRoute,\n next.previousRoute,\n );\n\n if (\n newRoute !== prev.route ||\n newPreviousRoute !== prev.previousRoute\n ) {\n source.updateSnapshot({\n route: newRoute,\n previousRoute: newPreviousRoute,\n });\n }\n });\n },\n onLastUnsubscribe: disconnect,\n onDestroy: disconnect,\n },\n );\n\n return source;\n}\n","import { stabilizeState } from \"./stabilizeState.js\";\n\nimport type { RouteNodeSnapshot } from \"./types.js\";\nimport type { Router, SubscribeState } from \"@real-router/core\";\n\nexport function computeSnapshot(\n currentSnapshot: RouteNodeSnapshot,\n router: Router,\n nodeName: string,\n next?: SubscribeState,\n): RouteNodeSnapshot {\n const currentRoute = next?.route ?? router.getState();\n const previousRoute = next?.previousRoute;\n\n const isNodeActive =\n nodeName === \"\" ||\n (currentRoute !== undefined &&\n (currentRoute.name === nodeName ||\n currentRoute.name.startsWith(`${nodeName}.`)));\n\n const route = isNodeActive ? currentRoute : undefined;\n\n if (\n route === currentSnapshot.route &&\n previousRoute === currentSnapshot.previousRoute\n ) {\n return currentSnapshot;\n }\n\n const newRoute = stabilizeState(currentSnapshot.route, route);\n const newPreviousRoute = stabilizeState(\n currentSnapshot.previousRoute,\n previousRoute,\n );\n\n if (\n newRoute === currentSnapshot.route &&\n newPreviousRoute === currentSnapshot.previousRoute\n ) {\n return currentSnapshot;\n }\n\n return { route: newRoute, previousRoute: newPreviousRoute };\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { computeSnapshot } from \"./computeSnapshot.js\";\n\nimport type { RouteNodeSnapshot, RouterSource } from \"./types.js\";\nimport type { Router } from \"@real-router/core\";\n\nconst nodeSourceCache = new WeakMap<\n Router,\n Map<string, RouterSource<RouteNodeSnapshot>>\n>();\n\n/**\n * Creates a source scoped to a specific route node.\n *\n * **Per-router + per-nodeName cache:** repeated calls with the same\n * `(router, nodeName)` return the same shared instance. `N` consumers\n * calling `createRouteNodeSource(r, \"users\")` produce one router subscription\n * shared across all of them.\n *\n * Uses a lazy-connection pattern: the router subscription is created when the\n * first listener subscribes and removed when the last listener unsubscribes.\n * This is compatible with React's useSyncExternalStore and Strict Mode.\n *\n * `destroy()` on the returned source is a no-op — the shared instance lives\n * as long as the router itself (the WeakMap entry releases automatically on\n * router GC). Callers that need an isolated instance with working teardown\n * can use `buildRouteNodeSource` internally (not exported).\n */\nexport function createRouteNodeSource(\n router: Router,\n nodeName: string,\n): RouterSource<RouteNodeSnapshot> {\n let perRouter = nodeSourceCache.get(router);\n\n if (!perRouter) {\n perRouter = new Map();\n nodeSourceCache.set(router, perRouter);\n }\n\n let cached = perRouter.get(nodeName);\n\n if (!cached) {\n const source = buildRouteNodeSource(router, nodeName);\n\n // Wrap with no-op destroy. The shared source lives with the router.\n cached = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n perRouter.set(nodeName, cached);\n }\n\n return cached;\n}\n\nfunction buildRouteNodeSource(\n router: Router,\n nodeName: string,\n): RouterSource<RouteNodeSnapshot> {\n let routerUnsubscribe: (() => void) | null = null;\n\n // Built once per cached source instance; safe — createRouteNodeSource is\n // itself per-(router, nodeName) cached, so shouldUpdate is called once.\n const shouldUpdate = router.shouldUpdateNode(nodeName);\n\n const initialSnapshot: RouteNodeSnapshot = {\n route: undefined,\n previousRoute: undefined,\n };\n\n const disconnect = (): void => {\n const unsub = routerUnsubscribe;\n\n routerUnsubscribe = null;\n unsub?.();\n };\n\n const source = new BaseSource<RouteNodeSnapshot>(\n computeSnapshot(initialSnapshot, router, nodeName),\n {\n onFirstSubscribe: () => {\n // Reconcile snapshot with current router state before connecting.\n // Covers reconnection after Activity hide/show cycles where the\n // source was disconnected and missed navigation events.\n const reconciled = computeSnapshot(\n source.getSnapshot(),\n router,\n nodeName,\n );\n\n if (!Object.is(reconciled, source.getSnapshot())) {\n source.updateSnapshot(reconciled);\n }\n\n // Connect to router on first subscription\n routerUnsubscribe = router.subscribe((next) => {\n if (!shouldUpdate(next.route, next.previousRoute)) {\n return;\n }\n\n const newSnapshot = computeSnapshot(\n source.getSnapshot(),\n router,\n nodeName,\n next,\n );\n\n if (!Object.is(source.getSnapshot(), newSnapshot)) {\n source.updateSnapshot(newSnapshot);\n }\n });\n },\n onLastUnsubscribe: disconnect,\n },\n );\n\n return source;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","/**\n * Serializes a value into a stable JSON string — object keys are sorted at\n * every level so that `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` produce the same\n * output.\n *\n * Used as a cache key for `createActiveRouteSource` so that equivalent params\n * objects share the same cached source regardless of key order.\n *\n * Edge cases:\n * - Arrays preserve order (canonical: index-ordered already).\n * - `undefined` values are dropped (standard JSON behaviour).\n * - `Symbol`, `BigInt`, `Date`, `Map`, `Set` etc. fall through to\n * `JSON.stringify` defaults — `Symbol` becomes `undefined`, `BigInt` throws.\n * In practice, route params carry primitives (`string | number | boolean`)\n * and such edge cases would hit a fresh source on cache miss — acceptable.\n */\nexport function canonicalJson(value: unknown): string {\n return JSON.stringify(value, replacer);\n}\n\nfunction replacer(_key: string, val: unknown): unknown {\n if (val !== null && typeof val === \"object\" && !Array.isArray(val)) {\n const sorted: Record<string, unknown> = {};\n const keys = Object.keys(val as Record<string, unknown>).toSorted(\n (left, right) => left.localeCompare(right),\n );\n\n for (const key of keys) {\n sorted[key] = (val as Record<string, unknown>)[key];\n }\n\n return sorted;\n }\n\n return val;\n}\n","import type { ActiveRouteSourceOptions } from \"./types.js\";\n\n/**\n * Default options for `createActiveRouteSource` and adapter-level helpers.\n *\n * Frozen to prevent accidental mutation by consumers.\n */\nexport const DEFAULT_ACTIVE_OPTIONS: Readonly<\n Required<ActiveRouteSourceOptions>\n> = Object.freeze({\n strict: false,\n ignoreQueryParams: true,\n});\n\n/**\n * Normalizes partial `ActiveRouteSourceOptions` into a fully-defaulted object.\n *\n * Use this to produce a stable options record for comparison, caching, or\n * downstream consumers that require all fields present.\n */\nexport function normalizeActiveOptions(\n options?: ActiveRouteSourceOptions,\n): Required<ActiveRouteSourceOptions> {\n return {\n strict: options?.strict ?? DEFAULT_ACTIVE_OPTIONS.strict,\n ignoreQueryParams:\n options?.ignoreQueryParams ?? DEFAULT_ACTIVE_OPTIONS.ignoreQueryParams,\n };\n}\n","import { areRoutesRelated } from \"@real-router/route-utils\";\n\nimport { BaseSource } from \"./BaseSource\";\nimport { canonicalJson } from \"./canonicalJson.js\";\nimport { normalizeActiveOptions } from \"./normalizeActiveOptions.js\";\n\nimport type { ActiveRouteSourceOptions, RouterSource } from \"./types.js\";\nimport type { Params, Router } from \"@real-router/core\";\n\nconst activeSourceCache = new WeakMap<\n Router,\n Map<string, RouterSource<boolean>>\n>();\n\n/**\n * Creates a source tracking whether a route (with given params/options) is active.\n *\n * **Per-router + canonical-args cache:** repeated calls with equivalent\n * arguments return the same shared instance. Param key order doesn't matter\n * (`{ a:1, b:2 }` and `{ b:2, a:1 }` hit the same cache entry via\n * `canonicalJson`).\n *\n * `destroy()` is a no-op — shared sources live with the router. The router\n * subscription stays active while any consumer subscribes; when the router\n * is garbage-collected, the WeakMap entry releases automatically.\n *\n * Edge cases: `Symbol`/`BigInt` in params bypass `canonicalJson` and produce\n * an unstable cache key — these will simply miss the cache and create a new\n * source on each call. Practical params are primitives, so this is not a\n * concern in real usage.\n */\nexport function createActiveRouteSource(\n router: Router,\n routeName: string,\n params?: Params,\n options?: ActiveRouteSourceOptions,\n): RouterSource<boolean> {\n const { strict, ignoreQueryParams } = normalizeActiveOptions(options);\n\n // BigInt/Symbol/circular refs cannot be serialized — fall back to creating\n // a fresh (non-cached) source. Callers pass these edge-case params rarely;\n // the extra allocation is acceptable.\n let key: string | undefined;\n\n try {\n key = `${routeName}|${canonicalJson(params)}|${String(strict)}|${String(ignoreQueryParams)}`;\n } catch {\n key = undefined;\n }\n\n if (key === undefined) {\n const source = buildActiveRouteSource(\n router,\n routeName,\n params,\n strict,\n ignoreQueryParams,\n );\n\n return {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n }\n\n let perRouter = activeSourceCache.get(router);\n\n if (!perRouter) {\n perRouter = new Map();\n activeSourceCache.set(router, perRouter);\n }\n\n let cached = perRouter.get(key);\n\n if (!cached) {\n const source = buildActiveRouteSource(\n router,\n routeName,\n params,\n strict,\n ignoreQueryParams,\n );\n\n cached = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n perRouter.set(key, cached);\n }\n\n return cached;\n}\n\nfunction buildActiveRouteSource(\n router: Router,\n routeName: string,\n params: Params | undefined,\n strict: boolean,\n ignoreQueryParams: boolean,\n): RouterSource<boolean> {\n const initialValue = router.isActiveRoute(\n routeName,\n params,\n strict,\n ignoreQueryParams,\n );\n\n const source = new BaseSource(initialValue);\n\n // Eager connection: subscribe to router immediately. This source is only\n // ever reached through the cached public `createActiveRouteSource`, whose\n // returned wrapper has a no-op destroy. The source lives with the router;\n // the router.subscribe handle is released on router GC.\n router.subscribe((next) => {\n const isNewRelated = areRoutesRelated(routeName, next.route.name);\n const isPrevRelated =\n next.previousRoute &&\n areRoutesRelated(routeName, next.previousRoute.name);\n\n if (!isNewRelated && !isPrevRelated) {\n return;\n }\n\n // If new route is not related, we know the route is inactive —\n // avoid calling isActiveRoute for the optimization\n const newValue = isNewRelated\n ? router.isActiveRoute(routeName, params, strict, ignoreQueryParams)\n : false;\n\n if (!Object.is(source.getSnapshot(), newValue)) {\n source.updateSnapshot(newValue);\n }\n });\n\n return source;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { events } from \"@real-router/core\";\nimport { getPluginApi } from \"@real-router/core/api\";\n\nimport { BaseSource } from \"./BaseSource\";\nimport { stabilizeState } from \"./stabilizeState.js\";\n\nimport type { RouterTransitionSnapshot, RouterSource } from \"./types.js\";\nimport type { Router, State } from \"@real-router/core\";\n\nconst IDLE_SNAPSHOT: RouterTransitionSnapshot = {\n isTransitioning: false,\n isLeaveApproved: false,\n toRoute: null,\n fromRoute: null,\n};\n\nconst transitionSourceCache = new WeakMap<\n Router,\n RouterSource<RouterTransitionSnapshot>\n>();\n\nexport function createTransitionSource(\n router: Router,\n): RouterSource<RouterTransitionSnapshot> {\n const source = new BaseSource(IDLE_SNAPSHOT, {\n onDestroy: () => {\n unsubs.forEach((unsub) => {\n unsub();\n });\n },\n });\n\n const api = getPluginApi(router);\n\n const resetToIdle = (): void => {\n source.updateSnapshot(IDLE_SNAPSHOT);\n };\n\n // Eager connection: subscribe to router events immediately\n const unsubs = [\n api.addEventListener(\n events.TRANSITION_START,\n (toState: State, fromState?: State) => {\n const prev = source.getSnapshot();\n const newToRoute = stabilizeState(prev.toRoute, toState);\n const newFromRoute = stabilizeState(prev.fromRoute, fromState ?? null);\n\n if (\n !prev.isTransitioning ||\n newToRoute !== prev.toRoute ||\n newFromRoute !== prev.fromRoute\n ) {\n source.updateSnapshot({\n isTransitioning: true,\n isLeaveApproved: false,\n toRoute: newToRoute,\n fromRoute: newFromRoute,\n });\n }\n },\n ),\n api.addEventListener(\n events.TRANSITION_LEAVE_APPROVE,\n (toState: State, fromState?: State) => {\n const prev = source.getSnapshot();\n\n source.updateSnapshot({\n isTransitioning: true,\n isLeaveApproved: true,\n toRoute: stabilizeState(prev.toRoute, toState),\n fromRoute: stabilizeState(prev.fromRoute, fromState ?? null),\n });\n },\n ),\n api.addEventListener(events.TRANSITION_SUCCESS, resetToIdle),\n api.addEventListener(events.TRANSITION_ERROR, resetToIdle),\n api.addEventListener(events.TRANSITION_CANCEL, resetToIdle),\n ];\n\n return source;\n}\n\n/**\n * Returns a per-router cached transition source shared across all consumers.\n *\n * Safe to call destroy() — the cached source ignores external destroy() calls\n * and lives until the router itself is garbage-collected (the WeakMap entry\n * releases automatically).\n *\n * Use this in framework adapters (React/Preact/Solid/Vue/Svelte/Angular) to\n * share a single TransitionSource instance across all mount/unmount cycles.\n *\n * For isolated/advanced use (ad-hoc, short-lived, per-owner teardown), call\n * `createTransitionSource(router)` directly — it returns a fresh instance with\n * a working `destroy()`.\n */\nexport function getTransitionSource(\n router: Router,\n): RouterSource<RouterTransitionSnapshot> {\n let cached = transitionSourceCache.get(router);\n\n if (!cached) {\n const source = createTransitionSource(router);\n\n // Wrap with no-op destroy. The underlying source is shared across all\n // consumers; letting any one consumer call destroy() would tear it down\n // for the rest. The source lives as long as the router (WeakMap key).\n cached = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n transitionSourceCache.set(router, cached);\n }\n\n return cached;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { events } from \"@real-router/core\";\nimport { getPluginApi } from \"@real-router/core/api\";\n\nimport { BaseSource } from \"./BaseSource\";\n\nimport type { RouterErrorSnapshot, RouterSource } from \"./types.js\";\nimport type { Router, State, RouterError } from \"@real-router/core\";\n\nconst INITIAL_SNAPSHOT: RouterErrorSnapshot = {\n error: null,\n toRoute: null,\n fromRoute: null,\n version: 0,\n};\n\nconst errorSourceCache = new WeakMap<\n Router,\n RouterSource<RouterErrorSnapshot>\n>();\n\nexport function createErrorSource(\n router: Router,\n): RouterSource<RouterErrorSnapshot> {\n let errorVersion = 0;\n\n const source = new BaseSource(INITIAL_SNAPSHOT, {\n onDestroy: () => {\n unsubs.forEach((unsub) => {\n unsub();\n });\n },\n });\n\n const api = getPluginApi(router);\n\n // Eager connection: subscribe to router events immediately\n const unsubs = [\n api.addEventListener(\n events.TRANSITION_ERROR,\n (\n toState: State | undefined,\n fromState: State | undefined,\n err: RouterError,\n ) => {\n errorVersion++;\n source.updateSnapshot({\n error: err,\n toRoute: toState ?? null,\n /* v8 ignore next -- @preserve: fromState undefined only during start() error; unreachable via navigate() */\n fromRoute: fromState ?? null,\n version: errorVersion,\n });\n },\n ),\n api.addEventListener(events.TRANSITION_SUCCESS, () => {\n // Skip if no error — avoids unnecessary re-renders.\n // BaseSource.updateSnapshot() always notifies listeners (new object = new ref),\n // and useSyncExternalStore compares via Object.is().\n if (source.getSnapshot().error !== null) {\n source.updateSnapshot({\n error: null,\n toRoute: null,\n fromRoute: null,\n version: errorVersion,\n });\n }\n }),\n ];\n\n return source;\n}\n\n/**\n * Returns a per-router cached error source shared across all consumers.\n *\n * Safe to call destroy() — the cached source ignores external destroy() calls\n * and lives until the router itself is garbage-collected (the WeakMap entry\n * releases automatically).\n *\n * Use this in framework adapters (React/Preact/Solid/Vue/Svelte/Angular) to\n * share a single ErrorSource instance across all mount/unmount cycles.\n *\n * For isolated/advanced use (ad-hoc, short-lived, per-owner teardown), call\n * `createErrorSource(router)` directly — it returns a fresh instance with a\n * working `destroy()`.\n */\nexport function getErrorSource(\n router: Router,\n): RouterSource<RouterErrorSnapshot> {\n let cached = errorSourceCache.get(router);\n\n if (!cached) {\n const source = createErrorSource(router);\n\n // Wrap with no-op destroy. The underlying source is shared across all\n // consumers; letting any one consumer call destroy() would tear it down\n // for the rest. The source lives as long as the router (WeakMap key).\n cached = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n errorSourceCache.set(router, cached);\n }\n\n return cached;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { getErrorSource } from \"./createErrorSource\";\n\nimport type { DismissableErrorSnapshot, RouterSource } from \"./types.js\";\nimport type { Router } from \"@real-router/core\";\n\nconst dismissableCache = new WeakMap<\n Router,\n RouterSource<DismissableErrorSnapshot>\n>();\n\n/**\n * Returns a per-router cached source that wraps `getErrorSource(router)` with\n * an integrated \"dismissed\" version counter, exposing a single reactive\n * snapshot `{ error, toRoute, fromRoute, version, resetError }`.\n *\n * Each `RouterErrorBoundary` in a framework adapter subscribes to this one\n * source instead of re-implementing the `dismissedVersion` state pattern\n * locally. The 6-copy duplicate across adapters collapses to this helper.\n *\n * **Semantics:**\n * - `error` is non-null only when `underlying.version > dismissedVersion`.\n * - `resetError()` sets `dismissedVersion = current underlying version`,\n * immediately clearing `error` to `null` and notifying all listeners.\n * - A subsequent `TRANSITION_ERROR` advances `version` beyond `dismissedVersion`,\n * so `error` becomes non-null again — no additional plumbing needed.\n *\n * **Cached:** one instance per router. `destroy()` on the returned source is\n * a no-op. Shared across all `RouterErrorBoundary` consumers.\n */\nexport function createDismissableError(\n router: Router,\n): RouterSource<DismissableErrorSnapshot> {\n const cached = dismissableCache.get(router);\n\n if (cached) {\n return cached;\n }\n\n const errorSource = getErrorSource(router);\n\n let dismissedVersion = -1;\n\n const computeSnapshot = (): DismissableErrorSnapshot => {\n const snap = errorSource.getSnapshot();\n const isDismissed = snap.version <= dismissedVersion;\n\n return {\n error: isDismissed ? null : snap.error,\n toRoute: isDismissed ? null : snap.toRoute,\n fromRoute: isDismissed ? null : snap.fromRoute,\n version: snap.version,\n resetError,\n };\n };\n\n const source = new BaseSource<DismissableErrorSnapshot>(computeSnapshot(), {\n onFirstSubscribe: () => {\n unsubFromError = errorSource.subscribe(() => {\n source.updateSnapshot(computeSnapshot());\n });\n },\n onLastUnsubscribe: () => {\n disconnect();\n },\n });\n\n let unsubFromError: (() => void) | null = null;\n\n function resetError(): void {\n dismissedVersion = errorSource.getSnapshot().version;\n source.updateSnapshot(computeSnapshot());\n }\n\n function disconnect(): void {\n const unsub = unsubFromError;\n\n unsubFromError = null;\n unsub?.();\n }\n\n const wrapper: RouterSource<DismissableErrorSnapshot> = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n\n dismissableCache.set(router, wrapper);\n\n return wrapper;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { areRoutesRelated } from \"@real-router/route-utils\";\n\nimport type { Router } from \"@real-router/core\";\n\nexport interface ActiveNameSelector {\n /**\n * Subscribes to active-state changes of a specific route name.\n * Listener is called only when `isActive(routeName)` for this name transitions.\n * Returns an unsubscribe function.\n */\n subscribe: (routeName: string, listener: () => void) => () => void;\n /**\n * O(1) active check for the given route name (non-strict by default —\n * matches descendants). Uses the shared underlying router subscription.\n */\n isActive: (routeName: string) => boolean;\n /** No-op on the cached wrapper. */\n destroy: () => void;\n}\n\nconst selectorCache = new WeakMap<Router, ActiveNameSelector>();\n\n/**\n * Per-router cached selector providing O(1) active-route checks with one\n * shared router subscription for any number of distinct `routeName`\n * consumers.\n *\n * **When to use:** framework `Link` components that need an active boolean\n * without custom `params` / `activeStrict` / `ignoreQueryParams` — e.g. the\n * common navigation-link case. Multiple `<Link>` components with different\n * `routeName` share ONE `router.subscribe` handle instead of creating one\n * per Link (which is what `createActiveRouteSource(router, name)` does — it\n * caches per-name, so N names = N subscriptions).\n *\n * **When NOT to use:** Link needs `activeStrict: true`, custom `routeParams`,\n * or `ignoreQueryParams: false`. Fall back to `createActiveRouteSource` —\n * its cache handles the full argument surface.\n *\n * Based on the `routeSelector` pattern pioneered by `@real-router/solid`'s\n * `RouterProvider` (`createSelector` + `areRoutesRelated`). This helper\n * ports it to framework-agnostic API so Vue / React / Preact / Svelte /\n * Angular Link components can adopt the same fast path.\n *\n * @see Solid reference implementation — `packages/solid/src/RouterProvider.tsx`\n */\nexport function createActiveNameSelector(router: Router): ActiveNameSelector {\n const cached = selectorCache.get(router);\n\n if (cached) {\n return cached;\n }\n\n // listeners per-name — re-evaluated on every router transition\n const listenersByName = new Map<string, Set<() => void>>();\n // cached active state per-name — used to diff before notifying\n const activeByName = new Map<string, boolean>();\n\n let routerUnsubscribe: (() => void) | null = null;\n\n const isActiveNonStrict = (routeName: string): boolean => {\n const current = router.getState();\n\n if (!current) {\n return false;\n }\n\n return (\n current.name === routeName || current.name.startsWith(`${routeName}.`)\n );\n };\n\n const connect = (): void => {\n routerUnsubscribe = router.subscribe((next) => {\n for (const [routeName, listeners] of listenersByName) {\n // Cheap pre-filter: if neither new nor previous route is related\n // to this name, its active state cannot have changed.\n const isNewRelated = areRoutesRelated(routeName, next.route.name);\n const isPrevRelated =\n next.previousRoute &&\n areRoutesRelated(routeName, next.previousRoute.name);\n\n if (!isNewRelated && !isPrevRelated) {\n continue;\n }\n\n // activeByName always has an entry for names present in listenersByName —\n // subscribe() seeds it, and we only iterate over listenersByName.\n const prevActive = activeByName.get(routeName) === true;\n const nextActive = isActiveNonStrict(routeName);\n\n if (prevActive === nextActive) {\n continue;\n }\n\n activeByName.set(routeName, nextActive);\n for (const listener of listeners) {\n listener();\n }\n }\n });\n };\n\n const disconnect = (): void => {\n const unsub = routerUnsubscribe;\n\n routerUnsubscribe = null;\n unsub?.();\n };\n\n const subscribe = (routeName: string, listener: () => void): (() => void) => {\n let listeners = listenersByName.get(routeName);\n\n if (!listeners) {\n listeners = new Set();\n listenersByName.set(routeName, listeners);\n activeByName.set(routeName, isActiveNonStrict(routeName));\n }\n\n listeners.add(listener);\n\n if (!routerUnsubscribe) {\n connect();\n }\n\n let unsubscribed = false;\n\n return () => {\n if (unsubscribed) {\n return;\n }\n\n unsubscribed = true;\n listeners.delete(listener);\n\n if (listeners.size === 0) {\n listenersByName.delete(routeName);\n activeByName.delete(routeName);\n }\n\n if (listenersByName.size === 0) {\n disconnect();\n }\n };\n };\n\n const isActive = (routeName: string): boolean => {\n const cachedActive = activeByName.get(routeName);\n\n if (cachedActive !== undefined) {\n return cachedActive;\n }\n\n // Not subscribed — compute on demand.\n return isActiveNonStrict(routeName);\n };\n\n const selector: ActiveNameSelector = {\n subscribe,\n isActive,\n destroy: noopDestroy,\n };\n\n selectorCache.set(router, selector);\n\n return selector;\n}\n\nfunction noopDestroy(): void {\n // Shared cached selector — external destroy() is a no-op.\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["#listeners","#onFirstSubscribe","#onLastUnsubscribe","#onDestroy","#currentSnapshot","#destroyed","readContextHash","noopDestroy","noopDestroy","noopDestroy","noopDestroy","noopDestroy"],"sources":["../../src/BaseSource.ts","../../src/stabilizeState.ts","../../src/createRouteSource.ts","../../src/computeSnapshot.ts","../../src/createRouteNodeSource.ts","../../src/canonicalJson.ts","../../src/normalizeActiveOptions.ts","../../src/createActiveRouteSource.ts","../../src/createTransitionSource.ts","../../src/createErrorSource.ts","../../src/createDismissableError.ts","../../src/createActiveNameSelector.ts"],"sourcesContent":["export interface BaseSourceOptions {\n onFirstSubscribe?: () => void;\n onLastUnsubscribe?: () => void;\n onDestroy?: () => void;\n}\n\nexport class BaseSource<T> {\n #currentSnapshot: T;\n #destroyed = false;\n\n readonly #listeners = new Set<() => void>();\n readonly #onFirstSubscribe: (() => void) | undefined;\n readonly #onLastUnsubscribe: (() => void) | undefined;\n readonly #onDestroy: (() => void) | undefined;\n\n constructor(initialSnapshot: T, options?: BaseSourceOptions) {\n this.#currentSnapshot = initialSnapshot;\n this.#onFirstSubscribe = options?.onFirstSubscribe;\n this.#onLastUnsubscribe = options?.onLastUnsubscribe;\n this.#onDestroy = options?.onDestroy;\n\n this.subscribe = this.subscribe.bind(this);\n this.getSnapshot = this.getSnapshot.bind(this);\n this.destroy = this.destroy.bind(this);\n }\n\n subscribe(listener: () => void): () => void {\n if (this.#destroyed) {\n return () => {};\n }\n\n const wasFirst = this.#listeners.size === 0;\n\n // Add listener BEFORE onFirstSubscribe so that if the reconciliation in\n // onFirstSubscribe calls updateSnapshot(), this listener receives the\n // notification. Critical for useSyncExternalStore in adapters — without\n // this the post-reconnection snapshot is missed and consumers render\n // stale data. (See Preact RouteView nested remount test.)\n this.#listeners.add(listener);\n\n if (wasFirst && this.#onFirstSubscribe) {\n this.#onFirstSubscribe();\n }\n\n return () => {\n this.#listeners.delete(listener);\n\n if (\n !this.#destroyed &&\n this.#listeners.size === 0 &&\n this.#onLastUnsubscribe\n ) {\n this.#onLastUnsubscribe();\n }\n };\n }\n\n getSnapshot(): T {\n return this.#currentSnapshot;\n }\n\n updateSnapshot(snapshot: T): void {\n /* v8 ignore next 2 -- @preserve: defensive guard unreachable via public API (destroy() removes router subscription first) */\n if (this.#destroyed) {\n return;\n }\n\n this.#currentSnapshot = snapshot;\n this.#listeners.forEach((listener) => {\n listener();\n });\n }\n\n destroy(): void {\n if (this.#destroyed) {\n return;\n }\n\n this.#destroyed = true;\n this.#onDestroy?.();\n this.#listeners.clear();\n }\n}\n","import type { State } from \"@real-router/core\";\n\n/**\n * State-aware stabilization for route snapshots.\n *\n * Compares `path` (canonical name+params) AND `state.context.url.hash`\n * (URL fragment, #532). When both match, returns `prev` (preserving\n * reference) so frameworks can skip re-renders. When hash flips on a\n * same-path navigation (tab-style UI), returns `next` so consumers\n * subscribing through `useRoute()` see the new state.\n *\n * Ignores `meta` (internal: auto-increment id), `transition` (reference\n * data: from, segments, reload), and `state.context.navigation` /\n * `state.context.browser` (transient transition metadata) — they don't\n * affect what is rendered. `state.context.url.hash` is the only context\n * field that participates in render identity, because tab-style UIs\n * subscribe to it directly.\n *\n * Accepts `null` for compatibility with `RouterTransitionSnapshot`\n * (toRoute/fromRoute are `State | null`).\n *\n * @internal Not exported from package public API.\n */\nexport function stabilizeState<T extends State | null | undefined>(\n prev: T,\n next: T,\n): T {\n if (prev === next) {\n return prev;\n }\n if (prev?.path !== next?.path) {\n return next;\n }\n\n // After the path check, both must be the same non-null State (paths\n // matched, prev !== next reference). Read context.url.hash to detect\n // same-path-different-hash navigation (#532) — render-relevant for\n // tab-style UIs that subscribe via useRoute(). Optional chaining keeps\n // the access null-safe without forbidden non-null assertions.\n if (readContextHash(prev) !== readContextHash(next)) {\n return next;\n }\n\n return prev;\n}\n\nfunction readContextHash(state: State | null | undefined): string | undefined {\n const ctx = state?.context as { url?: { hash?: string } } | undefined;\n\n return ctx?.url?.hash;\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { stabilizeState } from \"./stabilizeState.js\";\n\nimport type { RouteSnapshot, RouterSource } from \"./types.js\";\nimport type { Router } from \"@real-router/core\";\n\n/**\n * Creates a source for the full route state.\n *\n * Uses a lazy-connection pattern: the router subscription is created when the\n * first listener subscribes and removed when the last listener unsubscribes.\n * This is compatible with React's useSyncExternalStore and Strict Mode.\n */\nexport function createRouteSource(router: Router): RouterSource<RouteSnapshot> {\n let routerUnsubscribe: (() => void) | null = null;\n\n const disconnect = (): void => {\n const unsub = routerUnsubscribe;\n\n routerUnsubscribe = null;\n unsub?.();\n };\n\n const source = new BaseSource<RouteSnapshot>(\n {\n route: router.getState(),\n previousRoute: undefined,\n },\n {\n onFirstSubscribe: () => {\n routerUnsubscribe = router.subscribe((next) => {\n const prev = source.getSnapshot();\n const newRoute = stabilizeState(prev.route, next.route);\n const newPreviousRoute = stabilizeState(\n prev.previousRoute,\n next.previousRoute,\n );\n\n if (\n newRoute !== prev.route ||\n newPreviousRoute !== prev.previousRoute\n ) {\n source.updateSnapshot({\n route: newRoute,\n previousRoute: newPreviousRoute,\n });\n }\n });\n },\n onLastUnsubscribe: disconnect,\n onDestroy: disconnect,\n },\n );\n\n return source;\n}\n","import { stabilizeState } from \"./stabilizeState.js\";\n\nimport type { RouteNodeSnapshot } from \"./types.js\";\nimport type { Router, SubscribeState } from \"@real-router/core\";\n\nexport function computeSnapshot(\n currentSnapshot: RouteNodeSnapshot,\n router: Router,\n nodeName: string,\n next?: SubscribeState,\n): RouteNodeSnapshot {\n const currentRoute = next?.route ?? router.getState();\n const previousRoute = next?.previousRoute;\n\n const isNodeActive =\n nodeName === \"\" ||\n (currentRoute !== undefined &&\n (currentRoute.name === nodeName ||\n currentRoute.name.startsWith(`${nodeName}.`)));\n\n const route = isNodeActive ? currentRoute : undefined;\n\n if (\n route === currentSnapshot.route &&\n previousRoute === currentSnapshot.previousRoute\n ) {\n return currentSnapshot;\n }\n\n const newRoute = stabilizeState(currentSnapshot.route, route);\n const newPreviousRoute = stabilizeState(\n currentSnapshot.previousRoute,\n previousRoute,\n );\n\n if (\n newRoute === currentSnapshot.route &&\n newPreviousRoute === currentSnapshot.previousRoute\n ) {\n return currentSnapshot;\n }\n\n return { route: newRoute, previousRoute: newPreviousRoute };\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { computeSnapshot } from \"./computeSnapshot.js\";\n\nimport type { RouteNodeSnapshot, RouterSource } from \"./types.js\";\nimport type { Router } from \"@real-router/core\";\n\nconst nodeSourceCache = new WeakMap<\n Router,\n Map<string, RouterSource<RouteNodeSnapshot>>\n>();\n\n/**\n * Creates a source scoped to a specific route node.\n *\n * **Per-router + per-nodeName cache:** repeated calls with the same\n * `(router, nodeName)` return the same shared instance. `N` consumers\n * calling `createRouteNodeSource(r, \"users\")` produce one router subscription\n * shared across all of them.\n *\n * Uses a lazy-connection pattern: the router subscription is created when the\n * first listener subscribes and removed when the last listener unsubscribes.\n * This is compatible with React's useSyncExternalStore and Strict Mode.\n *\n * `destroy()` on the returned source is a no-op — the shared instance lives\n * as long as the router itself (the WeakMap entry releases automatically on\n * router GC). Callers that need an isolated instance with working teardown\n * can use `buildRouteNodeSource` internally (not exported).\n */\nexport function createRouteNodeSource(\n router: Router,\n nodeName: string,\n): RouterSource<RouteNodeSnapshot> {\n let perRouter = nodeSourceCache.get(router);\n\n if (!perRouter) {\n perRouter = new Map();\n nodeSourceCache.set(router, perRouter);\n }\n\n let cached = perRouter.get(nodeName);\n\n if (!cached) {\n const source = buildRouteNodeSource(router, nodeName);\n\n // Wrap with no-op destroy. The shared source lives with the router.\n cached = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n perRouter.set(nodeName, cached);\n }\n\n return cached;\n}\n\nfunction buildRouteNodeSource(\n router: Router,\n nodeName: string,\n): RouterSource<RouteNodeSnapshot> {\n let routerUnsubscribe: (() => void) | null = null;\n\n // Built once per cached source instance; safe — createRouteNodeSource is\n // itself per-(router, nodeName) cached, so shouldUpdate is called once.\n const shouldUpdate = router.shouldUpdateNode(nodeName);\n\n const initialSnapshot: RouteNodeSnapshot = {\n route: undefined,\n previousRoute: undefined,\n };\n\n const disconnect = (): void => {\n const unsub = routerUnsubscribe;\n\n routerUnsubscribe = null;\n unsub?.();\n };\n\n const source = new BaseSource<RouteNodeSnapshot>(\n computeSnapshot(initialSnapshot, router, nodeName),\n {\n onFirstSubscribe: () => {\n // Reconcile snapshot with current router state before connecting.\n // Covers reconnection after Activity hide/show cycles where the\n // source was disconnected and missed navigation events.\n const reconciled = computeSnapshot(\n source.getSnapshot(),\n router,\n nodeName,\n );\n\n if (!Object.is(reconciled, source.getSnapshot())) {\n source.updateSnapshot(reconciled);\n }\n\n // Connect to router on first subscription\n routerUnsubscribe = router.subscribe((next) => {\n if (!shouldUpdate(next.route, next.previousRoute)) {\n return;\n }\n\n const newSnapshot = computeSnapshot(\n source.getSnapshot(),\n router,\n nodeName,\n next,\n );\n\n if (!Object.is(source.getSnapshot(), newSnapshot)) {\n source.updateSnapshot(newSnapshot);\n }\n });\n },\n onLastUnsubscribe: disconnect,\n },\n );\n\n return source;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","/**\n * Serializes a value into a stable JSON string — object keys are sorted at\n * every level so that `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` produce the same\n * output.\n *\n * Used as a cache key for `createActiveRouteSource` so that equivalent params\n * objects share the same cached source regardless of key order.\n *\n * Edge cases:\n * - Arrays preserve order (canonical: index-ordered already).\n * - `undefined` values are dropped (standard JSON behaviour).\n * - `Symbol`, `BigInt`, `Date`, `Map`, `Set` etc. fall through to\n * `JSON.stringify` defaults — `Symbol` becomes `undefined`, `BigInt` throws.\n * In practice, route params carry primitives (`string | number | boolean`)\n * and such edge cases would hit a fresh source on cache miss — acceptable.\n */\nexport function canonicalJson(value: unknown): string {\n return JSON.stringify(value, replacer);\n}\n\nfunction replacer(_key: string, val: unknown): unknown {\n if (val !== null && typeof val === \"object\" && !Array.isArray(val)) {\n const sorted: Record<string, unknown> = {};\n const keys = Object.keys(val as Record<string, unknown>).toSorted(\n (left, right) => left.localeCompare(right),\n );\n\n for (const key of keys) {\n sorted[key] = (val as Record<string, unknown>)[key];\n }\n\n return sorted;\n }\n\n return val;\n}\n","import type { ActiveRouteSourceOptions } from \"./types.js\";\n\n/**\n * Normalized options shape — booleans are required (filled with defaults),\n * `hash` stays `string | undefined` because `undefined` is the meaningful\n * \"ignore hash\" sentinel that callers pass intentionally.\n */\nexport interface NormalizedActiveOptions {\n strict: boolean;\n ignoreQueryParams: boolean;\n hash: string | undefined;\n}\n\n/**\n * Default options for `createActiveRouteSource` and adapter-level helpers.\n *\n * Frozen to prevent accidental mutation by consumers.\n */\nexport const DEFAULT_ACTIVE_OPTIONS: Readonly<NormalizedActiveOptions> =\n Object.freeze({\n strict: false,\n ignoreQueryParams: true,\n hash: undefined,\n });\n\n/**\n * Normalizes partial `ActiveRouteSourceOptions` into a fully-defaulted object.\n *\n * Use this to produce a stable options record for comparison, caching, or\n * downstream consumers that require all fields present.\n */\nexport function normalizeActiveOptions(\n options?: ActiveRouteSourceOptions,\n): NormalizedActiveOptions {\n return {\n strict: options?.strict ?? DEFAULT_ACTIVE_OPTIONS.strict,\n ignoreQueryParams:\n options?.ignoreQueryParams ?? DEFAULT_ACTIVE_OPTIONS.ignoreQueryParams,\n hash: options?.hash,\n };\n}\n","import { areRoutesRelated } from \"@real-router/route-utils\";\n\nimport { BaseSource } from \"./BaseSource\";\nimport { canonicalJson } from \"./canonicalJson.js\";\nimport { normalizeActiveOptions } from \"./normalizeActiveOptions.js\";\n\nimport type { ActiveRouteSourceOptions, RouterSource } from \"./types.js\";\nimport type { Params, Router } from \"@real-router/core\";\n\nconst activeSourceCache = new WeakMap<\n Router,\n Map<string, RouterSource<boolean>>\n>();\n\n/**\n * Creates a source tracking whether a route (with given params/options) is active.\n *\n * **Per-router + canonical-args cache:** repeated calls with equivalent\n * arguments return the same shared instance. Param key order doesn't matter\n * (`{ a:1, b:2 }` and `{ b:2, a:1 }` hit the same cache entry via\n * `canonicalJson`).\n *\n * `destroy()` is a no-op — shared sources live with the router. The router\n * subscription stays active while any consumer subscribes; when the router\n * is garbage-collected, the WeakMap entry releases automatically.\n *\n * Edge cases: `Symbol`/`BigInt` in params bypass `canonicalJson` and produce\n * an unstable cache key — these will simply miss the cache and create a new\n * source on each call. Practical params are primitives, so this is not a\n * concern in real usage.\n */\nexport function createActiveRouteSource(\n router: Router,\n routeName: string,\n params?: Params,\n options?: ActiveRouteSourceOptions,\n): RouterSource<boolean> {\n const { strict, ignoreQueryParams, hash } = normalizeActiveOptions(options);\n\n // BigInt/Symbol/circular refs cannot be serialized — fall back to creating\n // a fresh (non-cached) source. Callers pass these edge-case params rarely;\n // the extra allocation is acceptable.\n let key: string | undefined;\n\n try {\n // `hash === undefined` produces \"\" via String(undefined) → \"undefined\";\n // we encode it as the empty string sentinel to keep the key short and\n // distinct from the literal \"undefined\" hash value (which is a valid,\n // if unusual, fragment).\n const hashKey = hash === undefined ? \"\" : `#${hash}`;\n\n key = `${routeName}|${canonicalJson(params)}|${String(strict)}|${String(ignoreQueryParams)}|${hashKey}`;\n } catch {\n key = undefined;\n }\n\n if (key === undefined) {\n const source = buildActiveRouteSource(\n router,\n routeName,\n params,\n strict,\n ignoreQueryParams,\n hash,\n );\n\n return {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n }\n\n let perRouter = activeSourceCache.get(router);\n\n if (!perRouter) {\n perRouter = new Map();\n activeSourceCache.set(router, perRouter);\n }\n\n let cached = perRouter.get(key);\n\n if (!cached) {\n const source = buildActiveRouteSource(\n router,\n routeName,\n params,\n strict,\n ignoreQueryParams,\n hash,\n );\n\n cached = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n perRouter.set(key, cached);\n }\n\n return cached;\n}\n\n/**\n * Reads the URL fragment published by browser/navigation plugins on the given\n * router state. Returns `\"\"` when no plugin claims the `\"url\"` namespace\n * (hash-plugin runtime, memory-plugin, SSR) — `undefined` is reserved for\n * \"no published fragment yet\" and not visible at the source layer.\n */\nfunction readContextHash(router: Router): string {\n const ctx = router.getState()?.context as\n | { url?: { hash?: string } }\n | undefined;\n\n return ctx?.url?.hash ?? \"\";\n}\n\n/**\n * Combines route-name match with optional hash match (#532).\n *\n * - Route-name match: `router.isActiveRoute(name, params, strict, ignoreQueryParams)`.\n * - Hash match (only when `hash !== undefined`): `state.context.url.hash` must\n * equal the requested fragment exactly. With hash-plugin (no `url`\n * namespace), this returns `false` — the documented limitation.\n */\nfunction computeActive(\n router: Router,\n routeName: string,\n params: Params | undefined,\n strict: boolean,\n ignoreQueryParams: boolean,\n hash: string | undefined,\n): boolean {\n const routeActive = router.isActiveRoute(\n routeName,\n params,\n strict,\n ignoreQueryParams,\n );\n\n if (!routeActive) {\n return false;\n }\n if (hash === undefined) {\n return true;\n }\n\n return readContextHash(router) === hash;\n}\n\nfunction buildActiveRouteSource(\n router: Router,\n routeName: string,\n params: Params | undefined,\n strict: boolean,\n ignoreQueryParams: boolean,\n hash: string | undefined,\n): RouterSource<boolean> {\n const initialValue = computeActive(\n router,\n routeName,\n params,\n strict,\n ignoreQueryParams,\n hash,\n );\n\n const source = new BaseSource(initialValue);\n\n // Eager connection: subscribe to router immediately. This source is only\n // ever reached through the cached public `createActiveRouteSource`, whose\n // returned wrapper has a no-op destroy. The source lives with the router;\n // the router.subscribe handle is released on router GC.\n router.subscribe((next) => {\n const isNewRelated = areRoutesRelated(routeName, next.route.name);\n const isPrevRelated =\n next.previousRoute &&\n areRoutesRelated(routeName, next.previousRoute.name);\n\n // Hash-aware sources also flip on same-path-different-hash transitions.\n // The route comparison alone misses these (route is identical), but the\n // hash claim updated, so we must re-evaluate. Detect via the `hashChanged`\n // flag published by URL plugins.\n const hashFlip =\n hash !== undefined &&\n ((next.route.context as { url?: { hashChanged?: boolean } } | undefined)\n ?.url?.hashChanged ??\n false);\n\n if (!isNewRelated && !isPrevRelated && !hashFlip) {\n return;\n }\n\n // If new route is not related, we know the route is inactive —\n // avoid calling isActiveRoute for the optimization. (Hash check would\n // also fail without route-match, so this short-circuit holds for\n // hash-aware sources too.)\n const newValue = isNewRelated\n ? computeActive(\n router,\n routeName,\n params,\n strict,\n ignoreQueryParams,\n hash,\n )\n : false;\n\n if (!Object.is(source.getSnapshot(), newValue)) {\n source.updateSnapshot(newValue);\n }\n });\n\n return source;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { events } from \"@real-router/core\";\nimport { getPluginApi } from \"@real-router/core/api\";\n\nimport { BaseSource } from \"./BaseSource\";\nimport { stabilizeState } from \"./stabilizeState.js\";\n\nimport type { RouterTransitionSnapshot, RouterSource } from \"./types.js\";\nimport type { Router, State } from \"@real-router/core\";\n\nconst IDLE_SNAPSHOT: RouterTransitionSnapshot = {\n isTransitioning: false,\n isLeaveApproved: false,\n toRoute: null,\n fromRoute: null,\n};\n\nconst transitionSourceCache = new WeakMap<\n Router,\n RouterSource<RouterTransitionSnapshot>\n>();\n\nexport function createTransitionSource(\n router: Router,\n): RouterSource<RouterTransitionSnapshot> {\n const source = new BaseSource(IDLE_SNAPSHOT, {\n onDestroy: () => {\n unsubs.forEach((unsub) => {\n unsub();\n });\n },\n });\n\n const api = getPluginApi(router);\n\n const resetToIdle = (): void => {\n source.updateSnapshot(IDLE_SNAPSHOT);\n };\n\n // Eager connection: subscribe to router events immediately\n const unsubs = [\n api.addEventListener(\n events.TRANSITION_START,\n (toState: State, fromState?: State) => {\n const prev = source.getSnapshot();\n const newToRoute = stabilizeState(prev.toRoute, toState);\n const newFromRoute = stabilizeState(prev.fromRoute, fromState ?? null);\n\n if (\n !prev.isTransitioning ||\n newToRoute !== prev.toRoute ||\n newFromRoute !== prev.fromRoute\n ) {\n source.updateSnapshot({\n isTransitioning: true,\n isLeaveApproved: false,\n toRoute: newToRoute,\n fromRoute: newFromRoute,\n });\n }\n },\n ),\n api.addEventListener(\n events.TRANSITION_LEAVE_APPROVE,\n (toState: State, fromState?: State) => {\n const prev = source.getSnapshot();\n\n source.updateSnapshot({\n isTransitioning: true,\n isLeaveApproved: true,\n toRoute: stabilizeState(prev.toRoute, toState),\n fromRoute: stabilizeState(prev.fromRoute, fromState ?? null),\n });\n },\n ),\n api.addEventListener(events.TRANSITION_SUCCESS, resetToIdle),\n api.addEventListener(events.TRANSITION_ERROR, resetToIdle),\n api.addEventListener(events.TRANSITION_CANCEL, resetToIdle),\n ];\n\n return source;\n}\n\n/**\n * Returns a per-router cached transition source shared across all consumers.\n *\n * Safe to call destroy() — the cached source ignores external destroy() calls\n * and lives until the router itself is garbage-collected (the WeakMap entry\n * releases automatically).\n *\n * Use this in framework adapters (React/Preact/Solid/Vue/Svelte/Angular) to\n * share a single TransitionSource instance across all mount/unmount cycles.\n *\n * For isolated/advanced use (ad-hoc, short-lived, per-owner teardown), call\n * `createTransitionSource(router)` directly — it returns a fresh instance with\n * a working `destroy()`.\n */\nexport function getTransitionSource(\n router: Router,\n): RouterSource<RouterTransitionSnapshot> {\n let cached = transitionSourceCache.get(router);\n\n if (!cached) {\n const source = createTransitionSource(router);\n\n // Wrap with no-op destroy. The underlying source is shared across all\n // consumers; letting any one consumer call destroy() would tear it down\n // for the rest. The source lives as long as the router (WeakMap key).\n cached = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n transitionSourceCache.set(router, cached);\n }\n\n return cached;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { events } from \"@real-router/core\";\nimport { getPluginApi } from \"@real-router/core/api\";\n\nimport { BaseSource } from \"./BaseSource\";\n\nimport type { RouterErrorSnapshot, RouterSource } from \"./types.js\";\nimport type { Router, State, RouterError } from \"@real-router/core\";\n\nconst INITIAL_SNAPSHOT: RouterErrorSnapshot = {\n error: null,\n toRoute: null,\n fromRoute: null,\n version: 0,\n};\n\nconst errorSourceCache = new WeakMap<\n Router,\n RouterSource<RouterErrorSnapshot>\n>();\n\nexport function createErrorSource(\n router: Router,\n): RouterSource<RouterErrorSnapshot> {\n let errorVersion = 0;\n\n const source = new BaseSource(INITIAL_SNAPSHOT, {\n onDestroy: () => {\n unsubs.forEach((unsub) => {\n unsub();\n });\n },\n });\n\n const api = getPluginApi(router);\n\n // Eager connection: subscribe to router events immediately\n const unsubs = [\n api.addEventListener(\n events.TRANSITION_ERROR,\n (\n toState: State | undefined,\n fromState: State | undefined,\n err: RouterError,\n ) => {\n errorVersion++;\n source.updateSnapshot({\n error: err,\n toRoute: toState ?? null,\n /* v8 ignore next -- @preserve: fromState undefined only during start() error; unreachable via navigate() */\n fromRoute: fromState ?? null,\n version: errorVersion,\n });\n },\n ),\n api.addEventListener(events.TRANSITION_SUCCESS, () => {\n // Skip if no error — avoids unnecessary re-renders.\n // BaseSource.updateSnapshot() always notifies listeners (new object = new ref),\n // and useSyncExternalStore compares via Object.is().\n if (source.getSnapshot().error !== null) {\n source.updateSnapshot({\n error: null,\n toRoute: null,\n fromRoute: null,\n version: errorVersion,\n });\n }\n }),\n ];\n\n return source;\n}\n\n/**\n * Returns a per-router cached error source shared across all consumers.\n *\n * Safe to call destroy() — the cached source ignores external destroy() calls\n * and lives until the router itself is garbage-collected (the WeakMap entry\n * releases automatically).\n *\n * Use this in framework adapters (React/Preact/Solid/Vue/Svelte/Angular) to\n * share a single ErrorSource instance across all mount/unmount cycles.\n *\n * For isolated/advanced use (ad-hoc, short-lived, per-owner teardown), call\n * `createErrorSource(router)` directly — it returns a fresh instance with a\n * working `destroy()`.\n */\nexport function getErrorSource(\n router: Router,\n): RouterSource<RouterErrorSnapshot> {\n let cached = errorSourceCache.get(router);\n\n if (!cached) {\n const source = createErrorSource(router);\n\n // Wrap with no-op destroy. The underlying source is shared across all\n // consumers; letting any one consumer call destroy() would tear it down\n // for the rest. The source lives as long as the router (WeakMap key).\n cached = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n errorSourceCache.set(router, cached);\n }\n\n return cached;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { getErrorSource } from \"./createErrorSource\";\n\nimport type { DismissableErrorSnapshot, RouterSource } from \"./types.js\";\nimport type { Router } from \"@real-router/core\";\n\nconst dismissableCache = new WeakMap<\n Router,\n RouterSource<DismissableErrorSnapshot>\n>();\n\n/**\n * Returns a per-router cached source that wraps `getErrorSource(router)` with\n * an integrated \"dismissed\" version counter, exposing a single reactive\n * snapshot `{ error, toRoute, fromRoute, version, resetError }`.\n *\n * Each `RouterErrorBoundary` in a framework adapter subscribes to this one\n * source instead of re-implementing the `dismissedVersion` state pattern\n * locally. The 6-copy duplicate across adapters collapses to this helper.\n *\n * **Semantics:**\n * - `error` is non-null only when `underlying.version > dismissedVersion`.\n * - `resetError()` sets `dismissedVersion = current underlying version`,\n * immediately clearing `error` to `null` and notifying all listeners.\n * - A subsequent `TRANSITION_ERROR` advances `version` beyond `dismissedVersion`,\n * so `error` becomes non-null again — no additional plumbing needed.\n *\n * **Cached:** one instance per router. `destroy()` on the returned source is\n * a no-op. Shared across all `RouterErrorBoundary` consumers.\n */\nexport function createDismissableError(\n router: Router,\n): RouterSource<DismissableErrorSnapshot> {\n const cached = dismissableCache.get(router);\n\n if (cached) {\n return cached;\n }\n\n const errorSource = getErrorSource(router);\n\n let dismissedVersion = -1;\n\n const computeSnapshot = (): DismissableErrorSnapshot => {\n const snap = errorSource.getSnapshot();\n const isDismissed = snap.version <= dismissedVersion;\n\n return {\n error: isDismissed ? null : snap.error,\n toRoute: isDismissed ? null : snap.toRoute,\n fromRoute: isDismissed ? null : snap.fromRoute,\n version: snap.version,\n resetError,\n };\n };\n\n const source = new BaseSource<DismissableErrorSnapshot>(computeSnapshot(), {\n onFirstSubscribe: () => {\n unsubFromError = errorSource.subscribe(() => {\n source.updateSnapshot(computeSnapshot());\n });\n },\n onLastUnsubscribe: () => {\n disconnect();\n },\n });\n\n let unsubFromError: (() => void) | null = null;\n\n function resetError(): void {\n dismissedVersion = errorSource.getSnapshot().version;\n source.updateSnapshot(computeSnapshot());\n }\n\n function disconnect(): void {\n const unsub = unsubFromError;\n\n unsubFromError = null;\n unsub?.();\n }\n\n const wrapper: RouterSource<DismissableErrorSnapshot> = {\n subscribe: source.subscribe,\n getSnapshot: source.getSnapshot,\n destroy: noopDestroy,\n };\n\n dismissableCache.set(router, wrapper);\n\n return wrapper;\n}\n\nfunction noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { areRoutesRelated } from \"@real-router/route-utils\";\n\nimport type { Router } from \"@real-router/core\";\n\nexport interface ActiveNameSelector {\n /**\n * Subscribes to active-state changes of a specific route name.\n * Listener is called only when `isActive(routeName)` for this name transitions.\n * Returns an unsubscribe function.\n */\n subscribe: (routeName: string, listener: () => void) => () => void;\n /**\n * O(1) active check for the given route name (non-strict by default —\n * matches descendants). Uses the shared underlying router subscription.\n */\n isActive: (routeName: string) => boolean;\n /** No-op on the cached wrapper. */\n destroy: () => void;\n}\n\nconst selectorCache = new WeakMap<Router, ActiveNameSelector>();\n\n/**\n * Per-router cached selector providing O(1) active-route checks with one\n * shared router subscription for any number of distinct `routeName`\n * consumers.\n *\n * **When to use:** framework `Link` components that need an active boolean\n * without custom `params` / `activeStrict` / `ignoreQueryParams` — e.g. the\n * common navigation-link case. Multiple `<Link>` components with different\n * `routeName` share ONE `router.subscribe` handle instead of creating one\n * per Link (which is what `createActiveRouteSource(router, name)` does — it\n * caches per-name, so N names = N subscriptions).\n *\n * **When NOT to use:** Link needs `activeStrict: true`, custom `routeParams`,\n * or `ignoreQueryParams: false`. Fall back to `createActiveRouteSource` —\n * its cache handles the full argument surface.\n *\n * Based on the `routeSelector` pattern pioneered by `@real-router/solid`'s\n * `RouterProvider` (`createSelector` + `areRoutesRelated`). This helper\n * ports it to framework-agnostic API so Vue / React / Preact / Svelte /\n * Angular Link components can adopt the same fast path.\n *\n * @see Solid reference implementation — `packages/solid/src/RouterProvider.tsx`\n */\nexport function createActiveNameSelector(router: Router): ActiveNameSelector {\n const cached = selectorCache.get(router);\n\n if (cached) {\n return cached;\n }\n\n // listeners per-name — re-evaluated on every router transition\n const listenersByName = new Map<string, Set<() => void>>();\n // cached active state per-name — used to diff before notifying\n const activeByName = new Map<string, boolean>();\n\n let routerUnsubscribe: (() => void) | null = null;\n\n const isActiveNonStrict = (routeName: string): boolean => {\n const current = router.getState();\n\n if (!current) {\n return false;\n }\n\n return (\n current.name === routeName || current.name.startsWith(`${routeName}.`)\n );\n };\n\n const connect = (): void => {\n routerUnsubscribe = router.subscribe((next) => {\n for (const [routeName, listeners] of listenersByName) {\n // Cheap pre-filter: if neither new nor previous route is related\n // to this name, its active state cannot have changed.\n const isNewRelated = areRoutesRelated(routeName, next.route.name);\n const isPrevRelated =\n next.previousRoute &&\n areRoutesRelated(routeName, next.previousRoute.name);\n\n if (!isNewRelated && !isPrevRelated) {\n continue;\n }\n\n // activeByName always has an entry for names present in listenersByName —\n // subscribe() seeds it, and we only iterate over listenersByName.\n const prevActive = activeByName.get(routeName) === true;\n const nextActive = isActiveNonStrict(routeName);\n\n if (prevActive === nextActive) {\n continue;\n }\n\n activeByName.set(routeName, nextActive);\n for (const listener of listeners) {\n listener();\n }\n }\n });\n };\n\n const disconnect = (): void => {\n const unsub = routerUnsubscribe;\n\n routerUnsubscribe = null;\n unsub?.();\n };\n\n const subscribe = (routeName: string, listener: () => void): (() => void) => {\n let listeners = listenersByName.get(routeName);\n\n if (!listeners) {\n listeners = new Set();\n listenersByName.set(routeName, listeners);\n activeByName.set(routeName, isActiveNonStrict(routeName));\n }\n\n listeners.add(listener);\n\n if (!routerUnsubscribe) {\n connect();\n }\n\n let unsubscribed = false;\n\n return () => {\n if (unsubscribed) {\n return;\n }\n\n unsubscribed = true;\n listeners.delete(listener);\n\n if (listeners.size === 0) {\n listenersByName.delete(routeName);\n activeByName.delete(routeName);\n }\n\n if (listenersByName.size === 0) {\n disconnect();\n }\n };\n };\n\n const isActive = (routeName: string): boolean => {\n const cachedActive = activeByName.get(routeName);\n\n if (cachedActive !== undefined) {\n return cachedActive;\n }\n\n // Not subscribed — compute on demand.\n return isActiveNonStrict(routeName);\n };\n\n const selector: ActiveNameSelector = {\n subscribe,\n isActive,\n destroy: noopDestroy,\n };\n\n selectorCache.set(router, selector);\n\n return selector;\n}\n\nfunction noopDestroy(): void {\n // Shared cached selector — external destroy() is a no-op.\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@real-router/sources",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"description": "Framework-agnostic subscription layer for Real-Router state",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"homepage": "https://github.com/greydragon888/real-router",
|
|
44
44
|
"sideEffects": false,
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@real-router/core": "^0.
|
|
47
|
-
"@real-router/route-utils": "^0.2.
|
|
46
|
+
"@real-router/core": "^0.51.0",
|
|
47
|
+
"@real-router/route-utils": "^0.2.2"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"mitata": "1.0.34"
|
|
@@ -35,7 +35,7 @@ export function createActiveRouteSource(
|
|
|
35
35
|
params?: Params,
|
|
36
36
|
options?: ActiveRouteSourceOptions,
|
|
37
37
|
): RouterSource<boolean> {
|
|
38
|
-
const { strict, ignoreQueryParams } = normalizeActiveOptions(options);
|
|
38
|
+
const { strict, ignoreQueryParams, hash } = normalizeActiveOptions(options);
|
|
39
39
|
|
|
40
40
|
// BigInt/Symbol/circular refs cannot be serialized — fall back to creating
|
|
41
41
|
// a fresh (non-cached) source. Callers pass these edge-case params rarely;
|
|
@@ -43,7 +43,13 @@ export function createActiveRouteSource(
|
|
|
43
43
|
let key: string | undefined;
|
|
44
44
|
|
|
45
45
|
try {
|
|
46
|
-
|
|
46
|
+
// `hash === undefined` produces "" via String(undefined) → "undefined";
|
|
47
|
+
// we encode it as the empty string sentinel to keep the key short and
|
|
48
|
+
// distinct from the literal "undefined" hash value (which is a valid,
|
|
49
|
+
// if unusual, fragment).
|
|
50
|
+
const hashKey = hash === undefined ? "" : `#${hash}`;
|
|
51
|
+
|
|
52
|
+
key = `${routeName}|${canonicalJson(params)}|${String(strict)}|${String(ignoreQueryParams)}|${hashKey}`;
|
|
47
53
|
} catch {
|
|
48
54
|
key = undefined;
|
|
49
55
|
}
|
|
@@ -55,6 +61,7 @@ export function createActiveRouteSource(
|
|
|
55
61
|
params,
|
|
56
62
|
strict,
|
|
57
63
|
ignoreQueryParams,
|
|
64
|
+
hash,
|
|
58
65
|
);
|
|
59
66
|
|
|
60
67
|
return {
|
|
@@ -80,6 +87,7 @@ export function createActiveRouteSource(
|
|
|
80
87
|
params,
|
|
81
88
|
strict,
|
|
82
89
|
ignoreQueryParams,
|
|
90
|
+
hash,
|
|
83
91
|
);
|
|
84
92
|
|
|
85
93
|
cached = {
|
|
@@ -93,18 +101,68 @@ export function createActiveRouteSource(
|
|
|
93
101
|
return cached;
|
|
94
102
|
}
|
|
95
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Reads the URL fragment published by browser/navigation plugins on the given
|
|
106
|
+
* router state. Returns `""` when no plugin claims the `"url"` namespace
|
|
107
|
+
* (hash-plugin runtime, memory-plugin, SSR) — `undefined` is reserved for
|
|
108
|
+
* "no published fragment yet" and not visible at the source layer.
|
|
109
|
+
*/
|
|
110
|
+
function readContextHash(router: Router): string {
|
|
111
|
+
const ctx = router.getState()?.context as
|
|
112
|
+
| { url?: { hash?: string } }
|
|
113
|
+
| undefined;
|
|
114
|
+
|
|
115
|
+
return ctx?.url?.hash ?? "";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Combines route-name match with optional hash match (#532).
|
|
120
|
+
*
|
|
121
|
+
* - Route-name match: `router.isActiveRoute(name, params, strict, ignoreQueryParams)`.
|
|
122
|
+
* - Hash match (only when `hash !== undefined`): `state.context.url.hash` must
|
|
123
|
+
* equal the requested fragment exactly. With hash-plugin (no `url`
|
|
124
|
+
* namespace), this returns `false` — the documented limitation.
|
|
125
|
+
*/
|
|
126
|
+
function computeActive(
|
|
127
|
+
router: Router,
|
|
128
|
+
routeName: string,
|
|
129
|
+
params: Params | undefined,
|
|
130
|
+
strict: boolean,
|
|
131
|
+
ignoreQueryParams: boolean,
|
|
132
|
+
hash: string | undefined,
|
|
133
|
+
): boolean {
|
|
134
|
+
const routeActive = router.isActiveRoute(
|
|
135
|
+
routeName,
|
|
136
|
+
params,
|
|
137
|
+
strict,
|
|
138
|
+
ignoreQueryParams,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (!routeActive) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
if (hash === undefined) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return readContextHash(router) === hash;
|
|
149
|
+
}
|
|
150
|
+
|
|
96
151
|
function buildActiveRouteSource(
|
|
97
152
|
router: Router,
|
|
98
153
|
routeName: string,
|
|
99
154
|
params: Params | undefined,
|
|
100
155
|
strict: boolean,
|
|
101
156
|
ignoreQueryParams: boolean,
|
|
157
|
+
hash: string | undefined,
|
|
102
158
|
): RouterSource<boolean> {
|
|
103
|
-
const initialValue =
|
|
159
|
+
const initialValue = computeActive(
|
|
160
|
+
router,
|
|
104
161
|
routeName,
|
|
105
162
|
params,
|
|
106
163
|
strict,
|
|
107
164
|
ignoreQueryParams,
|
|
165
|
+
hash,
|
|
108
166
|
);
|
|
109
167
|
|
|
110
168
|
const source = new BaseSource(initialValue);
|
|
@@ -119,14 +177,33 @@ function buildActiveRouteSource(
|
|
|
119
177
|
next.previousRoute &&
|
|
120
178
|
areRoutesRelated(routeName, next.previousRoute.name);
|
|
121
179
|
|
|
122
|
-
|
|
180
|
+
// Hash-aware sources also flip on same-path-different-hash transitions.
|
|
181
|
+
// The route comparison alone misses these (route is identical), but the
|
|
182
|
+
// hash claim updated, so we must re-evaluate. Detect via the `hashChanged`
|
|
183
|
+
// flag published by URL plugins.
|
|
184
|
+
const hashFlip =
|
|
185
|
+
hash !== undefined &&
|
|
186
|
+
((next.route.context as { url?: { hashChanged?: boolean } } | undefined)
|
|
187
|
+
?.url?.hashChanged ??
|
|
188
|
+
false);
|
|
189
|
+
|
|
190
|
+
if (!isNewRelated && !isPrevRelated && !hashFlip) {
|
|
123
191
|
return;
|
|
124
192
|
}
|
|
125
193
|
|
|
126
194
|
// If new route is not related, we know the route is inactive —
|
|
127
|
-
// avoid calling isActiveRoute for the optimization
|
|
195
|
+
// avoid calling isActiveRoute for the optimization. (Hash check would
|
|
196
|
+
// also fail without route-match, so this short-circuit holds for
|
|
197
|
+
// hash-aware sources too.)
|
|
128
198
|
const newValue = isNewRelated
|
|
129
|
-
?
|
|
199
|
+
? computeActive(
|
|
200
|
+
router,
|
|
201
|
+
routeName,
|
|
202
|
+
params,
|
|
203
|
+
strict,
|
|
204
|
+
ignoreQueryParams,
|
|
205
|
+
hash,
|
|
206
|
+
)
|
|
130
207
|
: false;
|
|
131
208
|
|
|
132
209
|
if (!Object.is(source.getSnapshot(), newValue)) {
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import type { ActiveRouteSourceOptions } from "./types.js";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Normalized options shape — booleans are required (filled with defaults),
|
|
5
|
+
* `hash` stays `string | undefined` because `undefined` is the meaningful
|
|
6
|
+
* "ignore hash" sentinel that callers pass intentionally.
|
|
7
|
+
*/
|
|
8
|
+
export interface NormalizedActiveOptions {
|
|
9
|
+
strict: boolean;
|
|
10
|
+
ignoreQueryParams: boolean;
|
|
11
|
+
hash: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
3
14
|
/**
|
|
4
15
|
* Default options for `createActiveRouteSource` and adapter-level helpers.
|
|
5
16
|
*
|
|
6
17
|
* Frozen to prevent accidental mutation by consumers.
|
|
7
18
|
*/
|
|
8
|
-
export const DEFAULT_ACTIVE_OPTIONS: Readonly<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
});
|
|
19
|
+
export const DEFAULT_ACTIVE_OPTIONS: Readonly<NormalizedActiveOptions> =
|
|
20
|
+
Object.freeze({
|
|
21
|
+
strict: false,
|
|
22
|
+
ignoreQueryParams: true,
|
|
23
|
+
hash: undefined,
|
|
24
|
+
});
|
|
14
25
|
|
|
15
26
|
/**
|
|
16
27
|
* Normalizes partial `ActiveRouteSourceOptions` into a fully-defaulted object.
|
|
@@ -20,10 +31,11 @@ export const DEFAULT_ACTIVE_OPTIONS: Readonly<
|
|
|
20
31
|
*/
|
|
21
32
|
export function normalizeActiveOptions(
|
|
22
33
|
options?: ActiveRouteSourceOptions,
|
|
23
|
-
):
|
|
34
|
+
): NormalizedActiveOptions {
|
|
24
35
|
return {
|
|
25
36
|
strict: options?.strict ?? DEFAULT_ACTIVE_OPTIONS.strict,
|
|
26
37
|
ignoreQueryParams:
|
|
27
38
|
options?.ignoreQueryParams ?? DEFAULT_ACTIVE_OPTIONS.ignoreQueryParams,
|
|
39
|
+
hash: options?.hash,
|
|
28
40
|
};
|
|
29
41
|
}
|
package/src/stabilizeState.ts
CHANGED
|
@@ -3,13 +3,18 @@ import type { State } from "@real-router/core";
|
|
|
3
3
|
/**
|
|
4
4
|
* State-aware stabilization for route snapshots.
|
|
5
5
|
*
|
|
6
|
-
* Compares `path`
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Compares `path` (canonical name+params) AND `state.context.url.hash`
|
|
7
|
+
* (URL fragment, #532). When both match, returns `prev` (preserving
|
|
8
|
+
* reference) so frameworks can skip re-renders. When hash flips on a
|
|
9
|
+
* same-path navigation (tab-style UI), returns `next` so consumers
|
|
10
|
+
* subscribing through `useRoute()` see the new state.
|
|
9
11
|
*
|
|
10
|
-
* Ignores `meta` (internal: auto-increment id)
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* Ignores `meta` (internal: auto-increment id), `transition` (reference
|
|
13
|
+
* data: from, segments, reload), and `state.context.navigation` /
|
|
14
|
+
* `state.context.browser` (transient transition metadata) — they don't
|
|
15
|
+
* affect what is rendered. `state.context.url.hash` is the only context
|
|
16
|
+
* field that participates in render identity, because tab-style UIs
|
|
17
|
+
* subscribe to it directly.
|
|
13
18
|
*
|
|
14
19
|
* Accepts `null` for compatibility with `RouterTransitionSnapshot`
|
|
15
20
|
* (toRoute/fromRoute are `State | null`).
|
|
@@ -27,5 +32,20 @@ export function stabilizeState<T extends State | null | undefined>(
|
|
|
27
32
|
return next;
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
// After the path check, both must be the same non-null State (paths
|
|
36
|
+
// matched, prev !== next reference). Read context.url.hash to detect
|
|
37
|
+
// same-path-different-hash navigation (#532) — render-relevant for
|
|
38
|
+
// tab-style UIs that subscribe via useRoute(). Optional chaining keeps
|
|
39
|
+
// the access null-safe without forbidden non-null assertions.
|
|
40
|
+
if (readContextHash(prev) !== readContextHash(next)) {
|
|
41
|
+
return next;
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
return prev;
|
|
31
45
|
}
|
|
46
|
+
|
|
47
|
+
function readContextHash(state: State | null | undefined): string | undefined {
|
|
48
|
+
const ctx = state?.context as { url?: { hash?: string } } | undefined;
|
|
49
|
+
|
|
50
|
+
return ctx?.url?.hash;
|
|
51
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -19,6 +19,20 @@ export interface RouterSource<T> {
|
|
|
19
19
|
export interface ActiveRouteSourceOptions {
|
|
20
20
|
strict?: boolean;
|
|
21
21
|
ignoreQueryParams?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* URL fragment match (#532). When defined, the source is active iff the
|
|
24
|
+
* route matches AND `state.context.url.hash` (decoded, populated by
|
|
25
|
+
* browser/navigation URL plugins) equals this value:
|
|
26
|
+
*
|
|
27
|
+
* - `undefined` (default): hash is ignored — legacy route-only matching.
|
|
28
|
+
* - `""`: active only when the current URL has no fragment (or empty).
|
|
29
|
+
* - `"value"`: active only when the current fragment equals `"value"`.
|
|
30
|
+
*
|
|
31
|
+
* Hash-plugin runtimes leave `state.context.url` undefined, so any non-
|
|
32
|
+
* undefined `hash` option will produce `false` there — consistent with the
|
|
33
|
+
* documented limitation that hash-plugin doesn't support URL fragments.
|
|
34
|
+
*/
|
|
35
|
+
hash?: string;
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
export interface RouterTransitionSnapshot {
|