@real-router/sources 0.14.29 → 0.14.31

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#listeners","#onFirstSubscribe","#onLastUnsubscribe","#onDestroy","#currentSnapshot","#destroyed","events","events"],"sources":["../../src/BaseSource.ts","../../src/internal/readContextHash.ts","../../src/stabilizeState.ts","../../src/createRouteSource.ts","../../src/computeSnapshot.ts","../../src/internal/noopDestroy.ts","../../src/createRouteNodeSource.ts","../../src/canonicalJson.ts","../../src/normalizeActiveOptions.ts","../../src/createActiveRouteSource.ts","../../src/createActiveNameSelector.ts","../../src/createActiveSource.ts","../../src/createTransitionSource.ts","../../src/createErrorSource.ts","../../src/createDismissableError.ts","../../src/createRouteEnterGate.ts","../../src/guardLeaveListener.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 // Isolate listener exceptions so a single throwing subscriber (e.g. React\n // error-boundary fallback throwing inside `onStoreChange`) does not block\n // the remaining subscribers — the invariant \"after updateSnapshot all\n // listeners see the new snapshot\" must hold. Re-throw asynchronously via\n // queueMicrotask so global error handlers / test harnesses still surface\n // the bug without breaking the synchronous notification fan-out.\n for (const listener of this.#listeners) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\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 * @internal\n *\n * Reads the URL fragment published by `browser-plugin` / `navigation-plugin`\n * on a router state. The plugins claim the `\"url\"` namespace via\n * `state.context.url` and the `hash` field carries the **decoded** fragment.\n *\n * Returns:\n * - `undefined` — no plugin claimed the `\"url\"` namespace (hash-plugin runtime,\n * memory-plugin, SSR before hydration) OR the state itself is nullish;\n * - `\"\"` — the URL namespace exists but the fragment is empty\n * (browser-plugin on a hash-less URL).\n *\n * Callers that need the \"no namespace at all\" branch (e.g. `stabilizeState`\n * comparing cross-plugin transitions) read the raw `undefined`. Callers that\n * collapse \"no namespace\" to \"no hash\" (e.g. `createActiveRouteSource`'s\n * hash-equality check) coalesce with `?? \"\"` themselves.\n *\n * Centralising the context cast removes the previous duplicate definitions in\n * `stabilizeState.ts` and `createActiveRouteSource.ts` that drifted in\n * signature (state vs router) and default-value (undefined vs \"\") — both\n * variants are reconstructible from this single helper at the callsite.\n */\nexport function readContextHash(\n state: State | null | undefined,\n): string | undefined {\n const ctx = state?.context as { url?: { hash?: string } } | undefined;\n\n return ctx?.url?.hash;\n}\n","import { readContextHash } from \"./internal/readContextHash.js\";\n\nimport type { State } from \"@real-router/core\";\n\n/**\n * State-aware stabilization for route snapshots.\n *\n * Compares `path` (canonical name+params), `state.context.url.hash`\n * (URL fragment, #532), and `state.transition.reload` (#605). When all\n * three match (idempotent navigation), returns `prev` (preserving\n * reference) so frameworks can skip re-renders. When any of them flips,\n * returns `next` so consumers subscribing through `useRoute()` see the\n * new state.\n *\n * `transition.reload === true` is the user's explicit signal for a\n * non-idempotent navigation — `router.navigate(name, params, search, {\n * reload: true })` is the canonical pairing for `invalidate(router, namespace)`\n * and any cache-bust pattern. Bypassing stabilization for reloads makes\n * `useRoute()` consumers see fresh `state.context.<namespace>` values\n * written by the SSR loader plugin's `subscribeLeave` handler.\n *\n * Ignores `meta` (internal: auto-increment id), other `transition` fields\n * (`from`, `segments`, `redirected`), and `state.context.navigation` /\n * `state.context.browser` (transient transition metadata) — they don't\n * affect render identity for idempotent navigations.\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 // Explicit reload navigation (#605) — caller asked to bypass dedupe so\n // observers see fresh `state.context` written by `invalidate()`-driven\n // loader re-runs. The path equality above guarantees both prev and next\n // are either non-null with matching paths or both nullish; only the\n // non-null branch can carry a meaningful `transition.reload`.\n if (readReloadFlag(next)) {\n return next;\n }\n\n return prev;\n}\n\nfunction readReloadFlag(state: State | null | undefined): boolean {\n // Defensive read: `transition` is mandatory in the public State type, but a\n // plugin returning a malformed state (or a future fork) shouldn't crash the\n // stabilizer with a TypeError. We cast to a structurally-loose shape so the\n // optional chain is permitted; the runtime guard preserves dedup (false =\n // not-a-reload) for malformed inputs.\n const transition = (\n state as { transition?: { reload?: boolean } } | null | undefined\n )?.transition;\n\n return transition?.reload === true;\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 // Reconcile the route with the current router state before connecting.\n // While this source had zero listeners it was disconnected\n // (onLastUnsubscribe below), so a navigation in that window was missed —\n // the just-added listener would otherwise observe a stale route (#765).\n // BaseSource registered the listener before this callback, so the\n // updateSnapshot below reaches it.\n //\n // Emit ONLY when the route actually changed: a hide/show (Activity)\n // cycle with no navigation in between must not fire a spurious\n // re-render, and its still-valid `previousRoute` must survive. When a\n // navigation WAS caught up, `previousRoute` resets to `undefined`: the\n // catch-up is a snap to the current state, not an observed navigation,\n // and the true previous route cannot be reconstructed outside a live\n // subscribe payload (parity of intent with createRouteNodeSource, whose\n // reconnect reconcile likewise drops previousRoute on catch-up).\n const current = source.getSnapshot();\n const reconciledRoute = stabilizeState(\n current.route,\n router.getState(),\n );\n\n if (reconciledRoute !== current.route) {\n source.updateSnapshot({\n route: reconciledRoute,\n previousRoute: undefined,\n });\n }\n\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","/**\n * @internal\n *\n * Shared no-op `destroy()` for cached `RouterSource` wrappers.\n *\n * Cached factories (`getTransitionSource`, `getErrorSource`, `createDismissableError`,\n * `createActiveNameSelector`, cache hit in `createRouteNodeSource`,\n * `createActiveRouteSource`) return a wrapper whose `destroy()` is a no-op —\n * the underlying source is shared across all consumers and lives as long as\n * the router (WeakMap entry releases on router GC).\n *\n * One module-level function shared by every cached factory keeps the wrapper\n * shape (`{ subscribe, getSnapshot, destroy: noopDestroy }`) byte-stable and\n * eliminates the previous six copies. (Bundlers inline a stand-alone arrow\n * just as readily, so the cost is purely on the maintenance side.)\n */\nexport function noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { computeSnapshot } from \"./computeSnapshot.js\";\nimport { noopDestroy } from \"./internal/noopDestroy.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 // computeSnapshot returns the SAME currentSnapshot reference when\n // both route and previousRoute stabilize to prev — guard against\n // emitting redundant updates to listeners (matters for signal-\n // based adapters that re-run effects on every set).\n /* v8 ignore next 3 -- @preserve: structurally unreachable after #605\n — reload navs always return fresh refs via stabilizeState, and\n within-node non-reload navs short-circuit at shouldUpdate. Guard\n kept for defensive correctness against future stabilizer changes. */\n if (Object.is(source.getSnapshot(), newSnapshot)) {\n return;\n }\n\n source.updateSnapshot(newSnapshot);\n });\n },\n onLastUnsubscribe: disconnect,\n },\n );\n\n return source;\n}\n","/**\n * Intrinsics captured at module load (#1971).\n *\n * ⚑ These DECIDE — each answers \"what is on this object\" for a value this module\n * did not build, so read off the live global they are the weakest point of every\n * check built on them. `guards.ts` states the doctrine and its measurement: one\n * naive `Object.hasOwn` polyfill walked straight through five sibling readers\n * while the single captured guard held.\n *\n * ⚠ Capture narrows the window from \"any time after boot\" to \"before this module\n * loads\". It does not close it — a shim evaluated ahead of core still wins\n * (#1798), which is the doctrine's own caveat and travels with it.\n */\nconst objectKeys = Object.keys;\n\n/** Captured like the deciding seven, but this one BUILDS the guarantee (#2072). */\nconst objectCreate = Object.create;\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 * **Divergence from `shared/dom-utils/scroll-restore.canonicalJson` — by\n * design.** That sibling implementation is the cheap navigation-hot-path\n * variant (uses `localeCompare`, plain-object accumulator, native cycle\n * detector) and pairs with `safeKeyOf` for crash-tolerance. This one is the\n * strict cache-key variant: byte-order compare, prototype-less accumulator,\n * bespoke cycle detection — eagerly throws on inputs that would cause\n * collisions or pollution, so callers can fall back to non-cached sources.\n * They are NOT interchangeable; cross-package equivalence is explicitly not\n * a goal (audit-2 / audit-2026-05-17 §2).\n *\n * Edge cases:\n * - Arrays preserve order (canonical: index-ordered already).\n * - `undefined` values are dropped (standard JSON behaviour).\n * - `Date` is serialized via its `toJSON` method (ISO string). **Collision\n * caveat:** `{ d: new Date(0) }` and `{ d: \"1970-01-01T00:00:00.000Z\" }`\n * produce the **same** cache key (both reduce to the ISO string), so the two\n * inputs share one cached source. Harmless in practice — `Date` params are\n * exotic and `isActiveRoute` stringifies them identically post-encoding.\n * - `Symbol` becomes `undefined` (standard JSON behaviour).\n * - `BigInt` throws via `JSON.stringify` defaults.\n * - `Map`, `Set`, `RegExp`, `WeakMap`, `WeakSet` would silently collapse to\n * `\"{}\"` (no enumerable own keys), which would cause **different inputs to\n * share the same cache key**. We detect these explicitly and throw — callers\n * then take the non-cached fallback path (same behaviour as `BigInt`).\n * - Circular references throw `TypeError` (parity with native `JSON.stringify`).\n * The replacer copies each object level into a fresh prototype-less record,\n * so we must run our own cycle detection — the native detector never sees\n * the original object graph.\n * - `__proto__` keys are preserved as own properties (no prototype pollution\n * and no silent collision between `{ __proto__: x, b: 1 }` and `{ b: 1 }`).\n *\n * In practice, route params carry primitives (`string | number | boolean`);\n * the cache-key-collision edge cases above are defensive bugs caught loudly.\n */\nexport function canonicalJson(value: unknown): string {\n return JSON.stringify(canonicalize(value, new Set<object>()));\n}\n\n// Key comparison uses byte-order (`<` / `>`) instead of `localeCompare()` so\n// the output is independent of the Node.js ICU build / system locale. Same\n// canonical form on every machine — required for cache-key stability.\nfunction compareKeys(left: string, right: string): number {\n return left < right ? -1 : 1;\n}\n\n/**\n * Returns a structural clone with object keys sorted at every level. Path-based\n * cycle detection (`path` set) matches the semantics of native `JSON.stringify`\n * — a `TypeError` is thrown on a true cycle, but the same object reachable via\n * two independent branches (DAG) serialises fine.\n *\n * `Date` instances pass through unchanged so `JSON.stringify` can invoke their\n * `toJSON` hook. Other built-ins (`Map`, `Set`, `WeakMap`, `WeakSet`, `RegExp`)\n * throw eagerly to avoid silent cache-key collisions.\n */\nfunction canonicalize(value: unknown, path: Set<object>): unknown {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (value instanceof Date) {\n return value;\n }\n\n if (\n value instanceof Map ||\n value instanceof Set ||\n value instanceof WeakMap ||\n value instanceof WeakSet ||\n value instanceof RegExp\n ) {\n throw new TypeError(\n `canonicalJson: cannot serialize ${(value as { constructor: { name: string } }).constructor.name} — non-enumerable own keys collapse to \"{}\" and would cause cache-key collisions. Pass primitive params (string | number | boolean) instead.`,\n );\n }\n\n if (path.has(value)) {\n throw new TypeError(\n \"canonicalJson: cannot serialize circular structure (cycle detected during traversal).\",\n );\n }\n\n path.add(value);\n try {\n if (Array.isArray(value)) {\n return value.map((item) => canonicalize(item, path));\n }\n\n // Use a null-prototype record so `__proto__` is treated as a regular\n // own property — assigning to a plain `{}` would set the prototype\n // chain instead and silently collide with inputs that omit the key.\n const sorted: Record<string, unknown> = objectCreate(null) as Record<\n string,\n unknown\n >;\n const keys = objectKeys(value).toSorted(compareKeys);\n\n for (const key of keys) {\n sorted[key] = canonicalize((value as Record<string, unknown>)[key], path);\n }\n\n return sorted;\n } finally {\n path.delete(value);\n }\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 { noopDestroy } from \"./internal/noopDestroy.js\";\nimport { readContextHash } from \"./internal/readContextHash.js\";\nimport { normalizeActiveOptions } from \"./normalizeActiveOptions.js\";\n\nimport type { ActiveRouteSourceOptions, RouterSource } from \"./types.js\";\nimport type { Params, Router, SearchParams } 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 * For cached entries `destroy()` is a no-op — shared sources live with the\n * router and release automatically on router GC (WeakMap entry).\n *\n * A non-string route name, or `BigInt`/circular params → the source bypasses the cache\n * and `destroy()` becomes a real teardown that detaches the underlying\n * `router.subscribe` handle.\n */\nexport function createActiveRouteSource(\n router: Router,\n routeName: string,\n params?: Params,\n search?: SearchParams,\n options?: ActiveRouteSourceOptions,\n): RouterSource<boolean> {\n const { strict, ignoreQueryParams, hash } = normalizeActiveOptions(options);\n\n // Two paths produce a fresh (non-cached) source: a NON-STRING route name\n // (the guard below, #1881) and params/search `canonicalJson` cannot\n // serialize. Callers hit either rarely; the extra allocation is acceptable.\n let key: string | undefined;\n\n // ⚑ A NON-STRING name never gets a cache slot, and this is a correctness\n // guard rather than a serialization one. The key below is a template literal,\n // so it COERCES the name: `{ toString: () => \"fwd\" }` and the string `\"fwd\"`\n // produce the identical key and therefore share one cached source. Whichever\n // call arrives first decides the answer for both.\n //\n // ⚠ Core does NOT refuse a non-string name (`packages/core/ARCHITECTURE.md`,\n // \"Route-Name Type Gates\"), so the collision is invisible for a name that\n // resolves through `forwardTo` — router on `/users`, a bag naming `\"fwd\"`\n // answers `true`, same as the string. It is NOT invisible for a plain name:\n // `isActiveRoute` compares the active name by IDENTITY first, so a bag naming\n // `\"users\"` answers `false` while the router is on `/users`, and the\n // well-typed sibling that shares its cache slot inherits that `false` — a\n // correct `<Link to=\"users\">` goes dark. That half needs no drifting\n // `toString` and no gate anywhere to reproduce; bypassing the cache keeps the\n // bad call's answer to itself.\n if (typeof routeName !== \"string\") {\n return buildActiveRouteSource(\n router,\n routeName,\n params,\n search,\n strict,\n ignoreQueryParams,\n hash,\n );\n }\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 // `params === undefined` is the common Link case (`<Link to=\"users\">`\n // with no params). Skip canonicalJson(undefined) — it returns the literal\n // string \"undefined\" and template interpolation would just embed it. An\n // explicit empty sentinel avoids the call and shaves the cache-key by 9\n // characters per Link.\n const paramsKey = params === undefined ? \"\" : canonicalJson(params);\n\n // Query channel (RFC-4 M2, #1548) — keyed the same way as `params`:\n // `undefined` (the common no-search Link) collapses to the empty sentinel\n // to skip `canonicalJson(undefined)` and share one entry with a no-search\n // call. Distinct from `params` so `{ search: { q } }` and `{ params: { q } }`\n // never collide on the same cache slot.\n const searchKey = search === undefined ? \"\" : canonicalJson(search);\n\n // Delimiter `|` is safe because route names use `.` as the segment\n // separator (`users.list`, not `users|list`) and canonicalJson-encoded\n // params escape `\"` (so any literal `|` inside params lives inside a\n // quoted JSON string and can't be confused with our delimiter). If route\n // names ever grow a `|` character, this composite key would become\n // ambiguous — change the separator to a control char or hash-encode each\n // field.\n key = `${routeName}|${paramsKey}|${searchKey}|${String(strict)}|${String(ignoreQueryParams)}|${hashKey}`;\n } catch {\n key = undefined;\n }\n\n if (key === undefined) {\n // Non-cached fallback (canonicalJson threw on BigInt / circular / etc.).\n // Return the real source — `destroy()` must unwind the router subscription;\n // otherwise the wrapper leaks for the lifetime of the router.\n return buildActiveRouteSource(\n router,\n routeName,\n params,\n search,\n strict,\n ignoreQueryParams,\n hash,\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 search,\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 * 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 search: SearchParams | undefined,\n strict: boolean,\n ignoreQueryParams: boolean,\n hash: string | undefined,\n): boolean {\n const routeActive = router.isActiveRoute(\n routeName,\n params,\n // Query channel at position 3 (RFC-4 M2 / #1548). `isActiveRoute` only\n // consults it when `ignoreQueryParams` is false — the default active-route\n // match (ignoreQueryParams: true) still ignores it, so a `routeSearch`-less\n // Link behaves exactly as before.\n search,\n strict,\n ignoreQueryParams,\n );\n\n if (!routeActive) {\n return false;\n }\n if (hash === undefined) {\n return true;\n }\n\n // `readContextHash` returns `undefined` when no URL plugin claimed the\n // namespace (hash-plugin runtime, memory-plugin, SSR). For hash-equality\n // matching we collapse that to `\"\"` — a hash-aware Link with no URL plugin\n // can only match when the consumer also asked for `hash: \"\"`.\n return (readContextHash(router.getState()) ?? \"\") === hash;\n}\n\nfunction buildActiveRouteSource(\n router: Router,\n routeName: string,\n params: Params | undefined,\n search: SearchParams | 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 search,\n strict,\n ignoreQueryParams,\n hash,\n );\n\n let routerUnsubscribe: (() => void) | undefined;\n\n const disconnect = (): void => {\n const unsub = routerUnsubscribe;\n\n routerUnsubscribe = undefined;\n unsub?.();\n };\n\n const source = new BaseSource(initialValue, {\n onFirstSubscribe: () => {\n // Reconcile before connecting: while disconnected (zero listeners) the\n // source missed navigations, so the active boolean may be stale.\n // Recompute against the current router state and notify the just-added\n // listener — BaseSource registers it before onFirstSubscribe. Mirrors the\n // reconnect reconcile of createRouteNodeSource / createRouteSource (#765).\n const reconciled = computeActive(\n router,\n routeName,\n params,\n search,\n strict,\n ignoreQueryParams,\n hash,\n );\n\n if (!Object.is(source.getSnapshot(), reconciled)) {\n source.updateSnapshot(reconciled);\n }\n\n // Lazy connection (#766): subscribe on the FIRST listener, disconnect on\n // the LAST (onLastUnsubscribe). Previously this source subscribed eagerly\n // at construction with a no-op destroy on the cached wrapper, so every\n // unique cache key held a PERMANENT router subscription that survived all\n // Link unmounts — a long-lived router with per-item-params Links\n // accumulated handles until the EventEmitter listener limit (10000)\n // crashed the render path. The cache entry still lives with the router\n // (cheap: a closure), but now holds no subscription while it has zero\n // listeners; the non-cached paths (a non-string name, or unserializable params) likewise\n // detaches through `disconnect`.\n routerUnsubscribe = 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\n // `hashChanged` flag published by URL plugins.\n const hashFlip =\n hash !== undefined &&\n ((\n next.route.context as\n { 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 search,\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 onLastUnsubscribe: disconnect,\n onDestroy: disconnect,\n });\n\n return source;\n}\n","import { areRoutesRelated } from \"@real-router/route-utils\";\n\nimport { noopDestroy } from \"./internal/noopDestroy.js\";\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 — for diffing 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 // Empty string represents the root of the name hierarchy — every named\n // route is its descendant. Without this short-circuit, `current.name`\n // would have to equal `\"\"` or start with `\".\"` (both impossible for\n // valid route names), breaking symmetry with `createRouteNodeSource(\"\")`\n // which is always-active when a route is current.\n if (routeName === \"\") {\n return true;\n }\n\n return (\n current.name === routeName || current.name.startsWith(`${routeName}.`)\n );\n };\n\n // Diffs and notifies a single route name on a router transition. Extracted\n // from the subscribe callback so the fan-out loop stays a trivial\n // isolate-per-name shell (#1478) — the recompute below (`areRoutesRelated` /\n // `isActiveNonStrict`) runs OUTSIDE the per-listener `try`, so the caller\n // wraps this whole call in a per-name `try` too.\n const notifyName = (\n routeName: string,\n listeners: Set<() => void>,\n newRouteName: string,\n previousRouteName: string | undefined,\n ): void => {\n // Cheap pre-filter: if neither new nor previous route is related to this\n // name, its active state cannot have changed. Empty routeName is the\n // implicit root — every route is its descendant, so the filter would\n // falsely exclude it (`areRoutesRelated` doesn't treat `\"\"` specially).\n // Skip the filter for the root.\n if (routeName !== \"\") {\n const isNewRelated = areRoutesRelated(routeName, newRouteName);\n const isPrevRelated =\n previousRouteName !== undefined &&\n areRoutesRelated(routeName, previousRouteName);\n\n if (!isNewRelated && !isPrevRelated) {\n return;\n }\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 return;\n }\n\n activeByName.set(routeName, nextActive);\n // Per-listener exception isolation — mirrors `BaseSource.notify`\n // (INVARIANTS \"BaseSource 3\"). Without it, one throwing Link listener would\n // abort notifications to the remaining listeners of this name (#767).\n // `activeByName.set` already ran above, so the per-name diff is committed\n // before any listener fires. Re-throw asynchronously so the genuine bug\n // still surfaces to global error handlers.\n for (const listener of listeners) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\n }\n };\n\n const connect = (): void => {\n routerUnsubscribe = router.subscribe((next) => {\n const newRouteName = next.route.name;\n const previousRouteName = next.previousRoute?.name;\n\n for (const [routeName, listeners] of listenersByName) {\n // Per-name exception isolation (#1478): `notifyName`'s recompute runs\n // OUTSIDE its per-listener `try`. A throw from it for one name would\n // unwind this shared subscribe callback, skipping every later name's\n // diff/notify and leaving their `activeByName` stale — structurally the\n // #767 failure mode one level up. Safe by construction today (`getState`\n // is a frozen-field read; the rest is pure string ops with no user code\n // in the path), but a future param-aware / predicate recompute could\n // throw here; isolate it the same way, re-throwing asynchronously so a\n // genuine bug still surfaces.\n try {\n notifyName(routeName, listeners, newRouteName, previousRouteName);\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\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\n // Stale-generation guard: if this name's registered Set is no longer the\n // one this closure captured, its generation was already fully torn down\n // (all listeners removed → Set deleted) and possibly re-created by a later\n // subscribe. Deleting / disconnecting against the stale Set would delete\n // the LIVE generation's map entry (the empty stale Set trips\n // `size === 0`), orphaning its subscribers. Nothing left for us to clean\n // up here — bail (#1206).\n if (listenersByName.get(routeName) !== listeners) {\n return;\n }\n\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","import { createActiveNameSelector } from \"./createActiveNameSelector.js\";\nimport { createActiveRouteSource } from \"./createActiveRouteSource.js\";\n\nimport type { RouterSource } from \"./types.js\";\nimport type { Params, Router, SearchParams } from \"@real-router/core\";\n\nconst NOOP = (): void => {};\n\n/**\n * Framework-agnostic fast/slow builder for an adapter `<Link>`'s active-route\n * `RouterSource<boolean>`. Every adapter Link routes its active-state check\n * through this one function so the fast/slow decision (and the `routeName !== \"\"`\n * guard) lives in a single place — a per-adapter copy of the decision drifting\n * from the shared one is exactly what produced #1416 (the vue Link kept building\n * a per-link source while the fast path sat unused in a composable).\n *\n * **Fast path** — a default-options link (non-empty `routeName`, no custom\n * `params`, non-strict, query params ignored, no `hash`) shares the per-router\n * {@link createActiveNameSelector}: ONE `router.subscribe` handle serves any\n * number of distinct-`routeName` links, versus a per-link\n * {@link createActiveRouteSource} that allocates a `BaseSource` AND opens its own\n * router subscription for every link (the 10k-listener-cap crash documented on\n * `createActiveRouteSource`). The selector's `isActive` is exactly non-strict,\n * query-ignoring, name-only matching — identical to the default\n * `createActiveRouteSource`, including for a non-string name: both coerce it and\n * both answer what its `toString` named (core does not gate the name here — see\n * `packages/core/ARCHITECTURE.md` \"Route-Name Type Gates\"). `destroy()` is a\n * no-op (the selector is per-router cached and outlives the link); the returned\n * `subscribe`'s unsubscribe removes only this link's listener.\n *\n * **Slow path** — any deviation from the defaults (custom `params`, `strict`,\n * `ignoreQueryParams: false`, hash-aware #532) falls to the per-link\n * `createActiveRouteSource`, whose cache covers the full argument surface. An\n * **empty `routeName`** also takes the slow path: it is a misuse (builds no href,\n * logs a `console.error`), and the selector's root-active `isActive(\"\") === true`\n * would light up a misused link, whereas `router.isActiveRoute(\"\")` is `false` —\n * the slow path preserves that.\n */\nexport function createActiveSource(\n router: Router,\n routeName: string,\n params: Params | undefined,\n search: SearchParams | undefined,\n strict: boolean,\n ignoreQueryParams: boolean,\n hash: string | undefined,\n): RouterSource<boolean> {\n if (\n routeName !== \"\" &&\n params === undefined &&\n // A `routeSearch` link forces the slow path (RFC-4 M2, #1548): the\n // name-only selector is search-blind, so it can't answer a query-scoped\n // active check — exactly the reason a custom `hash` also falls through.\n search === undefined &&\n !strict &&\n ignoreQueryParams &&\n hash === undefined\n ) {\n const selector = createActiveNameSelector(router);\n\n return {\n subscribe: (listener) => selector.subscribe(routeName, listener),\n getSnapshot: () => selector.isActive(routeName),\n destroy: NOOP,\n };\n }\n\n // The `hash` argument (#532) participates in the cache key when defined.\n // exactOptionalPropertyTypes forbids `{ hash: undefined }` literally — include\n // the `hash` key only when a value is provided.\n return createActiveRouteSource(\n router,\n routeName,\n params,\n search,\n hash === undefined\n ? { strict, ignoreQueryParams }\n : { strict, ignoreQueryParams, hash },\n );\n}\n","import { events } from \"@real-router/core\";\nimport { getPluginApi } from \"@real-router/core/api\";\n\nimport { BaseSource } from \"./BaseSource\";\nimport { noopDestroy } from \"./internal/noopDestroy.js\";\nimport { stabilizeState } from \"./stabilizeState.js\";\n\nimport type { RouterTransitionSnapshot, RouterSource } from \"./types.js\";\nimport type { Router, State } from \"@real-router/core\";\n\n// Frozen so accidental consumer mutation (`source.getSnapshot().toRoute = X`)\n// throws in strict mode. The singleton ref is shared across every IDLE state\n// for the lifetime of the process — mutating it would corrupt the contract\n// \"all IDLE snapshots are the same object reference\" relied on by every\n// adapter's useSyncExternalStore equivalent.\nconst IDLE_SNAPSHOT: RouterTransitionSnapshot = Object.freeze({\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\n/**\n * @internal test-only export — returns the next snapshot for a TRANSITION_START\n * payload, or `null` when the same-paths dedup guard should suppress the\n * update. Exported so the (structurally-unreachable after #605) guard can be\n * exercised by unit tests without resorting to private-API hacks.\n */\nexport function nextTransitionStartSnapshot(\n prev: RouterTransitionSnapshot,\n toState: State,\n fromState: State | undefined,\n): RouterTransitionSnapshot | null {\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 return null;\n }\n\n return {\n isTransitioning: true,\n isLeaveApproved: false,\n toRoute: newToRoute,\n fromRoute: newFromRoute,\n };\n}\n\n/**\n * @internal test-only export — analogous to {@link nextTransitionStartSnapshot}\n * for the LEAVE_APPROVE payload. The guard is structurally unreachable in\n * practice (router emits LEAVE_APPROVE exactly once per pipeline) but stays\n * for plugin-driven re-entrant flows.\n */\nexport function nextLeaveApproveSnapshot(\n prev: RouterTransitionSnapshot,\n toState: State,\n fromState: State | undefined,\n): RouterTransitionSnapshot | null {\n const newToRoute = stabilizeState(prev.toRoute, toState);\n const newFromRoute = stabilizeState(prev.fromRoute, fromState ?? null);\n\n if (\n prev.isLeaveApproved &&\n newToRoute === prev.toRoute &&\n newFromRoute === prev.fromRoute\n ) {\n return null;\n }\n\n return {\n isTransitioning: true,\n isLeaveApproved: true,\n toRoute: newToRoute,\n fromRoute: newFromRoute,\n };\n}\n\nexport function createTransitionSource(\n router: Router,\n): RouterSource<RouterTransitionSnapshot> {\n // Declared before `source` so its onDestroy closure closes over an\n // initialized binding — a mid-registration throw below no longer leaves it\n // in the TDZ, keeping the source destroyable (#1440).\n const unsubs: (() => void)[] = [];\n\n const source = new BaseSource(IDLE_SNAPSHOT, {\n onDestroy: () => {\n for (const unsub of unsubs) {\n unsub();\n }\n },\n });\n\n const api = getPluginApi(router);\n\n const resetToIdle = (): void => {\n source.updateSnapshot(IDLE_SNAPSHOT);\n };\n\n const onTransitionStart = (toState: State, fromState?: State): void => {\n // The same-paths dedup branch inside nextTransitionStartSnapshot is\n // structurally unreachable after #605 (every router-emitted\n // TRANSITION_START carries a fresh State per navigate()), but the\n // helper is kept testable for future stabilizer changes — see the\n // direct unit test in createTransitionSource.test.ts.\n const next = nextTransitionStartSnapshot(\n source.getSnapshot(),\n toState,\n fromState,\n );\n\n /* v8 ignore next 3 -- @preserve: dedup-skip branch unreachable through\n normal router flow; covered directly via nextTransitionStartSnapshot\n unit test. */\n if (next === null) {\n return;\n }\n\n source.updateSnapshot(next);\n };\n\n const onLeaveApprove = (toState: State, fromState?: State): void => {\n const next = nextLeaveApproveSnapshot(\n source.getSnapshot(),\n toState,\n fromState,\n );\n\n /* v8 ignore next 3 -- @preserve: dedup-skip branch unreachable through\n normal router flow (LEAVE_APPROVE fires once per pipeline); covered\n directly via nextLeaveApproveSnapshot unit test. */\n if (next === null) {\n return;\n }\n\n source.updateSnapshot(next);\n };\n\n // Eager connection: subscribe to router events immediately. Register\n // one-by-one so a throw mid-registration (the emitter rejects a duplicate\n // listener / hits its maxListeners cap) unwinds the already-registered\n // listeners instead of leaking them and stranding the half-wired source\n // (#1440). Mirrors @real-router/rx's events$ partial-registration safety.\n /* eslint-disable unicorn/prefer-single-call -- register one-by-one so a\n mid-registration throw unwinds the already-pushed unsubs; a single\n push(a, b) evaluates every addEventListener arg BEFORE pushing, leaving an\n earlier registration untracked and leaked (#1440). */\n try {\n unsubs.push(\n api.addEventListener(events.TRANSITION_START, onTransitionStart),\n );\n unsubs.push(\n api.addEventListener(events.TRANSITION_LEAVE_APPROVE, onLeaveApprove),\n );\n unsubs.push(api.addEventListener(events.TRANSITION_SUCCESS, resetToIdle));\n unsubs.push(api.addEventListener(events.TRANSITION_ERROR, resetToIdle));\n unsubs.push(api.addEventListener(events.TRANSITION_CANCEL, resetToIdle));\n } catch (error) {\n for (const unsub of unsubs) {\n unsub();\n }\n\n throw error;\n }\n /* eslint-enable unicorn/prefer-single-call */\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","import { events } from \"@real-router/core\";\nimport { getPluginApi } from \"@real-router/core/api\";\n\nimport { BaseSource } from \"./BaseSource\";\nimport { noopDestroy } from \"./internal/noopDestroy.js\";\n\nimport type { RouterErrorSnapshot, RouterSource } from \"./types.js\";\nimport type { Router, State, RouterError } from \"@real-router/core\";\n\n// Frozen shared singleton — returned by every error source's getSnapshot()\n// until the first error. Mirrors createTransitionSource's IDLE_SNAPSHOT: a\n// consumer mutating it would corrupt the shared singleton for every error\n// source of every router (#768).\nconst INITIAL_SNAPSHOT: RouterErrorSnapshot = Object.freeze({\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 let hasError = false;\n\n // Declared before `source` so its onDestroy closure closes over an\n // initialized binding — a mid-registration throw below no longer leaves it\n // in the TDZ, keeping the source destroyable (#1440).\n const unsubs: (() => void)[] = [];\n\n const source = new BaseSource(INITIAL_SNAPSHOT, {\n onDestroy: () => {\n for (const unsub of unsubs) {\n unsub();\n }\n },\n });\n\n const api = getPluginApi(router);\n\n // Eager connection: subscribe to router events immediately. Register\n // one-by-one so a throw mid-registration (the emitter rejects a duplicate\n // listener / hits its maxListeners cap) unwinds the already-registered\n // listeners instead of leaking them and stranding the half-wired source\n // (#1440). Mirrors @real-router/rx's events$ partial-registration safety.\n /* eslint-disable unicorn/prefer-single-call -- register one-by-one so a\n mid-registration throw unwinds the already-pushed unsubs; a single\n push(a, b) evaluates every addEventListener arg BEFORE pushing, leaving an\n earlier registration untracked and leaked (#1440). */\n try {\n unsubs.push(\n api.addEventListener(\n events.TRANSITION_ERROR,\n (\n toState: State | undefined,\n fromState: State | undefined,\n err: RouterError,\n ) => {\n errorVersion++;\n hasError = true;\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 );\n unsubs.push(\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 (hasError) {\n hasError = false;\n source.updateSnapshot({\n error: null,\n toRoute: null,\n fromRoute: null,\n version: errorVersion,\n });\n }\n }),\n );\n } catch (error) {\n for (const unsub of unsubs) {\n unsub();\n }\n\n throw error;\n }\n /* eslint-enable unicorn/prefer-single-call */\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\n/**\n * Eagerly create (and thus subscribe) the per-router error source IF the router\n * supports the plugin API — used by framework adapters' `RouterProvider` to\n * capture a navigation error that fires BEFORE a `RouterErrorBoundary` mounts\n * (#778). A router-like without an internals-registry entry (a test stub, an\n * `Object.create`-derived object, or anything not produced by `createRouter`)\n * makes `getErrorSource` throw via `getPluginApi`; here that degrades to a no-op.\n *\n * The asymmetry is deliberate: `createRouteSource` tolerates such routers (it is\n * lazy and only touches `router.subscribe`), so a Provider that eagerly primes\n * the stricter error source must not crash where the route source would not. A\n * boundary mounting later still creates the error source lazily and surfaces a\n * genuine invalid-router error there.\n */\nexport function primeErrorSource(router: Router): void {\n try {\n getErrorSource(router);\n } catch {\n // Router has no internals-registry entry — nothing to prime eagerly.\n }\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { getErrorSource } from \"./createErrorSource\";\nimport { noopDestroy } from \"./internal/noopDestroy.js\";\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 // Hoisted up here so the `onFirstSubscribe` closure below can read/write\n // it before `disconnect()`'s declaration. JS hoisting makes the original\n // post-declaration order legal, but reading top-to-bottom is clearer.\n let unsubFromError: (() => void) | null = null;\n\n const buildDismissableSnapshot = (): 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>(\n buildDismissableSnapshot(),\n {\n onFirstSubscribe: () => {\n unsubFromError = errorSource.subscribe(() => {\n source.updateSnapshot(buildDismissableSnapshot());\n });\n\n // Catch up: the eager underlying error source may already hold (or have\n // since cleared) an error that changed while this wrapper had zero\n // subscribers — at first-ever subscribe, or after a disconnect/reconnect\n // cycle (onLastUnsubscribe below). Without this, a RouterErrorBoundary\n // mounting AFTER a boot-time error would never see it (#765.2). Guard on\n // the resolved `error` so the common no-error case stays a no-op: the\n // listener was added before this callback (BaseSource ordering), so an\n // unconditional updateSnapshot would notify it redundantly and break the\n // \"subscribers fire only on state-relevant actions\" contract. The\n // `version` field can skip values for an error cycle fully missed during\n // a disconnect — harmless, monotonicity still holds.\n const caughtUp = buildDismissableSnapshot();\n\n if (caughtUp.error !== source.getSnapshot().error) {\n source.updateSnapshot(caughtUp);\n }\n },\n onLastUnsubscribe: () => {\n disconnect();\n },\n },\n );\n\n function resetError(): void {\n const currentVersion = errorSource.getSnapshot().version;\n\n // No-op guard: if we already dismissed at this version (or are even ahead\n // of the live error stream), there's nothing to clear. Skipping prevents\n // a redundant snapshot allocation + listener notification under tight\n // resetError(); resetError() patterns — common when a RouterErrorBoundary\n // user clicks \"dismiss\" while another dismiss is already in flight.\n if (currentVersion <= dismissedVersion) {\n return;\n }\n\n dismissedVersion = currentVersion;\n source.updateSnapshot(buildDismissableSnapshot());\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","import type { State } from \"@real-router/core\";\n\n/**\n * Context handed to a route-enter handler once a navigation-driven mount is\n * confirmed. `previousRoute` is **non-nullable** — the gate is the sole bridge\n * from the nullable route snapshot (`{ route?, previousRoute? }`) to this\n * non-nullable contract (#1218).\n */\nexport interface RouteEnterContext {\n /** The route that was just activated. */\n route: State;\n /** The route that was active immediately before this navigation. */\n previousRoute: State;\n}\n\n/**\n * Decision closure returned by {@link createRouteEnterGate}.\n *\n * Given the already-unwrapped route snapshot (adapters own the reactive\n * unwrap) and the per-call `skipSameRoute` flag, returns the\n * {@link RouteEnterContext} to dispatch — or `null` to skip. Stateful: it\n * holds `lastHandledRoute` for the dedupe arm.\n */\nexport type RouteEnterGate = (\n route: State | undefined,\n previousRoute: State | undefined,\n skipSameRoute: boolean,\n) => RouteEnterContext | null;\n\n/**\n * Builds the framework-agnostic route-enter guard gate shared by every\n * adapter's `useRouteEnter` / `injectRouteEnter` (#1435). Each adapter owns\n * only the reactive effect wiring + snapshot unwrap + dispatch; this closure\n * owns the canonical guard superset and the `lastHandledRoute` dedupe state.\n *\n * Guard order (behavior-neutral superset of all six adapters):\n *\n * 1. `!route` — no committed route yet (svelte SSR / pre-start; a no-op for\n * the other adapters, whose `route` is defined by the time they call in).\n * 2. skip-initial — `!route.transition.from`: the first commit from\n * `router.start()` is not an \"entry\".\n * 3. same-route skip — `route.transition.from === route.name` (query-only /\n * sort-filter navigations), gated by the per-call `skipSameRoute`.\n * 4. dedupe — same snapshot reference dispatches at most once (guards\n * React StrictMode's dev double-invoke; a defensive no-op elsewhere).\n * 5. `!previousRoute` — the sole guard for the non-nullable\n * `RouteEnterContext.previousRoute` contract (#1218: reachable via a\n * Provider mounted after a navigation / an `<Activity>` catch-up, where\n * the source snapshot carries `previousRoute: undefined`).\n *\n * **`skipSameRoute` is a per-call argument, not a factory option** — React\n * holds the returned closure in a `useRef` so its `lastHandledRoute` survives\n * StrictMode effect re-runs; a factory option would force a fresh gate (and\n * reset the dedupe state) on an options flip. Threading it per-call lets the\n * same gate honor the flip without losing state.\n *\n * The gate **returns** the context and does not dispatch — dispatch stays\n * adapter-side (React/Preact via a latest-handler ref, others via a\n * capture-at-init handler). Framework pre-gates (e.g. Vue's `<KeepAlive>`\n * `isDeactivated`, #1221) compose *before* this gate and stay adapter-side.\n */\nexport function createRouteEnterGate(): RouteEnterGate {\n let lastHandledRoute: State | null = null;\n\n return (route, previousRoute, skipSameRoute) => {\n if (!route) {\n return null;\n }\n\n // `?.` on a field the type declares required, because core's commit door\n // commits a foreign State's ABSENT `transition` rather than fabricating one\n // (#1792 / #1976) — measured, the flat read threw here. Absent means \"no\n // origin known\", the answer this gate already gives a first navigation.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- the required field is genuinely absent on a foreign committed State\n if (!route.transition?.from) {\n return null;\n }\n\n if (skipSameRoute && route.transition.from === route.name) {\n return null;\n }\n\n if (lastHandledRoute === route) {\n return null;\n }\n\n if (!previousRoute) {\n return null;\n }\n\n lastHandledRoute = route;\n\n return { route, previousRoute };\n };\n}\n","import type { Router, State } from \"@real-router/core\";\n\n// `LeaveState` / `LeaveFn` live in `@real-router/types` but are NOT re-exported\n// from `@real-router/core` (which re-exports `State`), and `sources` has no\n// `@real-router/types` dependency (house rule). Derive the exact leave-listener\n// type from the facade instead — tsc-clean with zero new deps (#1435 D-C).\ntype LeaveFn = Parameters<Router[\"subscribeLeave\"]>[0];\n\n/**\n * Context handed to a route-exit handler when a genuine departure is approved.\n * Re-states core's `LeaveState` shape under the adapter-canonical name so the\n * six adapters can re-export it verbatim (#1435).\n */\nexport interface RouteExitContext {\n /** The route being left (`fromState`). */\n route: State;\n /** The navigation target (`toState`). */\n nextRoute: State;\n /** Aborts if the navigation is cancelled or fails before commit. */\n signal: AbortSignal;\n}\n\nexport interface UseRouteExitOptions {\n /**\n * Skip the handler when `route.name === nextRoute.name` (sort/filter/\n * query-only navigations on the same route). Default: `true`.\n */\n skipSameRoute?: boolean;\n}\n\ntype LeaveHandler = (context: RouteExitContext) => void | Promise<void>;\n\n/**\n * Wraps a route-exit `handler` into the canonical `subscribeLeave` listener\n * shared by every adapter's `useRouteExit` / `injectRouteExit` (#1435). The\n * adapter owns only registration + cleanup (and, for React/Preact, the\n * latest-handler ref via a thunk); this HOF owns the guard sequence:\n *\n * 1. same-route skip — `route.name === nextRoute.name` (live `State` names,\n * pre-commit), gated by `skipSameRoute` (default `true`).\n * 2. reentrant-abort pre-check — a `signal` already aborted when the listener\n * fires means the navigation is gone; `addEventListener(\"abort\", …)` does\n * not fire retroactively, so skip the handler entirely.\n * 3. passthrough — `return handler(ctx)` so the returned Promise reaches\n * core's `settleLeavePromises` and **blocks** the activation phase.\n *\n * **Error contract:** a rejected handler Promise rejects `navigate()` with the\n * handler's **original error** and emits `TRANSITION_ERROR` — it is NOT\n * re-coded to `TRANSITION_CANCELLED`. `TRANSITION_CANCELLED` arises only when\n * the payload `signal` aborts (supersede / `stop()` / `dispose()` / external\n * `opts.signal`). A fire-and-forget call here would silently un-block the\n * transition, so arm 3 must stay a passthrough.\n */\nexport function guardLeaveListener(\n handler: LeaveHandler,\n options?: UseRouteExitOptions,\n): LeaveFn {\n const skipSameRoute = options?.skipSameRoute ?? true;\n\n return ({ route, nextRoute, signal }) => {\n if (skipSameRoute && route.name === nextRoute.name) {\n return;\n }\n\n if (signal.aborted) {\n return;\n }\n\n return handler({ route, nextRoute, signal });\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["#listeners","#onFirstSubscribe","#onLastUnsubscribe","#onDestroy","#currentSnapshot","#destroyed","events","events"],"sources":["../../src/BaseSource.ts","../../src/internal/readContextHash.ts","../../src/stabilizeState.ts","../../src/createRouteSource.ts","../../src/computeSnapshot.ts","../../src/internal/noopDestroy.ts","../../src/createRouteNodeSource.ts","../../src/canonicalJson.ts","../../src/normalizeActiveOptions.ts","../../src/createActiveRouteSource.ts","../../src/createActiveNameSelector.ts","../../src/createActiveSource.ts","../../src/createTransitionSource.ts","../../src/createErrorSource.ts","../../src/createDismissableError.ts","../../src/createRouteEnterGate.ts","../../src/guardLeaveListener.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 // Isolate listener exceptions so a single throwing subscriber (e.g. React\n // error-boundary fallback throwing inside `onStoreChange`) does not block\n // the remaining subscribers — the invariant \"after updateSnapshot all\n // listeners see the new snapshot\" must hold. Re-throw asynchronously via\n // queueMicrotask so global error handlers / test harnesses still surface\n // the bug without breaking the synchronous notification fan-out.\n for (const listener of this.#listeners) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\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 * @internal\n *\n * Reads the URL fragment published by `browser-plugin` / `navigation-plugin`\n * on a router state. The plugins claim the `\"url\"` namespace via\n * `state.context.url` and the `hash` field carries the **decoded** fragment.\n *\n * Returns:\n * - `undefined` — no plugin claimed the `\"url\"` namespace (hash-plugin runtime,\n * memory-plugin, SSR before hydration) OR the state itself is nullish;\n * - `\"\"` — the URL namespace exists but the fragment is empty\n * (browser-plugin on a hash-less URL).\n *\n * Callers that need the \"no namespace at all\" branch (e.g. `stabilizeState`\n * comparing cross-plugin transitions) read the raw `undefined`. Callers that\n * collapse \"no namespace\" to \"no hash\" (e.g. `createActiveRouteSource`'s\n * hash-equality check) coalesce with `?? \"\"` themselves.\n *\n * Centralising the context cast removes the previous duplicate definitions in\n * `stabilizeState.ts` and `createActiveRouteSource.ts` that drifted in\n * signature (state vs router) and default-value (undefined vs \"\") — both\n * variants are reconstructible from this single helper at the callsite.\n */\nexport function readContextHash(\n state: State | null | undefined,\n): string | undefined {\n const ctx = state?.context as { url?: { hash?: string } } | undefined;\n\n return ctx?.url?.hash;\n}\n","import { readContextHash } from \"./internal/readContextHash.js\";\n\nimport type { State } from \"@real-router/core\";\n\n/**\n * State-aware stabilization for route snapshots.\n *\n * Compares `path` (canonical name+params), `state.context.url.hash`\n * (URL fragment, #532), and `state.transition.reload` (#605). When all\n * three match (idempotent navigation), returns `prev` (preserving\n * reference) so frameworks can skip re-renders. When any of them flips,\n * returns `next` so consumers subscribing through `useRoute()` see the\n * new state.\n *\n * `transition.reload === true` is the user's explicit signal for a\n * non-idempotent navigation — `router.navigate(name, params, search, {\n * reload: true })` is the canonical pairing for `invalidate(router, namespace)`\n * and any cache-bust pattern. Bypassing stabilization for reloads makes\n * `useRoute()` consumers see fresh `state.context.<namespace>` values\n * written by the SSR loader plugin's `subscribeLeave` handler.\n *\n * Ignores `meta` (internal: auto-increment id), other `transition` fields\n * (`from`, `segments`, `redirected`), and `state.context.navigation` /\n * `state.context.browser` (transient transition metadata) — they don't\n * affect render identity for idempotent navigations.\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 // Explicit reload navigation (#605) — caller asked to bypass dedupe so\n // observers see fresh `state.context` written by `invalidate()`-driven\n // loader re-runs. The path equality above guarantees both prev and next\n // are either non-null with matching paths or both nullish; only the\n // non-null branch can carry a meaningful `transition.reload`.\n if (readReloadFlag(next)) {\n return next;\n }\n\n return prev;\n}\n\nfunction readReloadFlag(state: State | null | undefined): boolean {\n // Defensive read: `transition` is mandatory in the public State type, but a\n // plugin returning a malformed state (or a future fork) shouldn't crash the\n // stabilizer with a TypeError. We cast to a structurally-loose shape so the\n // optional chain is permitted; the runtime guard preserves dedup (false =\n // not-a-reload) for malformed inputs.\n const transition = (\n state as { transition?: { reload?: boolean } } | null | undefined\n )?.transition;\n\n return transition?.reload === true;\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 // Reconcile the route with the current router state before connecting.\n // While this source had zero listeners it was disconnected\n // (onLastUnsubscribe below), so a navigation in that window was missed —\n // the just-added listener would otherwise observe a stale route (#765).\n // BaseSource registered the listener before this callback, so the\n // updateSnapshot below reaches it.\n //\n // Emit ONLY when the route actually changed: a hide/show (Activity)\n // cycle with no navigation in between must not fire a spurious\n // re-render, and its still-valid `previousRoute` must survive. When a\n // navigation WAS caught up, `previousRoute` resets to `undefined`: the\n // catch-up is a snap to the current state, not an observed navigation,\n // and the true previous route cannot be reconstructed outside a live\n // subscribe payload (parity of intent with createRouteNodeSource, whose\n // reconnect reconcile likewise drops previousRoute on catch-up).\n const current = source.getSnapshot();\n const reconciledRoute = stabilizeState(\n current.route,\n router.getState(),\n );\n\n if (reconciledRoute !== current.route) {\n source.updateSnapshot({\n route: reconciledRoute,\n previousRoute: undefined,\n });\n }\n\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","/**\n * @internal\n *\n * Shared no-op `destroy()` for cached `RouterSource` wrappers.\n *\n * Cached factories (`getTransitionSource`, `getErrorSource`, `createDismissableError`,\n * `createActiveNameSelector`, cache hit in `createRouteNodeSource`,\n * `createActiveRouteSource`) return a wrapper whose `destroy()` is a no-op —\n * the underlying source is shared across all consumers and lives as long as\n * the router (WeakMap entry releases on router GC).\n *\n * One module-level function shared by every cached factory keeps the wrapper\n * shape (`{ subscribe, getSnapshot, destroy: noopDestroy }`) byte-stable and\n * eliminates the previous six copies. (Bundlers inline a stand-alone arrow\n * just as readily, so the cost is purely on the maintenance side.)\n */\nexport function noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { computeSnapshot } from \"./computeSnapshot.js\";\nimport { noopDestroy } from \"./internal/noopDestroy.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 // computeSnapshot returns the SAME currentSnapshot reference when\n // both route and previousRoute stabilize to prev — guard against\n // emitting redundant updates to listeners (matters for signal-\n // based adapters that re-run effects on every set).\n /* v8 ignore next 3 -- @preserve: structurally unreachable after #605\n — reload navs always return fresh refs via stabilizeState, and\n within-node non-reload navs short-circuit at shouldUpdate. Guard\n kept for defensive correctness against future stabilizer changes. */\n if (Object.is(source.getSnapshot(), newSnapshot)) {\n return;\n }\n\n source.updateSnapshot(newSnapshot);\n });\n },\n onLastUnsubscribe: disconnect,\n },\n );\n\n return source;\n}\n","/**\n * Intrinsics captured at module load (#1971).\n *\n * ⚑ These DECIDE — each answers \"what is on this object\" for a value this module\n * did not build, so read off the live global they are the weakest point of every\n * check built on them. `guards.ts` states the doctrine and its measurement: one\n * naive `Object.hasOwn` polyfill walked straight through five sibling readers\n * while the single captured guard held.\n *\n * ⚠ Capture narrows the window from \"any time after boot\" to \"before this module\n * loads\". It does not close it — a shim evaluated ahead of core still wins\n * (#1798), which is the doctrine's own caveat and travels with it.\n */\nconst objectKeys = Object.keys;\n\n/** Captured like the deciding seven, but this one BUILDS the guarantee (#2072). */\nconst objectCreate = Object.create;\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 * **Divergence from `shared/dom-utils/scroll-restore.canonicalJson` — by\n * design.** That sibling implementation is the cheap navigation-hot-path\n * variant (uses `localeCompare`, plain-object accumulator, native cycle\n * detector) and pairs with `safeKeyOf` for crash-tolerance. This one is the\n * strict cache-key variant: byte-order compare, prototype-less accumulator,\n * bespoke cycle detection — eagerly throws on inputs that would cause\n * collisions or pollution, so callers can fall back to non-cached sources.\n * They are NOT interchangeable; cross-package equivalence is explicitly not\n * a goal (audit-2 / audit-2026-05-17 §2).\n *\n * Edge cases:\n * - Arrays preserve order (canonical: index-ordered already).\n * - `undefined` values are dropped (standard JSON behaviour).\n * - `Date` is serialized via its `toJSON` method (ISO string). **Collision\n * caveat:** `{ d: new Date(0) }` and `{ d: \"1970-01-01T00:00:00.000Z\" }`\n * produce the **same** cache key (both reduce to the ISO string), so the two\n * inputs share one cached source. Harmless in practice — `Date` params are\n * exotic and `isActiveRoute` stringifies them identically post-encoding.\n * - `Symbol` becomes `undefined` (standard JSON behaviour).\n * - `BigInt` throws via `JSON.stringify` defaults.\n * - `Map`, `Set`, `RegExp`, `WeakMap`, `WeakSet` would silently collapse to\n * `\"{}\"` (no enumerable own keys), which would cause **different inputs to\n * share the same cache key**. We detect these explicitly and throw — callers\n * then take the non-cached fallback path (same behaviour as `BigInt`).\n * - Circular references throw `TypeError` (parity with native `JSON.stringify`).\n * The replacer copies each object level into a fresh prototype-less record,\n * so we must run our own cycle detection — the native detector never sees\n * the original object graph.\n * - `__proto__` keys are preserved as own properties (no prototype pollution\n * and no silent collision between `{ __proto__: x, b: 1 }` and `{ b: 1 }`).\n *\n * In practice, route params carry primitives (`string | number | boolean`);\n * the cache-key-collision edge cases above are defensive bugs caught loudly.\n */\nexport function canonicalJson(value: unknown): string {\n return JSON.stringify(canonicalize(value, new Set<object>()));\n}\n\n// Key comparison uses byte-order (`<` / `>`) instead of `localeCompare()` so\n// the output is independent of the Node.js ICU build / system locale. Same\n// canonical form on every machine — required for cache-key stability.\nfunction compareKeys(left: string, right: string): number {\n return left < right ? -1 : 1;\n}\n\n/**\n * Returns a structural clone with object keys sorted at every level. Path-based\n * cycle detection (`path` set) matches the semantics of native `JSON.stringify`\n * — a `TypeError` is thrown on a true cycle, but the same object reachable via\n * two independent branches (DAG) serialises fine.\n *\n * `Date` instances pass through unchanged so `JSON.stringify` can invoke their\n * `toJSON` hook. Other built-ins (`Map`, `Set`, `WeakMap`, `WeakSet`, `RegExp`)\n * throw eagerly to avoid silent cache-key collisions.\n */\nfunction canonicalize(value: unknown, path: Set<object>): unknown {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (value instanceof Date) {\n return value;\n }\n\n if (\n value instanceof Map ||\n value instanceof Set ||\n value instanceof WeakMap ||\n value instanceof WeakSet ||\n value instanceof RegExp\n ) {\n throw new TypeError(\n `canonicalJson: cannot serialize ${(value as { constructor: { name: string } }).constructor.name} — non-enumerable own keys collapse to \"{}\" and would cause cache-key collisions. Pass primitive params (string | number | boolean) instead.`,\n );\n }\n\n if (path.has(value)) {\n throw new TypeError(\n \"canonicalJson: cannot serialize circular structure (cycle detected during traversal).\",\n );\n }\n\n path.add(value);\n try {\n if (Array.isArray(value)) {\n return value.map((item) => canonicalize(item, path));\n }\n\n // Use a null-prototype record so `__proto__` is treated as a regular\n // own property — assigning to a plain `{}` would set the prototype\n // chain instead and silently collide with inputs that omit the key.\n const sorted: Record<string, unknown> = objectCreate(null) as Record<\n string,\n unknown\n >;\n const keys = objectKeys(value).toSorted(compareKeys);\n\n for (const key of keys) {\n sorted[key] = canonicalize((value as Record<string, unknown>)[key], path);\n }\n\n return sorted;\n } finally {\n path.delete(value);\n }\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 { noopDestroy } from \"./internal/noopDestroy.js\";\nimport { readContextHash } from \"./internal/readContextHash.js\";\nimport { normalizeActiveOptions } from \"./normalizeActiveOptions.js\";\n\nimport type { ActiveRouteSourceOptions, RouterSource } from \"./types.js\";\nimport type { Params, Router, SearchParams } 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 * For cached entries `destroy()` is a no-op — shared sources live with the\n * router and release automatically on router GC (WeakMap entry).\n *\n * A non-string route name, or `BigInt`/circular params → the source bypasses the cache\n * and `destroy()` becomes a real teardown that detaches the underlying\n * `router.subscribe` handle.\n */\nexport function createActiveRouteSource(\n router: Router,\n routeName: string,\n params?: Params,\n search?: SearchParams,\n options?: ActiveRouteSourceOptions,\n): RouterSource<boolean> {\n const { strict, ignoreQueryParams, hash } = normalizeActiveOptions(options);\n\n // Two paths produce a fresh (non-cached) source: a NON-STRING route name\n // (the guard below, #1881) and params/search `canonicalJson` cannot\n // serialize. Callers hit either rarely; the extra allocation is acceptable.\n let key: string | undefined;\n\n // ⚑ A NON-STRING name never gets a cache slot, and this is a correctness\n // guard rather than a serialization one. The key below is a template literal,\n // so it COERCES the name: `{ toString: () => \"fwd\" }` and the string `\"fwd\"`\n // produce the identical key and therefore share one cached source. Whichever\n // call arrives first decides the answer for both.\n //\n // ⚠ Core does NOT refuse a non-string name (`packages/core/ARCHITECTURE.md`,\n // \"Route-Name Type Gates\"), so the collision is invisible for a name that\n // resolves through `forwardTo` — router on `/users`, a bag naming `\"fwd\"`\n // answers `true`, same as the string. It is NOT invisible for a plain name:\n // `isActiveRoute` compares the active name by IDENTITY first, so a bag naming\n // `\"users\"` answers `false` while the router is on `/users`, and the\n // well-typed sibling that shares its cache slot inherits that `false` — a\n // correct `<Link to=\"users\">` goes dark. That half needs no drifting\n // `toString` and no gate anywhere to reproduce; bypassing the cache keeps the\n // bad call's answer to itself.\n if (typeof routeName !== \"string\") {\n return buildActiveRouteSource(\n router,\n routeName,\n params,\n search,\n strict,\n ignoreQueryParams,\n hash,\n );\n }\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 // `params === undefined` is the common Link case (`<Link to=\"users\">`\n // with no params). Skip canonicalJson(undefined) — it returns the literal\n // string \"undefined\" and template interpolation would just embed it. An\n // explicit empty sentinel avoids the call and shaves the cache-key by 9\n // characters per Link.\n const paramsKey = params === undefined ? \"\" : canonicalJson(params);\n\n // Query channel (RFC-4 M2, #1548) — keyed the same way as `params`:\n // `undefined` (the common no-search Link) collapses to the empty sentinel\n // to skip `canonicalJson(undefined)` and share one entry with a no-search\n // call. Distinct from `params` so `{ search: { q } }` and `{ params: { q } }`\n // never collide on the same cache slot.\n const searchKey = search === undefined ? \"\" : canonicalJson(search);\n\n // Delimiter `|` is safe because route names use `.` as the segment\n // separator (`users.list`, not `users|list`) and canonicalJson-encoded\n // params escape `\"` (so any literal `|` inside params lives inside a\n // quoted JSON string and can't be confused with our delimiter). If route\n // names ever grow a `|` character, this composite key would become\n // ambiguous — change the separator to a control char or hash-encode each\n // field.\n key = `${routeName}|${paramsKey}|${searchKey}|${String(strict)}|${String(ignoreQueryParams)}|${hashKey}`;\n } catch {\n key = undefined;\n }\n\n if (key === undefined) {\n // Non-cached fallback (canonicalJson threw on BigInt / circular / etc.).\n // Return the real source — `destroy()` must unwind the router subscription;\n // otherwise the wrapper leaks for the lifetime of the router.\n return buildActiveRouteSource(\n router,\n routeName,\n params,\n search,\n strict,\n ignoreQueryParams,\n hash,\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 search,\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 * 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 search: SearchParams | undefined,\n strict: boolean,\n ignoreQueryParams: boolean,\n hash: string | undefined,\n): boolean {\n const routeActive = router.isActiveRoute(\n routeName,\n params,\n // Query channel at position 3 (RFC-4 M2 / #1548). `isActiveRoute` only\n // consults it when `ignoreQueryParams` is false — the default active-route\n // match (ignoreQueryParams: true) still ignores it, so a `routeSearch`-less\n // Link behaves exactly as before.\n search,\n strict,\n ignoreQueryParams,\n );\n\n if (!routeActive) {\n return false;\n }\n if (hash === undefined) {\n return true;\n }\n\n // `readContextHash` returns `undefined` when no URL plugin claimed the\n // namespace (hash-plugin runtime, memory-plugin, SSR). For hash-equality\n // matching we collapse that to `\"\"` — a hash-aware Link with no URL plugin\n // can only match when the consumer also asked for `hash: \"\"`.\n return (readContextHash(router.getState()) ?? \"\") === hash;\n}\n\nfunction buildActiveRouteSource(\n router: Router,\n routeName: string,\n params: Params | undefined,\n search: SearchParams | 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 search,\n strict,\n ignoreQueryParams,\n hash,\n );\n\n let routerUnsubscribe: (() => void) | undefined;\n\n const disconnect = (): void => {\n const unsub = routerUnsubscribe;\n\n routerUnsubscribe = undefined;\n unsub?.();\n };\n\n const source = new BaseSource(initialValue, {\n onFirstSubscribe: () => {\n // Reconcile before connecting: while disconnected (zero listeners) the\n // source missed navigations, so the active boolean may be stale.\n // Recompute against the current router state and notify the just-added\n // listener — BaseSource registers it before onFirstSubscribe. Mirrors the\n // reconnect reconcile of createRouteNodeSource / createRouteSource (#765).\n const reconciled = computeActive(\n router,\n routeName,\n params,\n search,\n strict,\n ignoreQueryParams,\n hash,\n );\n\n if (!Object.is(source.getSnapshot(), reconciled)) {\n source.updateSnapshot(reconciled);\n }\n\n // Lazy connection (#766): subscribe on the FIRST listener, disconnect on\n // the LAST (onLastUnsubscribe). Previously this source subscribed eagerly\n // at construction with a no-op destroy on the cached wrapper, so every\n // unique cache key held a PERMANENT router subscription that survived all\n // Link unmounts — a long-lived router with per-item-params Links\n // accumulated handles until the EventEmitter listener limit (10000)\n // crashed the render path. The cache entry still lives with the router\n // (cheap: a closure), but now holds no subscription while it has zero\n // listeners; the non-cached paths (a non-string name, or unserializable params) likewise\n // detaches through `disconnect`.\n routerUnsubscribe = 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\n // `hashChanged` flag published by URL plugins.\n const hashFlip =\n hash !== undefined &&\n ((\n next.route.context as\n { 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 search,\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 onLastUnsubscribe: disconnect,\n onDestroy: disconnect,\n });\n\n return source;\n}\n","import { areRoutesRelated } from \"@real-router/route-utils\";\n\nimport { noopDestroy } from \"./internal/noopDestroy.js\";\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 — for diffing 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 // Empty string represents the root of the name hierarchy — every named\n // route is its descendant. Without this short-circuit, `current.name`\n // would have to equal `\"\"` or start with `\".\"` (both impossible for\n // valid route names), breaking symmetry with `createRouteNodeSource(\"\")`\n // which is always-active when a route is current.\n if (routeName === \"\") {\n return true;\n }\n\n return (\n current.name === routeName || current.name.startsWith(`${routeName}.`)\n );\n };\n\n // Diffs and notifies a single route name on a router transition. Extracted\n // from the subscribe callback so the fan-out loop stays a trivial\n // isolate-per-name shell (#1478) — the recompute below (`areRoutesRelated` /\n // `isActiveNonStrict`) runs OUTSIDE the per-listener `try`, so the caller\n // wraps this whole call in a per-name `try` too.\n const notifyName = (\n routeName: string,\n listeners: Set<() => void>,\n newRouteName: string,\n previousRouteName: string | undefined,\n ): void => {\n // Cheap pre-filter: if neither new nor previous route is related to this\n // name, its active state cannot have changed. Empty routeName is the\n // implicit root — every route is its descendant, so the filter would\n // falsely exclude it (`areRoutesRelated` doesn't treat `\"\"` specially).\n // Skip the filter for the root.\n if (routeName !== \"\") {\n const isNewRelated = areRoutesRelated(routeName, newRouteName);\n const isPrevRelated =\n previousRouteName !== undefined &&\n areRoutesRelated(routeName, previousRouteName);\n\n if (!isNewRelated && !isPrevRelated) {\n return;\n }\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 return;\n }\n\n activeByName.set(routeName, nextActive);\n // Per-listener exception isolation — mirrors `BaseSource.notify`\n // (INVARIANTS \"BaseSource 3\"). Without it, one throwing Link listener would\n // abort notifications to the remaining listeners of this name (#767).\n // `activeByName.set` already ran above, so the per-name diff is committed\n // before any listener fires. Re-throw asynchronously so the genuine bug\n // still surfaces to global error handlers.\n for (const listener of listeners) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\n }\n };\n\n const connect = (): void => {\n routerUnsubscribe = router.subscribe((next) => {\n const newRouteName = next.route.name;\n const previousRouteName = next.previousRoute?.name;\n\n for (const [routeName, listeners] of listenersByName) {\n // Per-name exception isolation (#1478): `notifyName`'s recompute runs\n // OUTSIDE its per-listener `try`. A throw from it for one name would\n // unwind this shared subscribe callback, skipping every later name's\n // diff/notify and leaving their `activeByName` stale — structurally the\n // #767 failure mode one level up. Safe by construction today (`getState`\n // is a frozen-field read; the rest is pure string ops with no user code\n // in the path), but a future param-aware / predicate recompute could\n // throw here; isolate it the same way, re-throwing asynchronously so a\n // genuine bug still surfaces.\n try {\n notifyName(routeName, listeners, newRouteName, previousRouteName);\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\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\n // Stale-generation guard: if this name's registered Set is no longer the\n // one this closure captured, its generation was already fully torn down\n // (all listeners removed → Set deleted) and possibly re-created by a later\n // subscribe. Deleting / disconnecting against the stale Set would delete\n // the LIVE generation's map entry (the empty stale Set trips\n // `size === 0`), orphaning its subscribers. Nothing left for us to clean\n // up here — bail (#1206).\n if (listenersByName.get(routeName) !== listeners) {\n return;\n }\n\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","import { createActiveNameSelector } from \"./createActiveNameSelector.js\";\nimport { createActiveRouteSource } from \"./createActiveRouteSource.js\";\n\nimport type { RouterSource } from \"./types.js\";\nimport type { Params, Router, SearchParams } from \"@real-router/core\";\n\nconst NOOP = (): void => {};\n\n/**\n * Framework-agnostic fast/slow builder for an adapter `<Link>`'s active-route\n * `RouterSource<boolean>`. Every adapter Link routes its active-state check\n * through this one function so the fast/slow decision (and the `routeName !== \"\"`\n * guard) lives in a single place — a per-adapter copy of the decision drifting\n * from the shared one is exactly what produced #1416 (the vue Link kept building\n * a per-link source while the fast path sat unused in a composable).\n *\n * **Fast path** — a default-options link (non-empty `routeName`, no custom\n * `params`, non-strict, query params ignored, no `hash`) shares the per-router\n * {@link createActiveNameSelector}: ONE `router.subscribe` handle serves any\n * number of distinct-`routeName` links, versus a per-link\n * {@link createActiveRouteSource} that allocates a `BaseSource` AND opens its own\n * router subscription for every link (the 10k-listener-cap crash documented on\n * `createActiveRouteSource`). The selector's `isActive` is exactly non-strict,\n * query-ignoring, name-only matching — identical to the default\n * `createActiveRouteSource`, including for a non-string name: both coerce it and\n * both answer what its `toString` named (core does not gate the name here — see\n * `packages/core/ARCHITECTURE.md` \"Route-Name Type Gates\"). `destroy()` is a\n * no-op (the selector is per-router cached and outlives the link); the returned\n * `subscribe`'s unsubscribe removes only this link's listener.\n *\n * **Slow path** — any deviation from the defaults (custom `params`, `strict`,\n * `ignoreQueryParams: false`, hash-aware #532) falls to the per-link\n * `createActiveRouteSource`, whose cache covers the full argument surface. An\n * **empty `routeName`** also takes the slow path: it is a misuse (builds no href,\n * logs a `console.error`), and the selector's root-active `isActive(\"\") === true`\n * would light up a misused link, whereas `router.isActiveRoute(\"\")` is `false` —\n * the slow path preserves that.\n */\nexport function createActiveSource(\n router: Router,\n routeName: string,\n params: Params | undefined,\n search: SearchParams | undefined,\n strict: boolean,\n ignoreQueryParams: boolean,\n hash: string | undefined,\n): RouterSource<boolean> {\n if (\n routeName !== \"\" &&\n params === undefined &&\n // A `routeSearch` link forces the slow path (RFC-4 M2, #1548): the\n // name-only selector is search-blind, so it can't answer a query-scoped\n // active check — exactly the reason a custom `hash` also falls through.\n search === undefined &&\n !strict &&\n ignoreQueryParams &&\n hash === undefined\n ) {\n const selector = createActiveNameSelector(router);\n\n return {\n subscribe: (listener) => selector.subscribe(routeName, listener),\n getSnapshot: () => selector.isActive(routeName),\n destroy: NOOP,\n };\n }\n\n // The `hash` argument (#532) participates in the cache key when defined.\n // exactOptionalPropertyTypes forbids `{ hash: undefined }` literally — include\n // the `hash` key only when a value is provided.\n return createActiveRouteSource(\n router,\n routeName,\n params,\n search,\n hash === undefined\n ? { strict, ignoreQueryParams }\n : { strict, ignoreQueryParams, hash },\n );\n}\n","import { events } from \"@real-router/core\";\nimport { getPluginApi } from \"@real-router/core/api\";\n\nimport { BaseSource } from \"./BaseSource\";\nimport { noopDestroy } from \"./internal/noopDestroy.js\";\nimport { stabilizeState } from \"./stabilizeState.js\";\n\nimport type { RouterTransitionSnapshot, RouterSource } from \"./types.js\";\nimport type { Router, State } from \"@real-router/core\";\n\n// Frozen so accidental consumer mutation (`source.getSnapshot().toRoute = X`)\n// throws in strict mode. The singleton ref is shared across every IDLE state\n// for the lifetime of the process — mutating it would corrupt the contract\n// \"all IDLE snapshots are the same object reference\" relied on by every\n// adapter's useSyncExternalStore equivalent.\nconst IDLE_SNAPSHOT: RouterTransitionSnapshot = Object.freeze({\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\n/**\n * @internal test-only export — returns the next snapshot for a TRANSITION_START\n * payload, or `null` when the same-paths dedup guard should suppress the\n * update. Exported so the (structurally-unreachable after #605) guard can be\n * exercised by unit tests without resorting to private-API hacks.\n */\nexport function nextTransitionStartSnapshot(\n prev: RouterTransitionSnapshot,\n toState: State,\n fromState: State | undefined,\n): RouterTransitionSnapshot | null {\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 return null;\n }\n\n return {\n isTransitioning: true,\n isLeaveApproved: false,\n toRoute: newToRoute,\n fromRoute: newFromRoute,\n };\n}\n\n/**\n * @internal test-only export — analogous to {@link nextTransitionStartSnapshot}\n * for the LEAVE_APPROVE payload. The guard is structurally unreachable in\n * practice (router emits LEAVE_APPROVE exactly once per pipeline) but stays\n * for plugin-driven re-entrant flows.\n */\nexport function nextLeaveApproveSnapshot(\n prev: RouterTransitionSnapshot,\n toState: State,\n fromState: State | undefined,\n): RouterTransitionSnapshot | null {\n const newToRoute = stabilizeState(prev.toRoute, toState);\n const newFromRoute = stabilizeState(prev.fromRoute, fromState ?? null);\n\n if (\n prev.isLeaveApproved &&\n newToRoute === prev.toRoute &&\n newFromRoute === prev.fromRoute\n ) {\n return null;\n }\n\n return {\n isTransitioning: true,\n isLeaveApproved: true,\n toRoute: newToRoute,\n fromRoute: newFromRoute,\n };\n}\n\nexport function createTransitionSource(\n router: Router,\n): RouterSource<RouterTransitionSnapshot> {\n // Declared before `source` so its onDestroy closure closes over an\n // initialized binding — a mid-registration throw below no longer leaves it\n // in the TDZ, keeping the source destroyable (#1440).\n const unsubs: (() => void)[] = [];\n\n const source = new BaseSource(IDLE_SNAPSHOT, {\n onDestroy: () => {\n for (const unsub of unsubs) {\n unsub();\n }\n },\n });\n\n const api = getPluginApi(router);\n\n const resetToIdle = (): void => {\n source.updateSnapshot(IDLE_SNAPSHOT);\n };\n\n const onTransitionStart = (toState: State, fromState?: State): void => {\n // The same-paths dedup branch inside nextTransitionStartSnapshot is\n // structurally unreachable after #605 (every router-emitted\n // TRANSITION_START carries a fresh State per navigate()), but the\n // helper is kept testable for future stabilizer changes — see the\n // direct unit test in createTransitionSource.test.ts.\n const next = nextTransitionStartSnapshot(\n source.getSnapshot(),\n toState,\n fromState,\n );\n\n /* v8 ignore next 3 -- @preserve: dedup-skip branch unreachable through\n normal router flow; covered directly via nextTransitionStartSnapshot\n unit test. */\n if (next === null) {\n return;\n }\n\n source.updateSnapshot(next);\n };\n\n const onLeaveApprove = (toState: State, fromState?: State): void => {\n const next = nextLeaveApproveSnapshot(\n source.getSnapshot(),\n toState,\n fromState,\n );\n\n /* v8 ignore next 3 -- @preserve: dedup-skip branch unreachable through\n normal router flow (LEAVE_APPROVE fires once per pipeline); covered\n directly via nextLeaveApproveSnapshot unit test. */\n if (next === null) {\n return;\n }\n\n source.updateSnapshot(next);\n };\n\n // Eager connection: subscribe to router events immediately. Register\n // one-by-one so a throw mid-registration (the emitter rejects a duplicate\n // listener / hits its maxListeners cap) unwinds the already-registered\n // listeners instead of leaking them and stranding the half-wired source\n // (#1440). Mirrors @real-router/rx's events$ partial-registration safety.\n /* eslint-disable unicorn/prefer-single-call -- register one-by-one so a\n mid-registration throw unwinds the already-pushed unsubs; a single\n push(a, b) evaluates every addEventListener arg BEFORE pushing, leaving an\n earlier registration untracked and leaked (#1440). */\n try {\n unsubs.push(\n api.addEventListener(events.TRANSITION_START, onTransitionStart),\n );\n unsubs.push(\n api.addEventListener(events.TRANSITION_LEAVE_APPROVE, onLeaveApprove),\n );\n unsubs.push(api.addEventListener(events.TRANSITION_SUCCESS, resetToIdle));\n unsubs.push(api.addEventListener(events.TRANSITION_ERROR, resetToIdle));\n unsubs.push(api.addEventListener(events.TRANSITION_CANCEL, resetToIdle));\n } catch (error) {\n for (const unsub of unsubs) {\n unsub();\n }\n\n throw error;\n }\n /* eslint-enable unicorn/prefer-single-call */\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","import { events } from \"@real-router/core\";\nimport { getPluginApi } from \"@real-router/core/api\";\n\nimport { BaseSource } from \"./BaseSource\";\nimport { noopDestroy } from \"./internal/noopDestroy.js\";\n\nimport type { RouterErrorSnapshot, RouterSource } from \"./types.js\";\nimport type { Router, State, RouterError } from \"@real-router/core\";\n\n// Frozen shared singleton — returned by every error source's getSnapshot()\n// until the first error. Mirrors createTransitionSource's IDLE_SNAPSHOT: a\n// consumer mutating it would corrupt the shared singleton for every error\n// source of every router (#768).\nconst INITIAL_SNAPSHOT: RouterErrorSnapshot = Object.freeze({\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 let hasError = false;\n\n // Declared before `source` so its onDestroy closure closes over an\n // initialized binding — a mid-registration throw below no longer leaves it\n // in the TDZ, keeping the source destroyable (#1440).\n const unsubs: (() => void)[] = [];\n\n const source = new BaseSource(INITIAL_SNAPSHOT, {\n onDestroy: () => {\n for (const unsub of unsubs) {\n unsub();\n }\n },\n });\n\n const api = getPluginApi(router);\n\n // Eager connection: subscribe to router events immediately. Register\n // one-by-one so a throw mid-registration (the emitter rejects a duplicate\n // listener / hits its maxListeners cap) unwinds the already-registered\n // listeners instead of leaking them and stranding the half-wired source\n // (#1440). Mirrors @real-router/rx's events$ partial-registration safety.\n /* eslint-disable unicorn/prefer-single-call -- register one-by-one so a\n mid-registration throw unwinds the already-pushed unsubs; a single\n push(a, b) evaluates every addEventListener arg BEFORE pushing, leaving an\n earlier registration untracked and leaked (#1440). */\n try {\n unsubs.push(\n api.addEventListener(\n events.TRANSITION_ERROR,\n (\n toState: State | undefined,\n fromState: State | undefined,\n err: RouterError,\n ) => {\n errorVersion++;\n hasError = true;\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 );\n unsubs.push(\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 (hasError) {\n hasError = false;\n source.updateSnapshot({\n error: null,\n toRoute: null,\n fromRoute: null,\n version: errorVersion,\n });\n }\n }),\n );\n } catch (error) {\n for (const unsub of unsubs) {\n unsub();\n }\n\n throw error;\n }\n /* eslint-enable unicorn/prefer-single-call */\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\n/**\n * Eagerly create (and thus subscribe) the per-router error source IF the router\n * supports the plugin API — used by framework adapters' `RouterProvider` to\n * capture a navigation error that fires BEFORE a `RouterErrorBoundary` mounts\n * (#778). A router-like without an internals-registry entry (a test stub, an\n * `Object.create`-derived object, or anything not produced by `createRouter`)\n * makes `getErrorSource` throw via `getPluginApi`; here that degrades to a no-op.\n *\n * The asymmetry is deliberate: `createRouteSource` tolerates such routers (it is\n * lazy and only touches `router.subscribe`), so a Provider that eagerly primes\n * the stricter error source must not crash where the route source would not. A\n * boundary mounting later still creates the error source lazily and surfaces a\n * genuine invalid-router error there.\n */\nexport function primeErrorSource(router: Router): void {\n try {\n getErrorSource(router);\n } catch {\n // Router has no internals-registry entry — nothing to prime eagerly.\n }\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { getErrorSource } from \"./createErrorSource\";\nimport { noopDestroy } from \"./internal/noopDestroy.js\";\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 // Hoisted up here so the `onFirstSubscribe` closure below can read/write\n // it before `disconnect()`'s declaration. JS hoisting makes the original\n // post-declaration order legal, but reading top-to-bottom is clearer.\n let unsubFromError: (() => void) | null = null;\n\n const buildDismissableSnapshot = (): 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>(\n buildDismissableSnapshot(),\n {\n onFirstSubscribe: () => {\n unsubFromError = errorSource.subscribe(() => {\n source.updateSnapshot(buildDismissableSnapshot());\n });\n\n // Catch up: the eager underlying error source may already hold (or have\n // since cleared) an error that changed while this wrapper had zero\n // subscribers — at first-ever subscribe, or after a disconnect/reconnect\n // cycle (onLastUnsubscribe below). Without this, a RouterErrorBoundary\n // mounting AFTER a boot-time error would never see it (#765.2). Guard on\n // the resolved `error` so the common no-error case stays a no-op: the\n // listener was added before this callback (BaseSource ordering), so an\n // unconditional updateSnapshot would notify it redundantly and break the\n // \"subscribers fire only on state-relevant actions\" contract. The\n // `version` field can skip values for an error cycle fully missed during\n // a disconnect — harmless, monotonicity still holds.\n const caughtUp = buildDismissableSnapshot();\n\n if (caughtUp.error !== source.getSnapshot().error) {\n source.updateSnapshot(caughtUp);\n }\n },\n onLastUnsubscribe: () => {\n disconnect();\n },\n },\n );\n\n function resetError(): void {\n const currentVersion = errorSource.getSnapshot().version;\n\n // No-op guard: if we already dismissed at this version (or are even ahead\n // of the live error stream), there's nothing to clear. Skipping prevents\n // a redundant snapshot allocation + listener notification under tight\n // resetError(); resetError() patterns — common when a RouterErrorBoundary\n // user clicks \"dismiss\" while another dismiss is already in flight.\n if (currentVersion <= dismissedVersion) {\n return;\n }\n\n dismissedVersion = currentVersion;\n source.updateSnapshot(buildDismissableSnapshot());\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","import type { State } from \"@real-router/core\";\n\n/**\n * Context handed to a route-enter handler once a navigation-driven mount is\n * confirmed. `previousRoute` is **non-nullable** — the gate is the sole bridge\n * from the nullable route snapshot (`{ route?, previousRoute? }`) to this\n * non-nullable contract (#1218).\n */\nexport interface RouteEnterContext {\n /** The route that was just activated. */\n route: State;\n /** The route that was active immediately before this navigation. */\n previousRoute: State;\n}\n\n/**\n * Decision closure returned by {@link createRouteEnterGate}.\n *\n * Given the already-unwrapped route snapshot (adapters own the reactive\n * unwrap) and the per-call `skipSameRoute` flag, returns the\n * {@link RouteEnterContext} to dispatch — or `null` to skip. Stateful: it\n * holds `lastHandledRoute` for the dedupe arm.\n */\nexport type RouteEnterGate = (\n route: State | undefined,\n previousRoute: State | undefined,\n skipSameRoute: boolean,\n) => RouteEnterContext | null;\n\n/**\n * Builds the framework-agnostic route-enter guard gate shared by every\n * adapter's `useRouteEnter` / `injectRouteEnter` (#1435). Each adapter owns\n * only the reactive effect wiring + snapshot unwrap + dispatch; this closure\n * owns the canonical guard superset and the `lastHandledRoute` dedupe state.\n *\n * Guard order (behavior-neutral superset of all six adapters):\n *\n * 1. `!route` — no committed route yet (svelte SSR / pre-start; a no-op for\n * the other adapters, whose `route` is defined by the time they call in).\n * 2. skip-initial — `!route.transition.from`: the first commit from\n * `router.start()` is not an \"entry\".\n * 3. same-route skip — `route.transition.from === route.name` (query-only /\n * sort-filter navigations), gated by the per-call `skipSameRoute`.\n * 4. dedupe — same snapshot reference dispatches at most once (guards\n * React StrictMode's dev double-invoke; a defensive no-op elsewhere).\n * 5. `!previousRoute` — the sole guard for the non-nullable\n * `RouteEnterContext.previousRoute` contract (#1218: reachable via a\n * Provider mounted after a navigation / an `<Activity>` catch-up, where\n * the source snapshot carries `previousRoute: undefined`).\n *\n * **`skipSameRoute` is a per-call argument, not a factory option** — React\n * holds the returned closure in a `useRef` so its `lastHandledRoute` survives\n * StrictMode effect re-runs; a factory option would force a fresh gate (and\n * reset the dedupe state) on an options flip. Threading it per-call lets the\n * same gate honor the flip without losing state.\n *\n * The gate **returns** the context and does not dispatch — dispatch stays\n * adapter-side (React/Preact via a latest-handler ref, others via a\n * capture-at-init handler). Framework pre-gates (e.g. Vue's `<KeepAlive>`\n * `isDeactivated`, #1221) compose *before* this gate and stay adapter-side.\n */\nexport function createRouteEnterGate(): RouteEnterGate {\n let lastHandledRoute: State | null = null;\n\n return (route, previousRoute, skipSameRoute) => {\n if (!route) {\n return null;\n }\n\n // `?.` on a field the type declares required, because core's commit door\n // commits a foreign State's ABSENT `transition` rather than fabricating one\n // (#1792 / #1976) — measured, the flat read threw here. Absent means \"no\n // origin known\", the answer this gate already gives a first navigation.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- the required field is genuinely absent on a foreign committed State\n if (!route.transition?.from) {\n return null;\n }\n\n if (skipSameRoute && route.transition.from === route.name) {\n return null;\n }\n\n if (lastHandledRoute === route) {\n return null;\n }\n\n if (!previousRoute) {\n return null;\n }\n\n lastHandledRoute = route;\n\n return { route, previousRoute };\n };\n}\n","import type { Router, State } from \"@real-router/core\";\n\n// `LeaveState` / `LeaveFn` live in `@real-router/core/types` but are NOT re-exported\n// from `@real-router/core` (which re-exports `State`), and `sources` has no\n// `@real-router/core/types` dependency (house rule). Derive the exact leave-listener\n// type from the facade instead — tsc-clean with zero new deps (#1435 D-C).\ntype LeaveFn = Parameters<Router[\"subscribeLeave\"]>[0];\n\n/**\n * Context handed to a route-exit handler when a genuine departure is approved.\n * Re-states core's `LeaveState` shape under the adapter-canonical name so the\n * six adapters can re-export it verbatim (#1435).\n */\nexport interface RouteExitContext {\n /** The route being left (`fromState`). */\n route: State;\n /** The navigation target (`toState`). */\n nextRoute: State;\n /** Aborts if the navigation is cancelled or fails before commit. */\n signal: AbortSignal;\n}\n\nexport interface UseRouteExitOptions {\n /**\n * Skip the handler when `route.name === nextRoute.name` (sort/filter/\n * query-only navigations on the same route). Default: `true`.\n */\n skipSameRoute?: boolean;\n}\n\ntype LeaveHandler = (context: RouteExitContext) => void | Promise<void>;\n\n/**\n * Wraps a route-exit `handler` into the canonical `subscribeLeave` listener\n * shared by every adapter's `useRouteExit` / `injectRouteExit` (#1435). The\n * adapter owns only registration + cleanup (and, for React/Preact, the\n * latest-handler ref via a thunk); this HOF owns the guard sequence:\n *\n * 1. same-route skip — `route.name === nextRoute.name` (live `State` names,\n * pre-commit), gated by `skipSameRoute` (default `true`).\n * 2. reentrant-abort pre-check — a `signal` already aborted when the listener\n * fires means the navigation is gone; `addEventListener(\"abort\", …)` does\n * not fire retroactively, so skip the handler entirely.\n * 3. passthrough — `return handler(ctx)` so the returned Promise reaches\n * core's `settleLeavePromises` and **blocks** the activation phase.\n *\n * **Error contract:** a rejected handler Promise rejects `navigate()` with the\n * handler's **original error** and emits `TRANSITION_ERROR` — it is NOT\n * re-coded to `TRANSITION_CANCELLED`. `TRANSITION_CANCELLED` arises only when\n * the payload `signal` aborts (supersede / `stop()` / `dispose()` / external\n * `opts.signal`). A fire-and-forget call here would silently un-block the\n * transition, so arm 3 must stay a passthrough.\n */\nexport function guardLeaveListener(\n handler: LeaveHandler,\n options?: UseRouteExitOptions,\n): LeaveFn {\n const skipSameRoute = options?.skipSameRoute ?? true;\n\n return ({ route, nextRoute, signal }) => {\n if (skipSameRoute && route.name === nextRoute.name) {\n return;\n }\n\n if (signal.aborted) {\n return;\n }\n\n return handler({ route, nextRoute, signal });\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["#listeners","#onFirstSubscribe","#onLastUnsubscribe","#onDestroy","#currentSnapshot","#destroyed"],"sources":["../../src/BaseSource.ts","../../src/internal/readContextHash.ts","../../src/stabilizeState.ts","../../src/createRouteSource.ts","../../src/computeSnapshot.ts","../../src/internal/noopDestroy.ts","../../src/createRouteNodeSource.ts","../../src/canonicalJson.ts","../../src/normalizeActiveOptions.ts","../../src/createActiveRouteSource.ts","../../src/createActiveNameSelector.ts","../../src/createActiveSource.ts","../../src/createTransitionSource.ts","../../src/createErrorSource.ts","../../src/createDismissableError.ts","../../src/createRouteEnterGate.ts","../../src/guardLeaveListener.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 // Isolate listener exceptions so a single throwing subscriber (e.g. React\n // error-boundary fallback throwing inside `onStoreChange`) does not block\n // the remaining subscribers — the invariant \"after updateSnapshot all\n // listeners see the new snapshot\" must hold. Re-throw asynchronously via\n // queueMicrotask so global error handlers / test harnesses still surface\n // the bug without breaking the synchronous notification fan-out.\n for (const listener of this.#listeners) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\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 * @internal\n *\n * Reads the URL fragment published by `browser-plugin` / `navigation-plugin`\n * on a router state. The plugins claim the `\"url\"` namespace via\n * `state.context.url` and the `hash` field carries the **decoded** fragment.\n *\n * Returns:\n * - `undefined` — no plugin claimed the `\"url\"` namespace (hash-plugin runtime,\n * memory-plugin, SSR before hydration) OR the state itself is nullish;\n * - `\"\"` — the URL namespace exists but the fragment is empty\n * (browser-plugin on a hash-less URL).\n *\n * Callers that need the \"no namespace at all\" branch (e.g. `stabilizeState`\n * comparing cross-plugin transitions) read the raw `undefined`. Callers that\n * collapse \"no namespace\" to \"no hash\" (e.g. `createActiveRouteSource`'s\n * hash-equality check) coalesce with `?? \"\"` themselves.\n *\n * Centralising the context cast removes the previous duplicate definitions in\n * `stabilizeState.ts` and `createActiveRouteSource.ts` that drifted in\n * signature (state vs router) and default-value (undefined vs \"\") — both\n * variants are reconstructible from this single helper at the callsite.\n */\nexport function readContextHash(\n state: State | null | undefined,\n): string | undefined {\n const ctx = state?.context as { url?: { hash?: string } } | undefined;\n\n return ctx?.url?.hash;\n}\n","import { readContextHash } from \"./internal/readContextHash.js\";\n\nimport type { State } from \"@real-router/core\";\n\n/**\n * State-aware stabilization for route snapshots.\n *\n * Compares `path` (canonical name+params), `state.context.url.hash`\n * (URL fragment, #532), and `state.transition.reload` (#605). When all\n * three match (idempotent navigation), returns `prev` (preserving\n * reference) so frameworks can skip re-renders. When any of them flips,\n * returns `next` so consumers subscribing through `useRoute()` see the\n * new state.\n *\n * `transition.reload === true` is the user's explicit signal for a\n * non-idempotent navigation — `router.navigate(name, params, search, {\n * reload: true })` is the canonical pairing for `invalidate(router, namespace)`\n * and any cache-bust pattern. Bypassing stabilization for reloads makes\n * `useRoute()` consumers see fresh `state.context.<namespace>` values\n * written by the SSR loader plugin's `subscribeLeave` handler.\n *\n * Ignores `meta` (internal: auto-increment id), other `transition` fields\n * (`from`, `segments`, `redirected`), and `state.context.navigation` /\n * `state.context.browser` (transient transition metadata) — they don't\n * affect render identity for idempotent navigations.\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 // Explicit reload navigation (#605) — caller asked to bypass dedupe so\n // observers see fresh `state.context` written by `invalidate()`-driven\n // loader re-runs. The path equality above guarantees both prev and next\n // are either non-null with matching paths or both nullish; only the\n // non-null branch can carry a meaningful `transition.reload`.\n if (readReloadFlag(next)) {\n return next;\n }\n\n return prev;\n}\n\nfunction readReloadFlag(state: State | null | undefined): boolean {\n // Defensive read: `transition` is mandatory in the public State type, but a\n // plugin returning a malformed state (or a future fork) shouldn't crash the\n // stabilizer with a TypeError. We cast to a structurally-loose shape so the\n // optional chain is permitted; the runtime guard preserves dedup (false =\n // not-a-reload) for malformed inputs.\n const transition = (\n state as { transition?: { reload?: boolean } } | null | undefined\n )?.transition;\n\n return transition?.reload === true;\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 // Reconcile the route with the current router state before connecting.\n // While this source had zero listeners it was disconnected\n // (onLastUnsubscribe below), so a navigation in that window was missed —\n // the just-added listener would otherwise observe a stale route (#765).\n // BaseSource registered the listener before this callback, so the\n // updateSnapshot below reaches it.\n //\n // Emit ONLY when the route actually changed: a hide/show (Activity)\n // cycle with no navigation in between must not fire a spurious\n // re-render, and its still-valid `previousRoute` must survive. When a\n // navigation WAS caught up, `previousRoute` resets to `undefined`: the\n // catch-up is a snap to the current state, not an observed navigation,\n // and the true previous route cannot be reconstructed outside a live\n // subscribe payload (parity of intent with createRouteNodeSource, whose\n // reconnect reconcile likewise drops previousRoute on catch-up).\n const current = source.getSnapshot();\n const reconciledRoute = stabilizeState(\n current.route,\n router.getState(),\n );\n\n if (reconciledRoute !== current.route) {\n source.updateSnapshot({\n route: reconciledRoute,\n previousRoute: undefined,\n });\n }\n\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","/**\n * @internal\n *\n * Shared no-op `destroy()` for cached `RouterSource` wrappers.\n *\n * Cached factories (`getTransitionSource`, `getErrorSource`, `createDismissableError`,\n * `createActiveNameSelector`, cache hit in `createRouteNodeSource`,\n * `createActiveRouteSource`) return a wrapper whose `destroy()` is a no-op —\n * the underlying source is shared across all consumers and lives as long as\n * the router (WeakMap entry releases on router GC).\n *\n * One module-level function shared by every cached factory keeps the wrapper\n * shape (`{ subscribe, getSnapshot, destroy: noopDestroy }`) byte-stable and\n * eliminates the previous six copies. (Bundlers inline a stand-alone arrow\n * just as readily, so the cost is purely on the maintenance side.)\n */\nexport function noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { computeSnapshot } from \"./computeSnapshot.js\";\nimport { noopDestroy } from \"./internal/noopDestroy.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 // computeSnapshot returns the SAME currentSnapshot reference when\n // both route and previousRoute stabilize to prev — guard against\n // emitting redundant updates to listeners (matters for signal-\n // based adapters that re-run effects on every set).\n /* v8 ignore next 3 -- @preserve: structurally unreachable after #605\n — reload navs always return fresh refs via stabilizeState, and\n within-node non-reload navs short-circuit at shouldUpdate. Guard\n kept for defensive correctness against future stabilizer changes. */\n if (Object.is(source.getSnapshot(), newSnapshot)) {\n return;\n }\n\n source.updateSnapshot(newSnapshot);\n });\n },\n onLastUnsubscribe: disconnect,\n },\n );\n\n return source;\n}\n","/**\n * Intrinsics captured at module load (#1971).\n *\n * ⚑ These DECIDE — each answers \"what is on this object\" for a value this module\n * did not build, so read off the live global they are the weakest point of every\n * check built on them. `guards.ts` states the doctrine and its measurement: one\n * naive `Object.hasOwn` polyfill walked straight through five sibling readers\n * while the single captured guard held.\n *\n * ⚠ Capture narrows the window from \"any time after boot\" to \"before this module\n * loads\". It does not close it — a shim evaluated ahead of core still wins\n * (#1798), which is the doctrine's own caveat and travels with it.\n */\nconst objectKeys = Object.keys;\n\n/** Captured like the deciding seven, but this one BUILDS the guarantee (#2072). */\nconst objectCreate = Object.create;\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 * **Divergence from `shared/dom-utils/scroll-restore.canonicalJson` — by\n * design.** That sibling implementation is the cheap navigation-hot-path\n * variant (uses `localeCompare`, plain-object accumulator, native cycle\n * detector) and pairs with `safeKeyOf` for crash-tolerance. This one is the\n * strict cache-key variant: byte-order compare, prototype-less accumulator,\n * bespoke cycle detection — eagerly throws on inputs that would cause\n * collisions or pollution, so callers can fall back to non-cached sources.\n * They are NOT interchangeable; cross-package equivalence is explicitly not\n * a goal (audit-2 / audit-2026-05-17 §2).\n *\n * Edge cases:\n * - Arrays preserve order (canonical: index-ordered already).\n * - `undefined` values are dropped (standard JSON behaviour).\n * - `Date` is serialized via its `toJSON` method (ISO string). **Collision\n * caveat:** `{ d: new Date(0) }` and `{ d: \"1970-01-01T00:00:00.000Z\" }`\n * produce the **same** cache key (both reduce to the ISO string), so the two\n * inputs share one cached source. Harmless in practice — `Date` params are\n * exotic and `isActiveRoute` stringifies them identically post-encoding.\n * - `Symbol` becomes `undefined` (standard JSON behaviour).\n * - `BigInt` throws via `JSON.stringify` defaults.\n * - `Map`, `Set`, `RegExp`, `WeakMap`, `WeakSet` would silently collapse to\n * `\"{}\"` (no enumerable own keys), which would cause **different inputs to\n * share the same cache key**. We detect these explicitly and throw — callers\n * then take the non-cached fallback path (same behaviour as `BigInt`).\n * - Circular references throw `TypeError` (parity with native `JSON.stringify`).\n * The replacer copies each object level into a fresh prototype-less record,\n * so we must run our own cycle detection — the native detector never sees\n * the original object graph.\n * - `__proto__` keys are preserved as own properties (no prototype pollution\n * and no silent collision between `{ __proto__: x, b: 1 }` and `{ b: 1 }`).\n *\n * In practice, route params carry primitives (`string | number | boolean`);\n * the cache-key-collision edge cases above are defensive bugs caught loudly.\n */\nexport function canonicalJson(value: unknown): string {\n return JSON.stringify(canonicalize(value, new Set<object>()));\n}\n\n// Key comparison uses byte-order (`<` / `>`) instead of `localeCompare()` so\n// the output is independent of the Node.js ICU build / system locale. Same\n// canonical form on every machine — required for cache-key stability.\nfunction compareKeys(left: string, right: string): number {\n return left < right ? -1 : 1;\n}\n\n/**\n * Returns a structural clone with object keys sorted at every level. Path-based\n * cycle detection (`path` set) matches the semantics of native `JSON.stringify`\n * — a `TypeError` is thrown on a true cycle, but the same object reachable via\n * two independent branches (DAG) serialises fine.\n *\n * `Date` instances pass through unchanged so `JSON.stringify` can invoke their\n * `toJSON` hook. Other built-ins (`Map`, `Set`, `WeakMap`, `WeakSet`, `RegExp`)\n * throw eagerly to avoid silent cache-key collisions.\n */\nfunction canonicalize(value: unknown, path: Set<object>): unknown {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (value instanceof Date) {\n return value;\n }\n\n if (\n value instanceof Map ||\n value instanceof Set ||\n value instanceof WeakMap ||\n value instanceof WeakSet ||\n value instanceof RegExp\n ) {\n throw new TypeError(\n `canonicalJson: cannot serialize ${(value as { constructor: { name: string } }).constructor.name} — non-enumerable own keys collapse to \"{}\" and would cause cache-key collisions. Pass primitive params (string | number | boolean) instead.`,\n );\n }\n\n if (path.has(value)) {\n throw new TypeError(\n \"canonicalJson: cannot serialize circular structure (cycle detected during traversal).\",\n );\n }\n\n path.add(value);\n try {\n if (Array.isArray(value)) {\n return value.map((item) => canonicalize(item, path));\n }\n\n // Use a null-prototype record so `__proto__` is treated as a regular\n // own property — assigning to a plain `{}` would set the prototype\n // chain instead and silently collide with inputs that omit the key.\n const sorted: Record<string, unknown> = objectCreate(null) as Record<\n string,\n unknown\n >;\n const keys = objectKeys(value).toSorted(compareKeys);\n\n for (const key of keys) {\n sorted[key] = canonicalize((value as Record<string, unknown>)[key], path);\n }\n\n return sorted;\n } finally {\n path.delete(value);\n }\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 { noopDestroy } from \"./internal/noopDestroy.js\";\nimport { readContextHash } from \"./internal/readContextHash.js\";\nimport { normalizeActiveOptions } from \"./normalizeActiveOptions.js\";\n\nimport type { ActiveRouteSourceOptions, RouterSource } from \"./types.js\";\nimport type { Params, Router, SearchParams } 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 * For cached entries `destroy()` is a no-op — shared sources live with the\n * router and release automatically on router GC (WeakMap entry).\n *\n * A non-string route name, or `BigInt`/circular params → the source bypasses the cache\n * and `destroy()` becomes a real teardown that detaches the underlying\n * `router.subscribe` handle.\n */\nexport function createActiveRouteSource(\n router: Router,\n routeName: string,\n params?: Params,\n search?: SearchParams,\n options?: ActiveRouteSourceOptions,\n): RouterSource<boolean> {\n const { strict, ignoreQueryParams, hash } = normalizeActiveOptions(options);\n\n // Two paths produce a fresh (non-cached) source: a NON-STRING route name\n // (the guard below, #1881) and params/search `canonicalJson` cannot\n // serialize. Callers hit either rarely; the extra allocation is acceptable.\n let key: string | undefined;\n\n // ⚑ A NON-STRING name never gets a cache slot, and this is a correctness\n // guard rather than a serialization one. The key below is a template literal,\n // so it COERCES the name: `{ toString: () => \"fwd\" }` and the string `\"fwd\"`\n // produce the identical key and therefore share one cached source. Whichever\n // call arrives first decides the answer for both.\n //\n // ⚠ Core does NOT refuse a non-string name (`packages/core/ARCHITECTURE.md`,\n // \"Route-Name Type Gates\"), so the collision is invisible for a name that\n // resolves through `forwardTo` — router on `/users`, a bag naming `\"fwd\"`\n // answers `true`, same as the string. It is NOT invisible for a plain name:\n // `isActiveRoute` compares the active name by IDENTITY first, so a bag naming\n // `\"users\"` answers `false` while the router is on `/users`, and the\n // well-typed sibling that shares its cache slot inherits that `false` — a\n // correct `<Link to=\"users\">` goes dark. That half needs no drifting\n // `toString` and no gate anywhere to reproduce; bypassing the cache keeps the\n // bad call's answer to itself.\n if (typeof routeName !== \"string\") {\n return buildActiveRouteSource(\n router,\n routeName,\n params,\n search,\n strict,\n ignoreQueryParams,\n hash,\n );\n }\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 // `params === undefined` is the common Link case (`<Link to=\"users\">`\n // with no params). Skip canonicalJson(undefined) — it returns the literal\n // string \"undefined\" and template interpolation would just embed it. An\n // explicit empty sentinel avoids the call and shaves the cache-key by 9\n // characters per Link.\n const paramsKey = params === undefined ? \"\" : canonicalJson(params);\n\n // Query channel (RFC-4 M2, #1548) — keyed the same way as `params`:\n // `undefined` (the common no-search Link) collapses to the empty sentinel\n // to skip `canonicalJson(undefined)` and share one entry with a no-search\n // call. Distinct from `params` so `{ search: { q } }` and `{ params: { q } }`\n // never collide on the same cache slot.\n const searchKey = search === undefined ? \"\" : canonicalJson(search);\n\n // Delimiter `|` is safe because route names use `.` as the segment\n // separator (`users.list`, not `users|list`) and canonicalJson-encoded\n // params escape `\"` (so any literal `|` inside params lives inside a\n // quoted JSON string and can't be confused with our delimiter). If route\n // names ever grow a `|` character, this composite key would become\n // ambiguous — change the separator to a control char or hash-encode each\n // field.\n key = `${routeName}|${paramsKey}|${searchKey}|${String(strict)}|${String(ignoreQueryParams)}|${hashKey}`;\n } catch {\n key = undefined;\n }\n\n if (key === undefined) {\n // Non-cached fallback (canonicalJson threw on BigInt / circular / etc.).\n // Return the real source — `destroy()` must unwind the router subscription;\n // otherwise the wrapper leaks for the lifetime of the router.\n return buildActiveRouteSource(\n router,\n routeName,\n params,\n search,\n strict,\n ignoreQueryParams,\n hash,\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 search,\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 * 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 search: SearchParams | undefined,\n strict: boolean,\n ignoreQueryParams: boolean,\n hash: string | undefined,\n): boolean {\n const routeActive = router.isActiveRoute(\n routeName,\n params,\n // Query channel at position 3 (RFC-4 M2 / #1548). `isActiveRoute` only\n // consults it when `ignoreQueryParams` is false — the default active-route\n // match (ignoreQueryParams: true) still ignores it, so a `routeSearch`-less\n // Link behaves exactly as before.\n search,\n strict,\n ignoreQueryParams,\n );\n\n if (!routeActive) {\n return false;\n }\n if (hash === undefined) {\n return true;\n }\n\n // `readContextHash` returns `undefined` when no URL plugin claimed the\n // namespace (hash-plugin runtime, memory-plugin, SSR). For hash-equality\n // matching we collapse that to `\"\"` — a hash-aware Link with no URL plugin\n // can only match when the consumer also asked for `hash: \"\"`.\n return (readContextHash(router.getState()) ?? \"\") === hash;\n}\n\nfunction buildActiveRouteSource(\n router: Router,\n routeName: string,\n params: Params | undefined,\n search: SearchParams | 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 search,\n strict,\n ignoreQueryParams,\n hash,\n );\n\n let routerUnsubscribe: (() => void) | undefined;\n\n const disconnect = (): void => {\n const unsub = routerUnsubscribe;\n\n routerUnsubscribe = undefined;\n unsub?.();\n };\n\n const source = new BaseSource(initialValue, {\n onFirstSubscribe: () => {\n // Reconcile before connecting: while disconnected (zero listeners) the\n // source missed navigations, so the active boolean may be stale.\n // Recompute against the current router state and notify the just-added\n // listener — BaseSource registers it before onFirstSubscribe. Mirrors the\n // reconnect reconcile of createRouteNodeSource / createRouteSource (#765).\n const reconciled = computeActive(\n router,\n routeName,\n params,\n search,\n strict,\n ignoreQueryParams,\n hash,\n );\n\n if (!Object.is(source.getSnapshot(), reconciled)) {\n source.updateSnapshot(reconciled);\n }\n\n // Lazy connection (#766): subscribe on the FIRST listener, disconnect on\n // the LAST (onLastUnsubscribe). Previously this source subscribed eagerly\n // at construction with a no-op destroy on the cached wrapper, so every\n // unique cache key held a PERMANENT router subscription that survived all\n // Link unmounts — a long-lived router with per-item-params Links\n // accumulated handles until the EventEmitter listener limit (10000)\n // crashed the render path. The cache entry still lives with the router\n // (cheap: a closure), but now holds no subscription while it has zero\n // listeners; the non-cached paths (a non-string name, or unserializable params) likewise\n // detaches through `disconnect`.\n routerUnsubscribe = 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\n // `hashChanged` flag published by URL plugins.\n const hashFlip =\n hash !== undefined &&\n ((\n next.route.context as\n { 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 search,\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 onLastUnsubscribe: disconnect,\n onDestroy: disconnect,\n });\n\n return source;\n}\n","import { areRoutesRelated } from \"@real-router/route-utils\";\n\nimport { noopDestroy } from \"./internal/noopDestroy.js\";\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 — for diffing 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 // Empty string represents the root of the name hierarchy — every named\n // route is its descendant. Without this short-circuit, `current.name`\n // would have to equal `\"\"` or start with `\".\"` (both impossible for\n // valid route names), breaking symmetry with `createRouteNodeSource(\"\")`\n // which is always-active when a route is current.\n if (routeName === \"\") {\n return true;\n }\n\n return (\n current.name === routeName || current.name.startsWith(`${routeName}.`)\n );\n };\n\n // Diffs and notifies a single route name on a router transition. Extracted\n // from the subscribe callback so the fan-out loop stays a trivial\n // isolate-per-name shell (#1478) — the recompute below (`areRoutesRelated` /\n // `isActiveNonStrict`) runs OUTSIDE the per-listener `try`, so the caller\n // wraps this whole call in a per-name `try` too.\n const notifyName = (\n routeName: string,\n listeners: Set<() => void>,\n newRouteName: string,\n previousRouteName: string | undefined,\n ): void => {\n // Cheap pre-filter: if neither new nor previous route is related to this\n // name, its active state cannot have changed. Empty routeName is the\n // implicit root — every route is its descendant, so the filter would\n // falsely exclude it (`areRoutesRelated` doesn't treat `\"\"` specially).\n // Skip the filter for the root.\n if (routeName !== \"\") {\n const isNewRelated = areRoutesRelated(routeName, newRouteName);\n const isPrevRelated =\n previousRouteName !== undefined &&\n areRoutesRelated(routeName, previousRouteName);\n\n if (!isNewRelated && !isPrevRelated) {\n return;\n }\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 return;\n }\n\n activeByName.set(routeName, nextActive);\n // Per-listener exception isolation — mirrors `BaseSource.notify`\n // (INVARIANTS \"BaseSource 3\"). Without it, one throwing Link listener would\n // abort notifications to the remaining listeners of this name (#767).\n // `activeByName.set` already ran above, so the per-name diff is committed\n // before any listener fires. Re-throw asynchronously so the genuine bug\n // still surfaces to global error handlers.\n for (const listener of listeners) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\n }\n };\n\n const connect = (): void => {\n routerUnsubscribe = router.subscribe((next) => {\n const newRouteName = next.route.name;\n const previousRouteName = next.previousRoute?.name;\n\n for (const [routeName, listeners] of listenersByName) {\n // Per-name exception isolation (#1478): `notifyName`'s recompute runs\n // OUTSIDE its per-listener `try`. A throw from it for one name would\n // unwind this shared subscribe callback, skipping every later name's\n // diff/notify and leaving their `activeByName` stale — structurally the\n // #767 failure mode one level up. Safe by construction today (`getState`\n // is a frozen-field read; the rest is pure string ops with no user code\n // in the path), but a future param-aware / predicate recompute could\n // throw here; isolate it the same way, re-throwing asynchronously so a\n // genuine bug still surfaces.\n try {\n notifyName(routeName, listeners, newRouteName, previousRouteName);\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\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\n // Stale-generation guard: if this name's registered Set is no longer the\n // one this closure captured, its generation was already fully torn down\n // (all listeners removed → Set deleted) and possibly re-created by a later\n // subscribe. Deleting / disconnecting against the stale Set would delete\n // the LIVE generation's map entry (the empty stale Set trips\n // `size === 0`), orphaning its subscribers. Nothing left for us to clean\n // up here — bail (#1206).\n if (listenersByName.get(routeName) !== listeners) {\n return;\n }\n\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","import { createActiveNameSelector } from \"./createActiveNameSelector.js\";\nimport { createActiveRouteSource } from \"./createActiveRouteSource.js\";\n\nimport type { RouterSource } from \"./types.js\";\nimport type { Params, Router, SearchParams } from \"@real-router/core\";\n\nconst NOOP = (): void => {};\n\n/**\n * Framework-agnostic fast/slow builder for an adapter `<Link>`'s active-route\n * `RouterSource<boolean>`. Every adapter Link routes its active-state check\n * through this one function so the fast/slow decision (and the `routeName !== \"\"`\n * guard) lives in a single place — a per-adapter copy of the decision drifting\n * from the shared one is exactly what produced #1416 (the vue Link kept building\n * a per-link source while the fast path sat unused in a composable).\n *\n * **Fast path** — a default-options link (non-empty `routeName`, no custom\n * `params`, non-strict, query params ignored, no `hash`) shares the per-router\n * {@link createActiveNameSelector}: ONE `router.subscribe` handle serves any\n * number of distinct-`routeName` links, versus a per-link\n * {@link createActiveRouteSource} that allocates a `BaseSource` AND opens its own\n * router subscription for every link (the 10k-listener-cap crash documented on\n * `createActiveRouteSource`). The selector's `isActive` is exactly non-strict,\n * query-ignoring, name-only matching — identical to the default\n * `createActiveRouteSource`, including for a non-string name: both coerce it and\n * both answer what its `toString` named (core does not gate the name here — see\n * `packages/core/ARCHITECTURE.md` \"Route-Name Type Gates\"). `destroy()` is a\n * no-op (the selector is per-router cached and outlives the link); the returned\n * `subscribe`'s unsubscribe removes only this link's listener.\n *\n * **Slow path** — any deviation from the defaults (custom `params`, `strict`,\n * `ignoreQueryParams: false`, hash-aware #532) falls to the per-link\n * `createActiveRouteSource`, whose cache covers the full argument surface. An\n * **empty `routeName`** also takes the slow path: it is a misuse (builds no href,\n * logs a `console.error`), and the selector's root-active `isActive(\"\") === true`\n * would light up a misused link, whereas `router.isActiveRoute(\"\")` is `false` —\n * the slow path preserves that.\n */\nexport function createActiveSource(\n router: Router,\n routeName: string,\n params: Params | undefined,\n search: SearchParams | undefined,\n strict: boolean,\n ignoreQueryParams: boolean,\n hash: string | undefined,\n): RouterSource<boolean> {\n if (\n routeName !== \"\" &&\n params === undefined &&\n // A `routeSearch` link forces the slow path (RFC-4 M2, #1548): the\n // name-only selector is search-blind, so it can't answer a query-scoped\n // active check — exactly the reason a custom `hash` also falls through.\n search === undefined &&\n !strict &&\n ignoreQueryParams &&\n hash === undefined\n ) {\n const selector = createActiveNameSelector(router);\n\n return {\n subscribe: (listener) => selector.subscribe(routeName, listener),\n getSnapshot: () => selector.isActive(routeName),\n destroy: NOOP,\n };\n }\n\n // The `hash` argument (#532) participates in the cache key when defined.\n // exactOptionalPropertyTypes forbids `{ hash: undefined }` literally — include\n // the `hash` key only when a value is provided.\n return createActiveRouteSource(\n router,\n routeName,\n params,\n search,\n hash === undefined\n ? { strict, ignoreQueryParams }\n : { strict, ignoreQueryParams, hash },\n );\n}\n","import { events } from \"@real-router/core\";\nimport { getPluginApi } from \"@real-router/core/api\";\n\nimport { BaseSource } from \"./BaseSource\";\nimport { noopDestroy } from \"./internal/noopDestroy.js\";\nimport { stabilizeState } from \"./stabilizeState.js\";\n\nimport type { RouterTransitionSnapshot, RouterSource } from \"./types.js\";\nimport type { Router, State } from \"@real-router/core\";\n\n// Frozen so accidental consumer mutation (`source.getSnapshot().toRoute = X`)\n// throws in strict mode. The singleton ref is shared across every IDLE state\n// for the lifetime of the process — mutating it would corrupt the contract\n// \"all IDLE snapshots are the same object reference\" relied on by every\n// adapter's useSyncExternalStore equivalent.\nconst IDLE_SNAPSHOT: RouterTransitionSnapshot = Object.freeze({\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\n/**\n * @internal test-only export — returns the next snapshot for a TRANSITION_START\n * payload, or `null` when the same-paths dedup guard should suppress the\n * update. Exported so the (structurally-unreachable after #605) guard can be\n * exercised by unit tests without resorting to private-API hacks.\n */\nexport function nextTransitionStartSnapshot(\n prev: RouterTransitionSnapshot,\n toState: State,\n fromState: State | undefined,\n): RouterTransitionSnapshot | null {\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 return null;\n }\n\n return {\n isTransitioning: true,\n isLeaveApproved: false,\n toRoute: newToRoute,\n fromRoute: newFromRoute,\n };\n}\n\n/**\n * @internal test-only export — analogous to {@link nextTransitionStartSnapshot}\n * for the LEAVE_APPROVE payload. The guard is structurally unreachable in\n * practice (router emits LEAVE_APPROVE exactly once per pipeline) but stays\n * for plugin-driven re-entrant flows.\n */\nexport function nextLeaveApproveSnapshot(\n prev: RouterTransitionSnapshot,\n toState: State,\n fromState: State | undefined,\n): RouterTransitionSnapshot | null {\n const newToRoute = stabilizeState(prev.toRoute, toState);\n const newFromRoute = stabilizeState(prev.fromRoute, fromState ?? null);\n\n if (\n prev.isLeaveApproved &&\n newToRoute === prev.toRoute &&\n newFromRoute === prev.fromRoute\n ) {\n return null;\n }\n\n return {\n isTransitioning: true,\n isLeaveApproved: true,\n toRoute: newToRoute,\n fromRoute: newFromRoute,\n };\n}\n\nexport function createTransitionSource(\n router: Router,\n): RouterSource<RouterTransitionSnapshot> {\n // Declared before `source` so its onDestroy closure closes over an\n // initialized binding — a mid-registration throw below no longer leaves it\n // in the TDZ, keeping the source destroyable (#1440).\n const unsubs: (() => void)[] = [];\n\n const source = new BaseSource(IDLE_SNAPSHOT, {\n onDestroy: () => {\n for (const unsub of unsubs) {\n unsub();\n }\n },\n });\n\n const api = getPluginApi(router);\n\n const resetToIdle = (): void => {\n source.updateSnapshot(IDLE_SNAPSHOT);\n };\n\n const onTransitionStart = (toState: State, fromState?: State): void => {\n // The same-paths dedup branch inside nextTransitionStartSnapshot is\n // structurally unreachable after #605 (every router-emitted\n // TRANSITION_START carries a fresh State per navigate()), but the\n // helper is kept testable for future stabilizer changes — see the\n // direct unit test in createTransitionSource.test.ts.\n const next = nextTransitionStartSnapshot(\n source.getSnapshot(),\n toState,\n fromState,\n );\n\n /* v8 ignore next 3 -- @preserve: dedup-skip branch unreachable through\n normal router flow; covered directly via nextTransitionStartSnapshot\n unit test. */\n if (next === null) {\n return;\n }\n\n source.updateSnapshot(next);\n };\n\n const onLeaveApprove = (toState: State, fromState?: State): void => {\n const next = nextLeaveApproveSnapshot(\n source.getSnapshot(),\n toState,\n fromState,\n );\n\n /* v8 ignore next 3 -- @preserve: dedup-skip branch unreachable through\n normal router flow (LEAVE_APPROVE fires once per pipeline); covered\n directly via nextLeaveApproveSnapshot unit test. */\n if (next === null) {\n return;\n }\n\n source.updateSnapshot(next);\n };\n\n // Eager connection: subscribe to router events immediately. Register\n // one-by-one so a throw mid-registration (the emitter rejects a duplicate\n // listener / hits its maxListeners cap) unwinds the already-registered\n // listeners instead of leaking them and stranding the half-wired source\n // (#1440). Mirrors @real-router/rx's events$ partial-registration safety.\n /* eslint-disable unicorn/prefer-single-call -- register one-by-one so a\n mid-registration throw unwinds the already-pushed unsubs; a single\n push(a, b) evaluates every addEventListener arg BEFORE pushing, leaving an\n earlier registration untracked and leaked (#1440). */\n try {\n unsubs.push(\n api.addEventListener(events.TRANSITION_START, onTransitionStart),\n );\n unsubs.push(\n api.addEventListener(events.TRANSITION_LEAVE_APPROVE, onLeaveApprove),\n );\n unsubs.push(api.addEventListener(events.TRANSITION_SUCCESS, resetToIdle));\n unsubs.push(api.addEventListener(events.TRANSITION_ERROR, resetToIdle));\n unsubs.push(api.addEventListener(events.TRANSITION_CANCEL, resetToIdle));\n } catch (error) {\n for (const unsub of unsubs) {\n unsub();\n }\n\n throw error;\n }\n /* eslint-enable unicorn/prefer-single-call */\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","import { events } from \"@real-router/core\";\nimport { getPluginApi } from \"@real-router/core/api\";\n\nimport { BaseSource } from \"./BaseSource\";\nimport { noopDestroy } from \"./internal/noopDestroy.js\";\n\nimport type { RouterErrorSnapshot, RouterSource } from \"./types.js\";\nimport type { Router, State, RouterError } from \"@real-router/core\";\n\n// Frozen shared singleton — returned by every error source's getSnapshot()\n// until the first error. Mirrors createTransitionSource's IDLE_SNAPSHOT: a\n// consumer mutating it would corrupt the shared singleton for every error\n// source of every router (#768).\nconst INITIAL_SNAPSHOT: RouterErrorSnapshot = Object.freeze({\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 let hasError = false;\n\n // Declared before `source` so its onDestroy closure closes over an\n // initialized binding — a mid-registration throw below no longer leaves it\n // in the TDZ, keeping the source destroyable (#1440).\n const unsubs: (() => void)[] = [];\n\n const source = new BaseSource(INITIAL_SNAPSHOT, {\n onDestroy: () => {\n for (const unsub of unsubs) {\n unsub();\n }\n },\n });\n\n const api = getPluginApi(router);\n\n // Eager connection: subscribe to router events immediately. Register\n // one-by-one so a throw mid-registration (the emitter rejects a duplicate\n // listener / hits its maxListeners cap) unwinds the already-registered\n // listeners instead of leaking them and stranding the half-wired source\n // (#1440). Mirrors @real-router/rx's events$ partial-registration safety.\n /* eslint-disable unicorn/prefer-single-call -- register one-by-one so a\n mid-registration throw unwinds the already-pushed unsubs; a single\n push(a, b) evaluates every addEventListener arg BEFORE pushing, leaving an\n earlier registration untracked and leaked (#1440). */\n try {\n unsubs.push(\n api.addEventListener(\n events.TRANSITION_ERROR,\n (\n toState: State | undefined,\n fromState: State | undefined,\n err: RouterError,\n ) => {\n errorVersion++;\n hasError = true;\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 );\n unsubs.push(\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 (hasError) {\n hasError = false;\n source.updateSnapshot({\n error: null,\n toRoute: null,\n fromRoute: null,\n version: errorVersion,\n });\n }\n }),\n );\n } catch (error) {\n for (const unsub of unsubs) {\n unsub();\n }\n\n throw error;\n }\n /* eslint-enable unicorn/prefer-single-call */\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\n/**\n * Eagerly create (and thus subscribe) the per-router error source IF the router\n * supports the plugin API — used by framework adapters' `RouterProvider` to\n * capture a navigation error that fires BEFORE a `RouterErrorBoundary` mounts\n * (#778). A router-like without an internals-registry entry (a test stub, an\n * `Object.create`-derived object, or anything not produced by `createRouter`)\n * makes `getErrorSource` throw via `getPluginApi`; here that degrades to a no-op.\n *\n * The asymmetry is deliberate: `createRouteSource` tolerates such routers (it is\n * lazy and only touches `router.subscribe`), so a Provider that eagerly primes\n * the stricter error source must not crash where the route source would not. A\n * boundary mounting later still creates the error source lazily and surfaces a\n * genuine invalid-router error there.\n */\nexport function primeErrorSource(router: Router): void {\n try {\n getErrorSource(router);\n } catch {\n // Router has no internals-registry entry — nothing to prime eagerly.\n }\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { getErrorSource } from \"./createErrorSource\";\nimport { noopDestroy } from \"./internal/noopDestroy.js\";\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 // Hoisted up here so the `onFirstSubscribe` closure below can read/write\n // it before `disconnect()`'s declaration. JS hoisting makes the original\n // post-declaration order legal, but reading top-to-bottom is clearer.\n let unsubFromError: (() => void) | null = null;\n\n const buildDismissableSnapshot = (): 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>(\n buildDismissableSnapshot(),\n {\n onFirstSubscribe: () => {\n unsubFromError = errorSource.subscribe(() => {\n source.updateSnapshot(buildDismissableSnapshot());\n });\n\n // Catch up: the eager underlying error source may already hold (or have\n // since cleared) an error that changed while this wrapper had zero\n // subscribers — at first-ever subscribe, or after a disconnect/reconnect\n // cycle (onLastUnsubscribe below). Without this, a RouterErrorBoundary\n // mounting AFTER a boot-time error would never see it (#765.2). Guard on\n // the resolved `error` so the common no-error case stays a no-op: the\n // listener was added before this callback (BaseSource ordering), so an\n // unconditional updateSnapshot would notify it redundantly and break the\n // \"subscribers fire only on state-relevant actions\" contract. The\n // `version` field can skip values for an error cycle fully missed during\n // a disconnect — harmless, monotonicity still holds.\n const caughtUp = buildDismissableSnapshot();\n\n if (caughtUp.error !== source.getSnapshot().error) {\n source.updateSnapshot(caughtUp);\n }\n },\n onLastUnsubscribe: () => {\n disconnect();\n },\n },\n );\n\n function resetError(): void {\n const currentVersion = errorSource.getSnapshot().version;\n\n // No-op guard: if we already dismissed at this version (or are even ahead\n // of the live error stream), there's nothing to clear. Skipping prevents\n // a redundant snapshot allocation + listener notification under tight\n // resetError(); resetError() patterns — common when a RouterErrorBoundary\n // user clicks \"dismiss\" while another dismiss is already in flight.\n if (currentVersion <= dismissedVersion) {\n return;\n }\n\n dismissedVersion = currentVersion;\n source.updateSnapshot(buildDismissableSnapshot());\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","import type { State } from \"@real-router/core\";\n\n/**\n * Context handed to a route-enter handler once a navigation-driven mount is\n * confirmed. `previousRoute` is **non-nullable** — the gate is the sole bridge\n * from the nullable route snapshot (`{ route?, previousRoute? }`) to this\n * non-nullable contract (#1218).\n */\nexport interface RouteEnterContext {\n /** The route that was just activated. */\n route: State;\n /** The route that was active immediately before this navigation. */\n previousRoute: State;\n}\n\n/**\n * Decision closure returned by {@link createRouteEnterGate}.\n *\n * Given the already-unwrapped route snapshot (adapters own the reactive\n * unwrap) and the per-call `skipSameRoute` flag, returns the\n * {@link RouteEnterContext} to dispatch — or `null` to skip. Stateful: it\n * holds `lastHandledRoute` for the dedupe arm.\n */\nexport type RouteEnterGate = (\n route: State | undefined,\n previousRoute: State | undefined,\n skipSameRoute: boolean,\n) => RouteEnterContext | null;\n\n/**\n * Builds the framework-agnostic route-enter guard gate shared by every\n * adapter's `useRouteEnter` / `injectRouteEnter` (#1435). Each adapter owns\n * only the reactive effect wiring + snapshot unwrap + dispatch; this closure\n * owns the canonical guard superset and the `lastHandledRoute` dedupe state.\n *\n * Guard order (behavior-neutral superset of all six adapters):\n *\n * 1. `!route` — no committed route yet (svelte SSR / pre-start; a no-op for\n * the other adapters, whose `route` is defined by the time they call in).\n * 2. skip-initial — `!route.transition.from`: the first commit from\n * `router.start()` is not an \"entry\".\n * 3. same-route skip — `route.transition.from === route.name` (query-only /\n * sort-filter navigations), gated by the per-call `skipSameRoute`.\n * 4. dedupe — same snapshot reference dispatches at most once (guards\n * React StrictMode's dev double-invoke; a defensive no-op elsewhere).\n * 5. `!previousRoute` — the sole guard for the non-nullable\n * `RouteEnterContext.previousRoute` contract (#1218: reachable via a\n * Provider mounted after a navigation / an `<Activity>` catch-up, where\n * the source snapshot carries `previousRoute: undefined`).\n *\n * **`skipSameRoute` is a per-call argument, not a factory option** — React\n * holds the returned closure in a `useRef` so its `lastHandledRoute` survives\n * StrictMode effect re-runs; a factory option would force a fresh gate (and\n * reset the dedupe state) on an options flip. Threading it per-call lets the\n * same gate honor the flip without losing state.\n *\n * The gate **returns** the context and does not dispatch — dispatch stays\n * adapter-side (React/Preact via a latest-handler ref, others via a\n * capture-at-init handler). Framework pre-gates (e.g. Vue's `<KeepAlive>`\n * `isDeactivated`, #1221) compose *before* this gate and stay adapter-side.\n */\nexport function createRouteEnterGate(): RouteEnterGate {\n let lastHandledRoute: State | null = null;\n\n return (route, previousRoute, skipSameRoute) => {\n if (!route) {\n return null;\n }\n\n // `?.` on a field the type declares required, because core's commit door\n // commits a foreign State's ABSENT `transition` rather than fabricating one\n // (#1792 / #1976) — measured, the flat read threw here. Absent means \"no\n // origin known\", the answer this gate already gives a first navigation.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- the required field is genuinely absent on a foreign committed State\n if (!route.transition?.from) {\n return null;\n }\n\n if (skipSameRoute && route.transition.from === route.name) {\n return null;\n }\n\n if (lastHandledRoute === route) {\n return null;\n }\n\n if (!previousRoute) {\n return null;\n }\n\n lastHandledRoute = route;\n\n return { route, previousRoute };\n };\n}\n","import type { Router, State } from \"@real-router/core\";\n\n// `LeaveState` / `LeaveFn` live in `@real-router/types` but are NOT re-exported\n// from `@real-router/core` (which re-exports `State`), and `sources` has no\n// `@real-router/types` dependency (house rule). Derive the exact leave-listener\n// type from the facade instead — tsc-clean with zero new deps (#1435 D-C).\ntype LeaveFn = Parameters<Router[\"subscribeLeave\"]>[0];\n\n/**\n * Context handed to a route-exit handler when a genuine departure is approved.\n * Re-states core's `LeaveState` shape under the adapter-canonical name so the\n * six adapters can re-export it verbatim (#1435).\n */\nexport interface RouteExitContext {\n /** The route being left (`fromState`). */\n route: State;\n /** The navigation target (`toState`). */\n nextRoute: State;\n /** Aborts if the navigation is cancelled or fails before commit. */\n signal: AbortSignal;\n}\n\nexport interface UseRouteExitOptions {\n /**\n * Skip the handler when `route.name === nextRoute.name` (sort/filter/\n * query-only navigations on the same route). Default: `true`.\n */\n skipSameRoute?: boolean;\n}\n\ntype LeaveHandler = (context: RouteExitContext) => void | Promise<void>;\n\n/**\n * Wraps a route-exit `handler` into the canonical `subscribeLeave` listener\n * shared by every adapter's `useRouteExit` / `injectRouteExit` (#1435). The\n * adapter owns only registration + cleanup (and, for React/Preact, the\n * latest-handler ref via a thunk); this HOF owns the guard sequence:\n *\n * 1. same-route skip — `route.name === nextRoute.name` (live `State` names,\n * pre-commit), gated by `skipSameRoute` (default `true`).\n * 2. reentrant-abort pre-check — a `signal` already aborted when the listener\n * fires means the navigation is gone; `addEventListener(\"abort\", …)` does\n * not fire retroactively, so skip the handler entirely.\n * 3. passthrough — `return handler(ctx)` so the returned Promise reaches\n * core's `settleLeavePromises` and **blocks** the activation phase.\n *\n * **Error contract:** a rejected handler Promise rejects `navigate()` with the\n * handler's **original error** and emits `TRANSITION_ERROR` — it is NOT\n * re-coded to `TRANSITION_CANCELLED`. `TRANSITION_CANCELLED` arises only when\n * the payload `signal` aborts (supersede / `stop()` / `dispose()` / external\n * `opts.signal`). A fire-and-forget call here would silently un-block the\n * transition, so arm 3 must stay a passthrough.\n */\nexport function guardLeaveListener(\n handler: LeaveHandler,\n options?: UseRouteExitOptions,\n): LeaveFn {\n const skipSameRoute = options?.skipSameRoute ?? true;\n\n return ({ route, nextRoute, signal }) => {\n if (skipSameRoute && route.name === nextRoute.name) {\n return;\n }\n\n if (signal.aborted) {\n return;\n }\n\n return handler({ route, nextRoute, signal });\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["#listeners","#onFirstSubscribe","#onLastUnsubscribe","#onDestroy","#currentSnapshot","#destroyed"],"sources":["../../src/BaseSource.ts","../../src/internal/readContextHash.ts","../../src/stabilizeState.ts","../../src/createRouteSource.ts","../../src/computeSnapshot.ts","../../src/internal/noopDestroy.ts","../../src/createRouteNodeSource.ts","../../src/canonicalJson.ts","../../src/normalizeActiveOptions.ts","../../src/createActiveRouteSource.ts","../../src/createActiveNameSelector.ts","../../src/createActiveSource.ts","../../src/createTransitionSource.ts","../../src/createErrorSource.ts","../../src/createDismissableError.ts","../../src/createRouteEnterGate.ts","../../src/guardLeaveListener.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 // Isolate listener exceptions so a single throwing subscriber (e.g. React\n // error-boundary fallback throwing inside `onStoreChange`) does not block\n // the remaining subscribers — the invariant \"after updateSnapshot all\n // listeners see the new snapshot\" must hold. Re-throw asynchronously via\n // queueMicrotask so global error handlers / test harnesses still surface\n // the bug without breaking the synchronous notification fan-out.\n for (const listener of this.#listeners) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\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 * @internal\n *\n * Reads the URL fragment published by `browser-plugin` / `navigation-plugin`\n * on a router state. The plugins claim the `\"url\"` namespace via\n * `state.context.url` and the `hash` field carries the **decoded** fragment.\n *\n * Returns:\n * - `undefined` — no plugin claimed the `\"url\"` namespace (hash-plugin runtime,\n * memory-plugin, SSR before hydration) OR the state itself is nullish;\n * - `\"\"` — the URL namespace exists but the fragment is empty\n * (browser-plugin on a hash-less URL).\n *\n * Callers that need the \"no namespace at all\" branch (e.g. `stabilizeState`\n * comparing cross-plugin transitions) read the raw `undefined`. Callers that\n * collapse \"no namespace\" to \"no hash\" (e.g. `createActiveRouteSource`'s\n * hash-equality check) coalesce with `?? \"\"` themselves.\n *\n * Centralising the context cast removes the previous duplicate definitions in\n * `stabilizeState.ts` and `createActiveRouteSource.ts` that drifted in\n * signature (state vs router) and default-value (undefined vs \"\") — both\n * variants are reconstructible from this single helper at the callsite.\n */\nexport function readContextHash(\n state: State | null | undefined,\n): string | undefined {\n const ctx = state?.context as { url?: { hash?: string } } | undefined;\n\n return ctx?.url?.hash;\n}\n","import { readContextHash } from \"./internal/readContextHash.js\";\n\nimport type { State } from \"@real-router/core\";\n\n/**\n * State-aware stabilization for route snapshots.\n *\n * Compares `path` (canonical name+params), `state.context.url.hash`\n * (URL fragment, #532), and `state.transition.reload` (#605). When all\n * three match (idempotent navigation), returns `prev` (preserving\n * reference) so frameworks can skip re-renders. When any of them flips,\n * returns `next` so consumers subscribing through `useRoute()` see the\n * new state.\n *\n * `transition.reload === true` is the user's explicit signal for a\n * non-idempotent navigation — `router.navigate(name, params, search, {\n * reload: true })` is the canonical pairing for `invalidate(router, namespace)`\n * and any cache-bust pattern. Bypassing stabilization for reloads makes\n * `useRoute()` consumers see fresh `state.context.<namespace>` values\n * written by the SSR loader plugin's `subscribeLeave` handler.\n *\n * Ignores `meta` (internal: auto-increment id), other `transition` fields\n * (`from`, `segments`, `redirected`), and `state.context.navigation` /\n * `state.context.browser` (transient transition metadata) — they don't\n * affect render identity for idempotent navigations.\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 // Explicit reload navigation (#605) — caller asked to bypass dedupe so\n // observers see fresh `state.context` written by `invalidate()`-driven\n // loader re-runs. The path equality above guarantees both prev and next\n // are either non-null with matching paths or both nullish; only the\n // non-null branch can carry a meaningful `transition.reload`.\n if (readReloadFlag(next)) {\n return next;\n }\n\n return prev;\n}\n\nfunction readReloadFlag(state: State | null | undefined): boolean {\n // Defensive read: `transition` is mandatory in the public State type, but a\n // plugin returning a malformed state (or a future fork) shouldn't crash the\n // stabilizer with a TypeError. We cast to a structurally-loose shape so the\n // optional chain is permitted; the runtime guard preserves dedup (false =\n // not-a-reload) for malformed inputs.\n const transition = (\n state as { transition?: { reload?: boolean } } | null | undefined\n )?.transition;\n\n return transition?.reload === true;\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 // Reconcile the route with the current router state before connecting.\n // While this source had zero listeners it was disconnected\n // (onLastUnsubscribe below), so a navigation in that window was missed —\n // the just-added listener would otherwise observe a stale route (#765).\n // BaseSource registered the listener before this callback, so the\n // updateSnapshot below reaches it.\n //\n // Emit ONLY when the route actually changed: a hide/show (Activity)\n // cycle with no navigation in between must not fire a spurious\n // re-render, and its still-valid `previousRoute` must survive. When a\n // navigation WAS caught up, `previousRoute` resets to `undefined`: the\n // catch-up is a snap to the current state, not an observed navigation,\n // and the true previous route cannot be reconstructed outside a live\n // subscribe payload (parity of intent with createRouteNodeSource, whose\n // reconnect reconcile likewise drops previousRoute on catch-up).\n const current = source.getSnapshot();\n const reconciledRoute = stabilizeState(\n current.route,\n router.getState(),\n );\n\n if (reconciledRoute !== current.route) {\n source.updateSnapshot({\n route: reconciledRoute,\n previousRoute: undefined,\n });\n }\n\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","/**\n * @internal\n *\n * Shared no-op `destroy()` for cached `RouterSource` wrappers.\n *\n * Cached factories (`getTransitionSource`, `getErrorSource`, `createDismissableError`,\n * `createActiveNameSelector`, cache hit in `createRouteNodeSource`,\n * `createActiveRouteSource`) return a wrapper whose `destroy()` is a no-op —\n * the underlying source is shared across all consumers and lives as long as\n * the router (WeakMap entry releases on router GC).\n *\n * One module-level function shared by every cached factory keeps the wrapper\n * shape (`{ subscribe, getSnapshot, destroy: noopDestroy }`) byte-stable and\n * eliminates the previous six copies. (Bundlers inline a stand-alone arrow\n * just as readily, so the cost is purely on the maintenance side.)\n */\nexport function noopDestroy(): void {\n // Shared cached source — external destroy() is a no-op.\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { computeSnapshot } from \"./computeSnapshot.js\";\nimport { noopDestroy } from \"./internal/noopDestroy.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 // computeSnapshot returns the SAME currentSnapshot reference when\n // both route and previousRoute stabilize to prev — guard against\n // emitting redundant updates to listeners (matters for signal-\n // based adapters that re-run effects on every set).\n /* v8 ignore next 3 -- @preserve: structurally unreachable after #605\n — reload navs always return fresh refs via stabilizeState, and\n within-node non-reload navs short-circuit at shouldUpdate. Guard\n kept for defensive correctness against future stabilizer changes. */\n if (Object.is(source.getSnapshot(), newSnapshot)) {\n return;\n }\n\n source.updateSnapshot(newSnapshot);\n });\n },\n onLastUnsubscribe: disconnect,\n },\n );\n\n return source;\n}\n","/**\n * Intrinsics captured at module load (#1971).\n *\n * ⚑ These DECIDE — each answers \"what is on this object\" for a value this module\n * did not build, so read off the live global they are the weakest point of every\n * check built on them. `guards.ts` states the doctrine and its measurement: one\n * naive `Object.hasOwn` polyfill walked straight through five sibling readers\n * while the single captured guard held.\n *\n * ⚠ Capture narrows the window from \"any time after boot\" to \"before this module\n * loads\". It does not close it — a shim evaluated ahead of core still wins\n * (#1798), which is the doctrine's own caveat and travels with it.\n */\nconst objectKeys = Object.keys;\n\n/** Captured like the deciding seven, but this one BUILDS the guarantee (#2072). */\nconst objectCreate = Object.create;\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 * **Divergence from `shared/dom-utils/scroll-restore.canonicalJson` — by\n * design.** That sibling implementation is the cheap navigation-hot-path\n * variant (uses `localeCompare`, plain-object accumulator, native cycle\n * detector) and pairs with `safeKeyOf` for crash-tolerance. This one is the\n * strict cache-key variant: byte-order compare, prototype-less accumulator,\n * bespoke cycle detection — eagerly throws on inputs that would cause\n * collisions or pollution, so callers can fall back to non-cached sources.\n * They are NOT interchangeable; cross-package equivalence is explicitly not\n * a goal (audit-2 / audit-2026-05-17 §2).\n *\n * Edge cases:\n * - Arrays preserve order (canonical: index-ordered already).\n * - `undefined` values are dropped (standard JSON behaviour).\n * - `Date` is serialized via its `toJSON` method (ISO string). **Collision\n * caveat:** `{ d: new Date(0) }` and `{ d: \"1970-01-01T00:00:00.000Z\" }`\n * produce the **same** cache key (both reduce to the ISO string), so the two\n * inputs share one cached source. Harmless in practice — `Date` params are\n * exotic and `isActiveRoute` stringifies them identically post-encoding.\n * - `Symbol` becomes `undefined` (standard JSON behaviour).\n * - `BigInt` throws via `JSON.stringify` defaults.\n * - `Map`, `Set`, `RegExp`, `WeakMap`, `WeakSet` would silently collapse to\n * `\"{}\"` (no enumerable own keys), which would cause **different inputs to\n * share the same cache key**. We detect these explicitly and throw — callers\n * then take the non-cached fallback path (same behaviour as `BigInt`).\n * - Circular references throw `TypeError` (parity with native `JSON.stringify`).\n * The replacer copies each object level into a fresh prototype-less record,\n * so we must run our own cycle detection — the native detector never sees\n * the original object graph.\n * - `__proto__` keys are preserved as own properties (no prototype pollution\n * and no silent collision between `{ __proto__: x, b: 1 }` and `{ b: 1 }`).\n *\n * In practice, route params carry primitives (`string | number | boolean`);\n * the cache-key-collision edge cases above are defensive bugs caught loudly.\n */\nexport function canonicalJson(value: unknown): string {\n return JSON.stringify(canonicalize(value, new Set<object>()));\n}\n\n// Key comparison uses byte-order (`<` / `>`) instead of `localeCompare()` so\n// the output is independent of the Node.js ICU build / system locale. Same\n// canonical form on every machine — required for cache-key stability.\nfunction compareKeys(left: string, right: string): number {\n return left < right ? -1 : 1;\n}\n\n/**\n * Returns a structural clone with object keys sorted at every level. Path-based\n * cycle detection (`path` set) matches the semantics of native `JSON.stringify`\n * — a `TypeError` is thrown on a true cycle, but the same object reachable via\n * two independent branches (DAG) serialises fine.\n *\n * `Date` instances pass through unchanged so `JSON.stringify` can invoke their\n * `toJSON` hook. Other built-ins (`Map`, `Set`, `WeakMap`, `WeakSet`, `RegExp`)\n * throw eagerly to avoid silent cache-key collisions.\n */\nfunction canonicalize(value: unknown, path: Set<object>): unknown {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (value instanceof Date) {\n return value;\n }\n\n if (\n value instanceof Map ||\n value instanceof Set ||\n value instanceof WeakMap ||\n value instanceof WeakSet ||\n value instanceof RegExp\n ) {\n throw new TypeError(\n `canonicalJson: cannot serialize ${(value as { constructor: { name: string } }).constructor.name} — non-enumerable own keys collapse to \"{}\" and would cause cache-key collisions. Pass primitive params (string | number | boolean) instead.`,\n );\n }\n\n if (path.has(value)) {\n throw new TypeError(\n \"canonicalJson: cannot serialize circular structure (cycle detected during traversal).\",\n );\n }\n\n path.add(value);\n try {\n if (Array.isArray(value)) {\n return value.map((item) => canonicalize(item, path));\n }\n\n // Use a null-prototype record so `__proto__` is treated as a regular\n // own property — assigning to a plain `{}` would set the prototype\n // chain instead and silently collide with inputs that omit the key.\n const sorted: Record<string, unknown> = objectCreate(null) as Record<\n string,\n unknown\n >;\n const keys = objectKeys(value).toSorted(compareKeys);\n\n for (const key of keys) {\n sorted[key] = canonicalize((value as Record<string, unknown>)[key], path);\n }\n\n return sorted;\n } finally {\n path.delete(value);\n }\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 { noopDestroy } from \"./internal/noopDestroy.js\";\nimport { readContextHash } from \"./internal/readContextHash.js\";\nimport { normalizeActiveOptions } from \"./normalizeActiveOptions.js\";\n\nimport type { ActiveRouteSourceOptions, RouterSource } from \"./types.js\";\nimport type { Params, Router, SearchParams } 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 * For cached entries `destroy()` is a no-op — shared sources live with the\n * router and release automatically on router GC (WeakMap entry).\n *\n * A non-string route name, or `BigInt`/circular params → the source bypasses the cache\n * and `destroy()` becomes a real teardown that detaches the underlying\n * `router.subscribe` handle.\n */\nexport function createActiveRouteSource(\n router: Router,\n routeName: string,\n params?: Params,\n search?: SearchParams,\n options?: ActiveRouteSourceOptions,\n): RouterSource<boolean> {\n const { strict, ignoreQueryParams, hash } = normalizeActiveOptions(options);\n\n // Two paths produce a fresh (non-cached) source: a NON-STRING route name\n // (the guard below, #1881) and params/search `canonicalJson` cannot\n // serialize. Callers hit either rarely; the extra allocation is acceptable.\n let key: string | undefined;\n\n // ⚑ A NON-STRING name never gets a cache slot, and this is a correctness\n // guard rather than a serialization one. The key below is a template literal,\n // so it COERCES the name: `{ toString: () => \"fwd\" }` and the string `\"fwd\"`\n // produce the identical key and therefore share one cached source. Whichever\n // call arrives first decides the answer for both.\n //\n // ⚠ Core does NOT refuse a non-string name (`packages/core/ARCHITECTURE.md`,\n // \"Route-Name Type Gates\"), so the collision is invisible for a name that\n // resolves through `forwardTo` — router on `/users`, a bag naming `\"fwd\"`\n // answers `true`, same as the string. It is NOT invisible for a plain name:\n // `isActiveRoute` compares the active name by IDENTITY first, so a bag naming\n // `\"users\"` answers `false` while the router is on `/users`, and the\n // well-typed sibling that shares its cache slot inherits that `false` — a\n // correct `<Link to=\"users\">` goes dark. That half needs no drifting\n // `toString` and no gate anywhere to reproduce; bypassing the cache keeps the\n // bad call's answer to itself.\n if (typeof routeName !== \"string\") {\n return buildActiveRouteSource(\n router,\n routeName,\n params,\n search,\n strict,\n ignoreQueryParams,\n hash,\n );\n }\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 // `params === undefined` is the common Link case (`<Link to=\"users\">`\n // with no params). Skip canonicalJson(undefined) — it returns the literal\n // string \"undefined\" and template interpolation would just embed it. An\n // explicit empty sentinel avoids the call and shaves the cache-key by 9\n // characters per Link.\n const paramsKey = params === undefined ? \"\" : canonicalJson(params);\n\n // Query channel (RFC-4 M2, #1548) — keyed the same way as `params`:\n // `undefined` (the common no-search Link) collapses to the empty sentinel\n // to skip `canonicalJson(undefined)` and share one entry with a no-search\n // call. Distinct from `params` so `{ search: { q } }` and `{ params: { q } }`\n // never collide on the same cache slot.\n const searchKey = search === undefined ? \"\" : canonicalJson(search);\n\n // Delimiter `|` is safe because route names use `.` as the segment\n // separator (`users.list`, not `users|list`) and canonicalJson-encoded\n // params escape `\"` (so any literal `|` inside params lives inside a\n // quoted JSON string and can't be confused with our delimiter). If route\n // names ever grow a `|` character, this composite key would become\n // ambiguous — change the separator to a control char or hash-encode each\n // field.\n key = `${routeName}|${paramsKey}|${searchKey}|${String(strict)}|${String(ignoreQueryParams)}|${hashKey}`;\n } catch {\n key = undefined;\n }\n\n if (key === undefined) {\n // Non-cached fallback (canonicalJson threw on BigInt / circular / etc.).\n // Return the real source — `destroy()` must unwind the router subscription;\n // otherwise the wrapper leaks for the lifetime of the router.\n return buildActiveRouteSource(\n router,\n routeName,\n params,\n search,\n strict,\n ignoreQueryParams,\n hash,\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 search,\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 * 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 search: SearchParams | undefined,\n strict: boolean,\n ignoreQueryParams: boolean,\n hash: string | undefined,\n): boolean {\n const routeActive = router.isActiveRoute(\n routeName,\n params,\n // Query channel at position 3 (RFC-4 M2 / #1548). `isActiveRoute` only\n // consults it when `ignoreQueryParams` is false — the default active-route\n // match (ignoreQueryParams: true) still ignores it, so a `routeSearch`-less\n // Link behaves exactly as before.\n search,\n strict,\n ignoreQueryParams,\n );\n\n if (!routeActive) {\n return false;\n }\n if (hash === undefined) {\n return true;\n }\n\n // `readContextHash` returns `undefined` when no URL plugin claimed the\n // namespace (hash-plugin runtime, memory-plugin, SSR). For hash-equality\n // matching we collapse that to `\"\"` — a hash-aware Link with no URL plugin\n // can only match when the consumer also asked for `hash: \"\"`.\n return (readContextHash(router.getState()) ?? \"\") === hash;\n}\n\nfunction buildActiveRouteSource(\n router: Router,\n routeName: string,\n params: Params | undefined,\n search: SearchParams | 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 search,\n strict,\n ignoreQueryParams,\n hash,\n );\n\n let routerUnsubscribe: (() => void) | undefined;\n\n const disconnect = (): void => {\n const unsub = routerUnsubscribe;\n\n routerUnsubscribe = undefined;\n unsub?.();\n };\n\n const source = new BaseSource(initialValue, {\n onFirstSubscribe: () => {\n // Reconcile before connecting: while disconnected (zero listeners) the\n // source missed navigations, so the active boolean may be stale.\n // Recompute against the current router state and notify the just-added\n // listener — BaseSource registers it before onFirstSubscribe. Mirrors the\n // reconnect reconcile of createRouteNodeSource / createRouteSource (#765).\n const reconciled = computeActive(\n router,\n routeName,\n params,\n search,\n strict,\n ignoreQueryParams,\n hash,\n );\n\n if (!Object.is(source.getSnapshot(), reconciled)) {\n source.updateSnapshot(reconciled);\n }\n\n // Lazy connection (#766): subscribe on the FIRST listener, disconnect on\n // the LAST (onLastUnsubscribe). Previously this source subscribed eagerly\n // at construction with a no-op destroy on the cached wrapper, so every\n // unique cache key held a PERMANENT router subscription that survived all\n // Link unmounts — a long-lived router with per-item-params Links\n // accumulated handles until the EventEmitter listener limit (10000)\n // crashed the render path. The cache entry still lives with the router\n // (cheap: a closure), but now holds no subscription while it has zero\n // listeners; the non-cached paths (a non-string name, or unserializable params) likewise\n // detaches through `disconnect`.\n routerUnsubscribe = 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\n // `hashChanged` flag published by URL plugins.\n const hashFlip =\n hash !== undefined &&\n ((\n next.route.context as\n { 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 search,\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 onLastUnsubscribe: disconnect,\n onDestroy: disconnect,\n });\n\n return source;\n}\n","import { areRoutesRelated } from \"@real-router/route-utils\";\n\nimport { noopDestroy } from \"./internal/noopDestroy.js\";\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 — for diffing 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 // Empty string represents the root of the name hierarchy — every named\n // route is its descendant. Without this short-circuit, `current.name`\n // would have to equal `\"\"` or start with `\".\"` (both impossible for\n // valid route names), breaking symmetry with `createRouteNodeSource(\"\")`\n // which is always-active when a route is current.\n if (routeName === \"\") {\n return true;\n }\n\n return (\n current.name === routeName || current.name.startsWith(`${routeName}.`)\n );\n };\n\n // Diffs and notifies a single route name on a router transition. Extracted\n // from the subscribe callback so the fan-out loop stays a trivial\n // isolate-per-name shell (#1478) — the recompute below (`areRoutesRelated` /\n // `isActiveNonStrict`) runs OUTSIDE the per-listener `try`, so the caller\n // wraps this whole call in a per-name `try` too.\n const notifyName = (\n routeName: string,\n listeners: Set<() => void>,\n newRouteName: string,\n previousRouteName: string | undefined,\n ): void => {\n // Cheap pre-filter: if neither new nor previous route is related to this\n // name, its active state cannot have changed. Empty routeName is the\n // implicit root — every route is its descendant, so the filter would\n // falsely exclude it (`areRoutesRelated` doesn't treat `\"\"` specially).\n // Skip the filter for the root.\n if (routeName !== \"\") {\n const isNewRelated = areRoutesRelated(routeName, newRouteName);\n const isPrevRelated =\n previousRouteName !== undefined &&\n areRoutesRelated(routeName, previousRouteName);\n\n if (!isNewRelated && !isPrevRelated) {\n return;\n }\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 return;\n }\n\n activeByName.set(routeName, nextActive);\n // Per-listener exception isolation — mirrors `BaseSource.notify`\n // (INVARIANTS \"BaseSource 3\"). Without it, one throwing Link listener would\n // abort notifications to the remaining listeners of this name (#767).\n // `activeByName.set` already ran above, so the per-name diff is committed\n // before any listener fires. Re-throw asynchronously so the genuine bug\n // still surfaces to global error handlers.\n for (const listener of listeners) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\n }\n };\n\n const connect = (): void => {\n routerUnsubscribe = router.subscribe((next) => {\n const newRouteName = next.route.name;\n const previousRouteName = next.previousRoute?.name;\n\n for (const [routeName, listeners] of listenersByName) {\n // Per-name exception isolation (#1478): `notifyName`'s recompute runs\n // OUTSIDE its per-listener `try`. A throw from it for one name would\n // unwind this shared subscribe callback, skipping every later name's\n // diff/notify and leaving their `activeByName` stale — structurally the\n // #767 failure mode one level up. Safe by construction today (`getState`\n // is a frozen-field read; the rest is pure string ops with no user code\n // in the path), but a future param-aware / predicate recompute could\n // throw here; isolate it the same way, re-throwing asynchronously so a\n // genuine bug still surfaces.\n try {\n notifyName(routeName, listeners, newRouteName, previousRouteName);\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\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\n // Stale-generation guard: if this name's registered Set is no longer the\n // one this closure captured, its generation was already fully torn down\n // (all listeners removed → Set deleted) and possibly re-created by a later\n // subscribe. Deleting / disconnecting against the stale Set would delete\n // the LIVE generation's map entry (the empty stale Set trips\n // `size === 0`), orphaning its subscribers. Nothing left for us to clean\n // up here — bail (#1206).\n if (listenersByName.get(routeName) !== listeners) {\n return;\n }\n\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","import { createActiveNameSelector } from \"./createActiveNameSelector.js\";\nimport { createActiveRouteSource } from \"./createActiveRouteSource.js\";\n\nimport type { RouterSource } from \"./types.js\";\nimport type { Params, Router, SearchParams } from \"@real-router/core\";\n\nconst NOOP = (): void => {};\n\n/**\n * Framework-agnostic fast/slow builder for an adapter `<Link>`'s active-route\n * `RouterSource<boolean>`. Every adapter Link routes its active-state check\n * through this one function so the fast/slow decision (and the `routeName !== \"\"`\n * guard) lives in a single place — a per-adapter copy of the decision drifting\n * from the shared one is exactly what produced #1416 (the vue Link kept building\n * a per-link source while the fast path sat unused in a composable).\n *\n * **Fast path** — a default-options link (non-empty `routeName`, no custom\n * `params`, non-strict, query params ignored, no `hash`) shares the per-router\n * {@link createActiveNameSelector}: ONE `router.subscribe` handle serves any\n * number of distinct-`routeName` links, versus a per-link\n * {@link createActiveRouteSource} that allocates a `BaseSource` AND opens its own\n * router subscription for every link (the 10k-listener-cap crash documented on\n * `createActiveRouteSource`). The selector's `isActive` is exactly non-strict,\n * query-ignoring, name-only matching — identical to the default\n * `createActiveRouteSource`, including for a non-string name: both coerce it and\n * both answer what its `toString` named (core does not gate the name here — see\n * `packages/core/ARCHITECTURE.md` \"Route-Name Type Gates\"). `destroy()` is a\n * no-op (the selector is per-router cached and outlives the link); the returned\n * `subscribe`'s unsubscribe removes only this link's listener.\n *\n * **Slow path** — any deviation from the defaults (custom `params`, `strict`,\n * `ignoreQueryParams: false`, hash-aware #532) falls to the per-link\n * `createActiveRouteSource`, whose cache covers the full argument surface. An\n * **empty `routeName`** also takes the slow path: it is a misuse (builds no href,\n * logs a `console.error`), and the selector's root-active `isActive(\"\") === true`\n * would light up a misused link, whereas `router.isActiveRoute(\"\")` is `false` —\n * the slow path preserves that.\n */\nexport function createActiveSource(\n router: Router,\n routeName: string,\n params: Params | undefined,\n search: SearchParams | undefined,\n strict: boolean,\n ignoreQueryParams: boolean,\n hash: string | undefined,\n): RouterSource<boolean> {\n if (\n routeName !== \"\" &&\n params === undefined &&\n // A `routeSearch` link forces the slow path (RFC-4 M2, #1548): the\n // name-only selector is search-blind, so it can't answer a query-scoped\n // active check — exactly the reason a custom `hash` also falls through.\n search === undefined &&\n !strict &&\n ignoreQueryParams &&\n hash === undefined\n ) {\n const selector = createActiveNameSelector(router);\n\n return {\n subscribe: (listener) => selector.subscribe(routeName, listener),\n getSnapshot: () => selector.isActive(routeName),\n destroy: NOOP,\n };\n }\n\n // The `hash` argument (#532) participates in the cache key when defined.\n // exactOptionalPropertyTypes forbids `{ hash: undefined }` literally — include\n // the `hash` key only when a value is provided.\n return createActiveRouteSource(\n router,\n routeName,\n params,\n search,\n hash === undefined\n ? { strict, ignoreQueryParams }\n : { strict, ignoreQueryParams, hash },\n );\n}\n","import { events } from \"@real-router/core\";\nimport { getPluginApi } from \"@real-router/core/api\";\n\nimport { BaseSource } from \"./BaseSource\";\nimport { noopDestroy } from \"./internal/noopDestroy.js\";\nimport { stabilizeState } from \"./stabilizeState.js\";\n\nimport type { RouterTransitionSnapshot, RouterSource } from \"./types.js\";\nimport type { Router, State } from \"@real-router/core\";\n\n// Frozen so accidental consumer mutation (`source.getSnapshot().toRoute = X`)\n// throws in strict mode. The singleton ref is shared across every IDLE state\n// for the lifetime of the process — mutating it would corrupt the contract\n// \"all IDLE snapshots are the same object reference\" relied on by every\n// adapter's useSyncExternalStore equivalent.\nconst IDLE_SNAPSHOT: RouterTransitionSnapshot = Object.freeze({\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\n/**\n * @internal test-only export — returns the next snapshot for a TRANSITION_START\n * payload, or `null` when the same-paths dedup guard should suppress the\n * update. Exported so the (structurally-unreachable after #605) guard can be\n * exercised by unit tests without resorting to private-API hacks.\n */\nexport function nextTransitionStartSnapshot(\n prev: RouterTransitionSnapshot,\n toState: State,\n fromState: State | undefined,\n): RouterTransitionSnapshot | null {\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 return null;\n }\n\n return {\n isTransitioning: true,\n isLeaveApproved: false,\n toRoute: newToRoute,\n fromRoute: newFromRoute,\n };\n}\n\n/**\n * @internal test-only export — analogous to {@link nextTransitionStartSnapshot}\n * for the LEAVE_APPROVE payload. The guard is structurally unreachable in\n * practice (router emits LEAVE_APPROVE exactly once per pipeline) but stays\n * for plugin-driven re-entrant flows.\n */\nexport function nextLeaveApproveSnapshot(\n prev: RouterTransitionSnapshot,\n toState: State,\n fromState: State | undefined,\n): RouterTransitionSnapshot | null {\n const newToRoute = stabilizeState(prev.toRoute, toState);\n const newFromRoute = stabilizeState(prev.fromRoute, fromState ?? null);\n\n if (\n prev.isLeaveApproved &&\n newToRoute === prev.toRoute &&\n newFromRoute === prev.fromRoute\n ) {\n return null;\n }\n\n return {\n isTransitioning: true,\n isLeaveApproved: true,\n toRoute: newToRoute,\n fromRoute: newFromRoute,\n };\n}\n\nexport function createTransitionSource(\n router: Router,\n): RouterSource<RouterTransitionSnapshot> {\n // Declared before `source` so its onDestroy closure closes over an\n // initialized binding — a mid-registration throw below no longer leaves it\n // in the TDZ, keeping the source destroyable (#1440).\n const unsubs: (() => void)[] = [];\n\n const source = new BaseSource(IDLE_SNAPSHOT, {\n onDestroy: () => {\n for (const unsub of unsubs) {\n unsub();\n }\n },\n });\n\n const api = getPluginApi(router);\n\n const resetToIdle = (): void => {\n source.updateSnapshot(IDLE_SNAPSHOT);\n };\n\n const onTransitionStart = (toState: State, fromState?: State): void => {\n // The same-paths dedup branch inside nextTransitionStartSnapshot is\n // structurally unreachable after #605 (every router-emitted\n // TRANSITION_START carries a fresh State per navigate()), but the\n // helper is kept testable for future stabilizer changes — see the\n // direct unit test in createTransitionSource.test.ts.\n const next = nextTransitionStartSnapshot(\n source.getSnapshot(),\n toState,\n fromState,\n );\n\n /* v8 ignore next 3 -- @preserve: dedup-skip branch unreachable through\n normal router flow; covered directly via nextTransitionStartSnapshot\n unit test. */\n if (next === null) {\n return;\n }\n\n source.updateSnapshot(next);\n };\n\n const onLeaveApprove = (toState: State, fromState?: State): void => {\n const next = nextLeaveApproveSnapshot(\n source.getSnapshot(),\n toState,\n fromState,\n );\n\n /* v8 ignore next 3 -- @preserve: dedup-skip branch unreachable through\n normal router flow (LEAVE_APPROVE fires once per pipeline); covered\n directly via nextLeaveApproveSnapshot unit test. */\n if (next === null) {\n return;\n }\n\n source.updateSnapshot(next);\n };\n\n // Eager connection: subscribe to router events immediately. Register\n // one-by-one so a throw mid-registration (the emitter rejects a duplicate\n // listener / hits its maxListeners cap) unwinds the already-registered\n // listeners instead of leaking them and stranding the half-wired source\n // (#1440). Mirrors @real-router/rx's events$ partial-registration safety.\n /* eslint-disable unicorn/prefer-single-call -- register one-by-one so a\n mid-registration throw unwinds the already-pushed unsubs; a single\n push(a, b) evaluates every addEventListener arg BEFORE pushing, leaving an\n earlier registration untracked and leaked (#1440). */\n try {\n unsubs.push(\n api.addEventListener(events.TRANSITION_START, onTransitionStart),\n );\n unsubs.push(\n api.addEventListener(events.TRANSITION_LEAVE_APPROVE, onLeaveApprove),\n );\n unsubs.push(api.addEventListener(events.TRANSITION_SUCCESS, resetToIdle));\n unsubs.push(api.addEventListener(events.TRANSITION_ERROR, resetToIdle));\n unsubs.push(api.addEventListener(events.TRANSITION_CANCEL, resetToIdle));\n } catch (error) {\n for (const unsub of unsubs) {\n unsub();\n }\n\n throw error;\n }\n /* eslint-enable unicorn/prefer-single-call */\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","import { events } from \"@real-router/core\";\nimport { getPluginApi } from \"@real-router/core/api\";\n\nimport { BaseSource } from \"./BaseSource\";\nimport { noopDestroy } from \"./internal/noopDestroy.js\";\n\nimport type { RouterErrorSnapshot, RouterSource } from \"./types.js\";\nimport type { Router, State, RouterError } from \"@real-router/core\";\n\n// Frozen shared singleton — returned by every error source's getSnapshot()\n// until the first error. Mirrors createTransitionSource's IDLE_SNAPSHOT: a\n// consumer mutating it would corrupt the shared singleton for every error\n// source of every router (#768).\nconst INITIAL_SNAPSHOT: RouterErrorSnapshot = Object.freeze({\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 let hasError = false;\n\n // Declared before `source` so its onDestroy closure closes over an\n // initialized binding — a mid-registration throw below no longer leaves it\n // in the TDZ, keeping the source destroyable (#1440).\n const unsubs: (() => void)[] = [];\n\n const source = new BaseSource(INITIAL_SNAPSHOT, {\n onDestroy: () => {\n for (const unsub of unsubs) {\n unsub();\n }\n },\n });\n\n const api = getPluginApi(router);\n\n // Eager connection: subscribe to router events immediately. Register\n // one-by-one so a throw mid-registration (the emitter rejects a duplicate\n // listener / hits its maxListeners cap) unwinds the already-registered\n // listeners instead of leaking them and stranding the half-wired source\n // (#1440). Mirrors @real-router/rx's events$ partial-registration safety.\n /* eslint-disable unicorn/prefer-single-call -- register one-by-one so a\n mid-registration throw unwinds the already-pushed unsubs; a single\n push(a, b) evaluates every addEventListener arg BEFORE pushing, leaving an\n earlier registration untracked and leaked (#1440). */\n try {\n unsubs.push(\n api.addEventListener(\n events.TRANSITION_ERROR,\n (\n toState: State | undefined,\n fromState: State | undefined,\n err: RouterError,\n ) => {\n errorVersion++;\n hasError = true;\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 );\n unsubs.push(\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 (hasError) {\n hasError = false;\n source.updateSnapshot({\n error: null,\n toRoute: null,\n fromRoute: null,\n version: errorVersion,\n });\n }\n }),\n );\n } catch (error) {\n for (const unsub of unsubs) {\n unsub();\n }\n\n throw error;\n }\n /* eslint-enable unicorn/prefer-single-call */\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\n/**\n * Eagerly create (and thus subscribe) the per-router error source IF the router\n * supports the plugin API — used by framework adapters' `RouterProvider` to\n * capture a navigation error that fires BEFORE a `RouterErrorBoundary` mounts\n * (#778). A router-like without an internals-registry entry (a test stub, an\n * `Object.create`-derived object, or anything not produced by `createRouter`)\n * makes `getErrorSource` throw via `getPluginApi`; here that degrades to a no-op.\n *\n * The asymmetry is deliberate: `createRouteSource` tolerates such routers (it is\n * lazy and only touches `router.subscribe`), so a Provider that eagerly primes\n * the stricter error source must not crash where the route source would not. A\n * boundary mounting later still creates the error source lazily and surfaces a\n * genuine invalid-router error there.\n */\nexport function primeErrorSource(router: Router): void {\n try {\n getErrorSource(router);\n } catch {\n // Router has no internals-registry entry — nothing to prime eagerly.\n }\n}\n","import { BaseSource } from \"./BaseSource\";\nimport { getErrorSource } from \"./createErrorSource\";\nimport { noopDestroy } from \"./internal/noopDestroy.js\";\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 // Hoisted up here so the `onFirstSubscribe` closure below can read/write\n // it before `disconnect()`'s declaration. JS hoisting makes the original\n // post-declaration order legal, but reading top-to-bottom is clearer.\n let unsubFromError: (() => void) | null = null;\n\n const buildDismissableSnapshot = (): 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>(\n buildDismissableSnapshot(),\n {\n onFirstSubscribe: () => {\n unsubFromError = errorSource.subscribe(() => {\n source.updateSnapshot(buildDismissableSnapshot());\n });\n\n // Catch up: the eager underlying error source may already hold (or have\n // since cleared) an error that changed while this wrapper had zero\n // subscribers — at first-ever subscribe, or after a disconnect/reconnect\n // cycle (onLastUnsubscribe below). Without this, a RouterErrorBoundary\n // mounting AFTER a boot-time error would never see it (#765.2). Guard on\n // the resolved `error` so the common no-error case stays a no-op: the\n // listener was added before this callback (BaseSource ordering), so an\n // unconditional updateSnapshot would notify it redundantly and break the\n // \"subscribers fire only on state-relevant actions\" contract. The\n // `version` field can skip values for an error cycle fully missed during\n // a disconnect — harmless, monotonicity still holds.\n const caughtUp = buildDismissableSnapshot();\n\n if (caughtUp.error !== source.getSnapshot().error) {\n source.updateSnapshot(caughtUp);\n }\n },\n onLastUnsubscribe: () => {\n disconnect();\n },\n },\n );\n\n function resetError(): void {\n const currentVersion = errorSource.getSnapshot().version;\n\n // No-op guard: if we already dismissed at this version (or are even ahead\n // of the live error stream), there's nothing to clear. Skipping prevents\n // a redundant snapshot allocation + listener notification under tight\n // resetError(); resetError() patterns — common when a RouterErrorBoundary\n // user clicks \"dismiss\" while another dismiss is already in flight.\n if (currentVersion <= dismissedVersion) {\n return;\n }\n\n dismissedVersion = currentVersion;\n source.updateSnapshot(buildDismissableSnapshot());\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","import type { State } from \"@real-router/core\";\n\n/**\n * Context handed to a route-enter handler once a navigation-driven mount is\n * confirmed. `previousRoute` is **non-nullable** — the gate is the sole bridge\n * from the nullable route snapshot (`{ route?, previousRoute? }`) to this\n * non-nullable contract (#1218).\n */\nexport interface RouteEnterContext {\n /** The route that was just activated. */\n route: State;\n /** The route that was active immediately before this navigation. */\n previousRoute: State;\n}\n\n/**\n * Decision closure returned by {@link createRouteEnterGate}.\n *\n * Given the already-unwrapped route snapshot (adapters own the reactive\n * unwrap) and the per-call `skipSameRoute` flag, returns the\n * {@link RouteEnterContext} to dispatch — or `null` to skip. Stateful: it\n * holds `lastHandledRoute` for the dedupe arm.\n */\nexport type RouteEnterGate = (\n route: State | undefined,\n previousRoute: State | undefined,\n skipSameRoute: boolean,\n) => RouteEnterContext | null;\n\n/**\n * Builds the framework-agnostic route-enter guard gate shared by every\n * adapter's `useRouteEnter` / `injectRouteEnter` (#1435). Each adapter owns\n * only the reactive effect wiring + snapshot unwrap + dispatch; this closure\n * owns the canonical guard superset and the `lastHandledRoute` dedupe state.\n *\n * Guard order (behavior-neutral superset of all six adapters):\n *\n * 1. `!route` — no committed route yet (svelte SSR / pre-start; a no-op for\n * the other adapters, whose `route` is defined by the time they call in).\n * 2. skip-initial — `!route.transition.from`: the first commit from\n * `router.start()` is not an \"entry\".\n * 3. same-route skip — `route.transition.from === route.name` (query-only /\n * sort-filter navigations), gated by the per-call `skipSameRoute`.\n * 4. dedupe — same snapshot reference dispatches at most once (guards\n * React StrictMode's dev double-invoke; a defensive no-op elsewhere).\n * 5. `!previousRoute` — the sole guard for the non-nullable\n * `RouteEnterContext.previousRoute` contract (#1218: reachable via a\n * Provider mounted after a navigation / an `<Activity>` catch-up, where\n * the source snapshot carries `previousRoute: undefined`).\n *\n * **`skipSameRoute` is a per-call argument, not a factory option** — React\n * holds the returned closure in a `useRef` so its `lastHandledRoute` survives\n * StrictMode effect re-runs; a factory option would force a fresh gate (and\n * reset the dedupe state) on an options flip. Threading it per-call lets the\n * same gate honor the flip without losing state.\n *\n * The gate **returns** the context and does not dispatch — dispatch stays\n * adapter-side (React/Preact via a latest-handler ref, others via a\n * capture-at-init handler). Framework pre-gates (e.g. Vue's `<KeepAlive>`\n * `isDeactivated`, #1221) compose *before* this gate and stay adapter-side.\n */\nexport function createRouteEnterGate(): RouteEnterGate {\n let lastHandledRoute: State | null = null;\n\n return (route, previousRoute, skipSameRoute) => {\n if (!route) {\n return null;\n }\n\n // `?.` on a field the type declares required, because core's commit door\n // commits a foreign State's ABSENT `transition` rather than fabricating one\n // (#1792 / #1976) — measured, the flat read threw here. Absent means \"no\n // origin known\", the answer this gate already gives a first navigation.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- the required field is genuinely absent on a foreign committed State\n if (!route.transition?.from) {\n return null;\n }\n\n if (skipSameRoute && route.transition.from === route.name) {\n return null;\n }\n\n if (lastHandledRoute === route) {\n return null;\n }\n\n if (!previousRoute) {\n return null;\n }\n\n lastHandledRoute = route;\n\n return { route, previousRoute };\n };\n}\n","import type { Router, State } from \"@real-router/core\";\n\n// `LeaveState` / `LeaveFn` live in `@real-router/core/types` but are NOT re-exported\n// from `@real-router/core` (which re-exports `State`), and `sources` has no\n// `@real-router/core/types` dependency (house rule). Derive the exact leave-listener\n// type from the facade instead — tsc-clean with zero new deps (#1435 D-C).\ntype LeaveFn = Parameters<Router[\"subscribeLeave\"]>[0];\n\n/**\n * Context handed to a route-exit handler when a genuine departure is approved.\n * Re-states core's `LeaveState` shape under the adapter-canonical name so the\n * six adapters can re-export it verbatim (#1435).\n */\nexport interface RouteExitContext {\n /** The route being left (`fromState`). */\n route: State;\n /** The navigation target (`toState`). */\n nextRoute: State;\n /** Aborts if the navigation is cancelled or fails before commit. */\n signal: AbortSignal;\n}\n\nexport interface UseRouteExitOptions {\n /**\n * Skip the handler when `route.name === nextRoute.name` (sort/filter/\n * query-only navigations on the same route). Default: `true`.\n */\n skipSameRoute?: boolean;\n}\n\ntype LeaveHandler = (context: RouteExitContext) => void | Promise<void>;\n\n/**\n * Wraps a route-exit `handler` into the canonical `subscribeLeave` listener\n * shared by every adapter's `useRouteExit` / `injectRouteExit` (#1435). The\n * adapter owns only registration + cleanup (and, for React/Preact, the\n * latest-handler ref via a thunk); this HOF owns the guard sequence:\n *\n * 1. same-route skip — `route.name === nextRoute.name` (live `State` names,\n * pre-commit), gated by `skipSameRoute` (default `true`).\n * 2. reentrant-abort pre-check — a `signal` already aborted when the listener\n * fires means the navigation is gone; `addEventListener(\"abort\", …)` does\n * not fire retroactively, so skip the handler entirely.\n * 3. passthrough — `return handler(ctx)` so the returned Promise reaches\n * core's `settleLeavePromises` and **blocks** the activation phase.\n *\n * **Error contract:** a rejected handler Promise rejects `navigate()` with the\n * handler's **original error** and emits `TRANSITION_ERROR` — it is NOT\n * re-coded to `TRANSITION_CANCELLED`. `TRANSITION_CANCELLED` arises only when\n * the payload `signal` aborts (supersede / `stop()` / `dispose()` / external\n * `opts.signal`). A fire-and-forget call here would silently un-block the\n * transition, so arm 3 must stay a passthrough.\n */\nexport function guardLeaveListener(\n handler: LeaveHandler,\n options?: UseRouteExitOptions,\n): LeaveFn {\n const skipSameRoute = options?.skipSameRoute ?? true;\n\n return ({ route, nextRoute, signal }) => {\n if (skipSameRoute && route.name === nextRoute.name) {\n return;\n }\n\n if (signal.aborted) {\n return;\n }\n\n return handler({ route, nextRoute, signal });\n };\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.14.29",
3
+ "version": "0.14.31",
4
4
  "type": "commonjs",
5
5
  "description": "Framework-agnostic subscription layer for Real-Router state",
6
6
  "main": "./dist/cjs/index.js",
@@ -42,7 +42,7 @@
42
42
  "homepage": "https://github.com/greydragon888/real-router",
43
43
  "sideEffects": false,
44
44
  "dependencies": {
45
- "@real-router/core": "^0.125.0",
45
+ "@real-router/core": "^0.126.2",
46
46
  "@real-router/route-utils": "^0.3.1"
47
47
  },
48
48
  "scripts": {