@real-router/core 0.130.0 → 0.131.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/Router-BiGqB1Jw.js +2 -0
- package/dist/cjs/Router-BiGqB1Jw.js.map +1 -0
- package/dist/cjs/Router.d.ts.map +1 -1
- package/dist/cjs/api/getRoutesApi.d.ts.map +1 -1
- package/dist/cjs/api.js +1 -1
- package/dist/cjs/api.js.map +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/namespaces/RoutesNamespace/routesStore.d.ts +11 -0
- package/dist/cjs/namespaces/RoutesNamespace/routesStore.d.ts.map +1 -1
- package/dist/esm/Router-6i5tIy-l.mjs +2 -0
- package/dist/esm/Router-6i5tIy-l.mjs.map +1 -0
- package/dist/esm/Router.d.mts.map +1 -1
- package/dist/esm/api/getRoutesApi.d.mts.map +1 -1
- package/dist/esm/api.mjs +1 -1
- package/dist/esm/api.mjs.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/namespaces/RoutesNamespace/routesStore.d.mts +11 -0
- package/dist/esm/namespaces/RoutesNamespace/routesStore.d.mts.map +1 -1
- package/package.json +1 -1
- package/dist/cjs/Router-B_Zdy0II.js +0 -2
- package/dist/cjs/Router-B_Zdy0II.js.map +0 -1
- package/dist/esm/Router-DYp2xjnc.mjs +0 -2
- package/dist/esm/Router-DYp2xjnc.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Router-BiGqB1Jw.js","names":["objectEntries","hasOwn","objectEntries","freeze","EMPTY_SEARCH","LONE_SURROGATE_RGX","EMPTY_SEARCH","splitPathSegments","parseSegment","objectCreate","parseSegment","freeze","hasOwn","splitPathSegments","parseSegment","EMPTY_PARAM_META","EMPTY_SEARCH","emptyRecord","concealUnsafeKey","publishRecord","INVALID_QUERY_NAME_RGX","CONFIG_FAULT","hasOwn","objectKeys","getOwnPropertyDescriptor","freeze","buildParamMeta","EMPTY_PARAM_META","encodeValue","defineProperty","objectKeys","hasOwn","hasOwn","objectKeys","objectKeys","objectValues","hasOwn","events","SEAM","emptyRecord","freeze","DEFAULT_LIMITS","objectCreate","DEFAULT_LIMITS","freeze","dropUnsafeKey","objectKeys","raw","normalizeChannel","EMPTY_PARAMS","EMPTY_SEARCH","mergeQueryChannel","mergePathChannel","freeze","DEFAULT_TRANSITION","freezeStateShell","EMPTY_PARAMS","slotsShallowEqual","recordsShallowEqual","objectKeys","PLUGINS_CONST","EVENTS_CONST","freeze","objectCreate","freeze","objectCreate","hasOwn","objectKeys","areParamValuesEqual","raw","freeze","objectCreate","objectEntries","objectKeys","copyOwnData","freeze","objectKeys","freeze","hasOwn","constants","normalizeChannel","EMPTY_PARAMS","EMPTY_SEARCH","withoutUnsafeKey","recordsShallowEqual","mergeDefined","adoptChannel","RouterError","errorCodes","freeze","constants","RouterError","errorCodes","freezeThrownError","objectEntries","RouterError","errorCodes","freezeThrownError","freezeThrownError","RouterError","errorCodes","freeze","freezeThrownError","RouterError","errorCodes","constants","ownSignal","adoptNavigationOptions","ownFlag","freeze","constants","EMPTY_PARAMS","EMPTY_SEARCH","RouterError","errorCodes","freezeThrownError","constants","RouterError","errorCodes","findMisChanneledKey","misChanneledKeyMessage","adoptForeignBag","EMPTY_PARAMS","EMPTY_SEARCH","DEFAULT_TRANSITION","freezeStateShell","RouterError","errorCodes","freezeThrownError","objectCreate","hasOwn","freezeStateShell","freeze","RouterError","errorCodes","events","freezeThrownError","adoptForeignBag","EMPTY_PARAMS","EMPTY_SEARCH","adoptForeignTransition","RouterError","errorCodes","getInternals","withoutUnsafeKey","createTernaryInterceptable","SEAM","EMPTY_PARAMS","createInterceptable","EMPTY_OPTS","dropUnsafeKey","getInternals","adoptChannel","findMisChanneledKey","freezeThrownError","RouterError","errorCodes","copyOwnData"],"sources":["../../src/channels/defaults.ts","../../src/channels/modeGate.ts","../../src/engine/builder/buildTree.ts","../../src/engine/path-matcher/encoding.ts","../../src/engine/path-matcher/pathUtils.ts","../../src/engine/path-matcher/percentEncoding.ts","../../src/engine/path-matcher/registration/context.ts","../../src/engine/path-matcher/registration/buildParts.ts","../../src/engine/path-matcher/registration/errors.ts","../../src/engine/path-matcher/registration/trieNodes.ts","../../src/engine/path-matcher/registration/trie.ts","../../src/engine/path-matcher/registration/index.ts","../../src/engine/path-matcher/SegmentMatcher.ts","../../src/engine/builder/computeCaches.ts","../../src/engine/builder/createRouteTree.ts","../../src/engine/operations/routeTreeToDefinitions.ts","../../src/engine/search-params/decode.ts","../../src/engine/search-params/utils.ts","../../src/engine/search-params/strategies/array.ts","../../src/engine/search-params/strategies/boolean.ts","../../src/engine/search-params/strategies/null.ts","../../src/engine/search-params/strategies/number.ts","../../src/engine/search-params/strategies/index.ts","../../src/engine/search-params/encode.ts","../../src/engine/search-params/searchParams.ts","../../src/engine/createMatcher.ts","../../src/guards.ts","../../src/limits.ts","../../src/namespaces/DependenciesNamespace/dependenciesStore.ts","../../src/namespaces/OptionsNamespace/constants.ts","../../src/namespaces/OptionsNamespace/validators.ts","../../src/namespaces/OptionsNamespace/OptionsNamespace.ts","../../src/namespaces/OptionsNamespace/helpers.ts","../../src/pipeline/canonicalize.ts","../../src/pipeline/buildURL.ts","../../src/pipeline/materialize.ts","../../src/namespaces/StateNamespace/StateNamespace.ts","../../src/namespaces/PluginsNamespace/constants.ts","../../src/namespaces/PluginsNamespace/validators.ts","../../src/namespaces/PluginsNamespace/PluginsNamespace.ts","../../src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts","../../src/namespaces/RoutesNamespace/constants.ts","../../src/namespaces/RoutesNamespace/helpers.ts","../../src/namespaces/RoutesNamespace/forwardChain.ts","../../src/namespaces/RoutesNamespace/routesStore.ts","../../src/transitionPath.ts","../../src/namespaces/RoutesNamespace/RoutesNamespace.ts","../../src/namespaces/NavigationNamespace/constants.ts","../../src/namespaces/NavigationNamespace/transition/completeTransition.ts","../../src/namespaces/NavigationNamespace/transition/errorHandling.ts","../../src/namespaces/NavigationNamespace/transition/guardPhase.ts","../../src/namespaces/NavigationNamespace/transition/executeNavigation.ts","../../src/namespaces/NavigationNamespace/transition/navigateToNotFound.ts","../../src/namespaces/NavigationNamespace/NavigationNamespace.ts","../../src/namespaces/NavigationNamespace/types.ts","../../src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts","../../src/namespaces/EventBusNamespace/types.ts","../../src/utils/fsm/fsm.ts","../../src/routerFSM.ts","../../src/namespaces/EventBusNamespace/EventBusNamespace.ts","../../src/namespaces/RouterLifecycleNamespace/constants.ts","../../src/utils/event-emitter/EventEmitter.ts","../../src/utils/logger/constants.ts","../../src/utils/logger/RouterLogger.ts","../../src/wiring/wireNamespaces.ts","../../src/Router.ts"],"sourcesContent":["// packages/core/src/channels/defaults.ts\n\nimport { assertChannelCorrect } from \"./guard\";\nimport { putField } from \"../utils/ingest\";\n\nimport type { Params, SearchParams } from \"../types\";\n\nconst objectEntries = Object.entries;\n\n/**\n * Intrinsics captured at module load: `hasOwn`.\n *\n * ⚑ A guard is only as strong as the intrinsic it reads WHEN IT RUNS, and an\n * application can re-point any of these AFTER boot — which is what this closes.\n * Measured on the uncaptured form: one naive `Object.hasOwn` polyfill walked\n * straight through five sibling readers while the single captured guard held.\n *\n * ⚠ The limit of what capture buys — and the shim order that defeats it — is\n * stated once, in `guards.ts`. Not restated here (#2091).\n */\nconst hasOwn = Object.hasOwn;\n\n/**\n * Withholds a query default whose key the caller already filled with the RETIRED\n * single-bag spelling — the rule #1570 states for a `forwardTo` chain's\n * defaults, applied where no seam runs to enforce it.\n *\n * Nothing is moved between channels: the caller's key stays in the bag the\n * caller chose, only the default is declined. Without this the default and the\n * caller's params-twin sit in DIFFERENT channels, where no merge ranks them, and\n * the query default wins by default — the §1.1 priority inversion the channel\n * split exists to remove.\n *\n * ⚠ Scoped to `declaredQuery` — the route's `?`-declared names — and the scope\n * is what keeps `buildPath` in step with every other producer. Only a DECLARED\n * query name can have a params-bag \"twin\" at all: that spelling is the v1\n * single-bag form the migrated entry points retired, so withholding is the whole\n * point. A key the route declares NOWHERE (`/u` + `defaultSearch { theme }`) or\n * one that owns a PATH SLOT beside its query twin (`/items/:id?id`, the\n * #843/#1549 carve-out) is not a twin — the caller's params entry and the query\n * default describe different things, and withholding there printed an href the\n * route's own `matchPath` immediately rewrote (the #1552/#1578 class: href ≠\n * destination, with `buildPath` the only producer out of agreement).\n *\n * `undefined` is absence (#1550 / #1551), so a caller's removal marker does not\n * count as \"already filled\" and the default survives it.\n *\n * Returns the input untouched (no allocation) when nothing is withheld — the\n * common path, and the only one the zero-default hot path ever takes.\n */\nexport function withholdFilledSlots(\n defaults: SearchParams | undefined,\n params: Params,\n declaredQuery: readonly string[],\n): SearchParams | undefined {\n if (defaults === undefined || declaredQuery.length === 0) {\n return defaults;\n }\n\n let kept: Record<string, unknown> | undefined;\n let dropped = false;\n\n for (const [key, value] of objectEntries(defaults)) {\n // `Object.hasOwn` before the read, exactly as `findMisChanneledKey` does in\n // `./guard` and for the same reason: a bare `params[key]` walks\n // the PROTOTYPE, so a route declaring `?toString` / `?constructor` /\n // `?valueOf` read as \"the caller already filled this slot\" on an EMPTY bag.\n // The default was then withheld from every LITERAL-form producer while the\n // resolving form still applied it — `buildPath` out of agreement with\n // `navigate`, printing an href its own `matchPath` does not reproduce, which\n // is the #1552/#1578 class this very rule exists to close. (`makeState`\n // joined the literal form in Phase 4, so it withholds too; the shape is\n // unreachable there because `makeState` checks the bag it SHIPS, after the\n // canonical channels are built (#1927).\n //\n // ⚠ NOT \"because P1 refuses the triggering bag on the same predicate\":\n // P1 reads the CALLER's object and the producer reads it again, so a bag\n // answering `undefined` while P1 looks ships a value P1 never saw.)\n if (\n hasOwn(params, key) &&\n params[key] !== undefined &&\n declaredQuery.includes(key)\n ) {\n dropped = true;\n continue;\n }\n\n kept ??= {};\n // ⚑ `putField`: the key is whatever the route's own `defaultSearch` spells,\n // and registration accepts any name (#1852). Two things this closes that the\n // former plain store did not. `__proto__` reached the inherited setter and\n // replaced `kept`'s prototype instead of adding an entry; it is ordinary\n // data now. And an AMBIENT accessor under a perfectly normal name did worse.\n //\n // ⚑ A reachability argument — \"the merge below walks OWN keys, so nothing\n // this loop produces reaches a committed channel\" — does NOT hold here\n // (#1852): `__proto__` is not the whole hazard. The key is a name from the\n // route's own `defaultSearch`, and an\n // ambient accessor under that name made `buildPath` THROW instead of\n // printing a URL — measured, `TypeError: Cannot set property theme of\n // #<Object> which has only a getter`, from this line.\n putField(kept, key, value);\n }\n\n // ⚑ The COPY is returned on both arms (#1847). Handing the route's own object\n // back whenever nothing is dropped aliases it, and that is the second half of\n // the defect: the loop above has already read every key once, and the channel\n // merge downstream then reads the LIVE object again. A route's `defaultSearch`\n // is held by reference and read on every navigation by design, so an\n // accessor-backed one answers those two reads independently — which is how\n // `buildPath` prints a key `navigate` did not ship, and the reverse.\n //\n // The literal form is the only caller, so this is also the only place the two\n // doors could diverge on one intent: with one read each, they agree by\n // construction rather than by luck.\n if (kept !== undefined) {\n return kept as SearchParams;\n }\n\n // Nothing survived. Either every key was dropped — in which case there is no\n // default left — or `defaults` carries no own enumerable key at all.\n //\n // ⚑ Handing THAT one back is safe by OWNERSHIP, not by emptiness (#2135).\n // \"There is nothing in it to read\" is a reachability argument, and this\n // package's own rule refuses one (`packages/core/CLAUDE.md`: omitting a guard\n // needs ownership, not reachability). A drifting container refutes it\n // directly — answering `[]` to the loop above and its real keys to the merge\n // below is one bag enumerated twice in a single `buildPath` frame, with\n // #1570's withholding rule never seeing the key it exists to withhold.\n //\n // What actually closes it is that `defaults` is not the caller's object.\n // Since #2172 the route door and the options door both store `copyOwnData`'s\n // FROZEN PLAIN copy, and a frozen plain object cannot answer two reads\n // differently. Measured on both doors: the caller's container is enumerated\n // once at registration and ZERO times on the frame.\n // `config-aliasing-authority-1958` and `computed-key-write-authority-1852`\n // hold that — dropping either copy reds three cells across them.\n return dropped ? undefined : defaults;\n}\n\n/**\n * Config-time channel check: a route's `defaultParams` may not name a key the\n * route declares with `?`.\n *\n * The static half of \"params and search meet only in the URL\". Without it the\n * router builds a state out of its OWN config that its OWN always-on channel\n * guard then rejects — `start()` throwing `WRONG_CHANNEL` about a bag the user\n * never passed, which is the deferred-crash shape core's invariant guards exist\n * to prevent. The dynamic half (a forwarding hop whose target is only known at\n * resolution) is caught at the `forwardState` seam instead.\n *\n * Runs over the WHOLE config after every rebuild rather than over the routes\n * just added: `setRootPath(\"?lang\")` declares a name on every route at once, so\n * a config that was legal a moment ago can stop being legal without any route\n * changing.\n *\n * Takes `queryNamesOf` as DATA rather than reaching for a matcher, so this\n * subsystem imports nothing from the namespaces or the engine — the same\n * inversion `src/pipeline` makes with its `RouteResolver` port. The caller owns\n * the derivation AND its caches, which matters here: every call site runs on\n * PREPARED artifacts before any swap, with caches local to the attempt, because\n * checking against the live store would validate a tree the rejected batch has\n * not installed. `RoutesNamespace/helpers.assertRouteDefaultChannelsFor` is that\n * caller — a small adapter holding the per-attempt caches, so the four entry\n * points do not each rebuild the closure.\n */\nexport function assertRouteDefaultChannels(\n defaultParams: Readonly<Record<string, Params>>,\n queryNamesOf: (name: string) => readonly string[],\n method: string,\n): void {\n for (const [name, defaults] of objectEntries(defaultParams)) {\n assertChannelCorrect(\n method,\n name,\n defaults,\n queryNamesOf(name),\n \"this route's `defaultParams`\",\n \"Move it to `defaultSearch`\",\n );\n }\n}\n","// packages/core/src/channels/modeGate.ts\n\nimport { EMPTY_SEARCH } from \"../constants\";\nimport { putField } from \"../utils/ingest\";\n\nimport type { SearchParams } from \"../types\";\n\nconst objectEntries = Object.entries;\n\n// ⚑ Captured at module load, for the reason `helpers.ts` states over its own\n// three: a guarantee is only as strong as the intrinsic it reads WHEN IT RUNS,\n// and an application can re-point `Object.freeze` after boot. Measured with the\n// global neutered: this site handed back an UNFROZEN `state.search`, and the\n// cell that pins the capture next door stayed green because its arc gets\n// `search` from the channel merge, whose freeze was already captured (#1928\n// walked the level above and found this one).\nconst freeze = Object.freeze;\n\n/**\n * The mode gate (#1575): the query channel restricted to what the active\n * `queryParamsMode` will actually PRINT.\n *\n * Under `loose` the build prints undeclared keys too, so the whole bag is\n * admitted and the caller skips this entirely (`admitsUndeclaredQuery()`).\n * Under `default` / `strict` the build prints declared names only — so a key\n * that survives into `state.search` here can never appear in `state.path`, and\n * the two channels of one state disagree. Filtering at the merge, on BOTH\n * directions, is what buys the invariant `keys(state.search) ⊆\n * keys(matchPath(state.path).search)` in every mode.\n *\n * A DROP, not a move: the key does not migrate to `state.params` (that would\n * re-create the channel ambiguity #1553 is about) — it simply is not state.\n * `validation-plugin` reports the drop; bare core is silent by the same\n * always-on-fixes / opt-in-diagnoses split as the channel guard.\n *\n * Returns the input bag unchanged when nothing is dropped, so the common case\n * (a route whose query keys are all declared) allocates nothing.\n *\n * ⚑ The write goes through `putField` (#1852), and neither argument for\n * omitting a guard holds — worth recording, because each is plausible and\n * neither names the hazard.\n *\n * Reachability (\"no foreign input can get here\") is refuted directly: the\n * upstream copy it trusts has a hole, and through that hole this line is\n * reached. Ownership (\"every bag this gate is handed is one core BUILT\")\n * survives that refutation and is true —\n * `pipeline/canonicalize` passes the output of `mergeQueryChannel`, and every one\n * of its exits is core's own object. What ownership does NOT survive is the\n * ambient prototype: whose bag the SOURCE is says nothing about what\n * `Object.prototype` carries under the name being written, and the accumulator\n * here is a plain `{}` whatever the source was. Measured on a `?page` route with\n * an ambient accessor: `navigate` rejected with a `TypeError` from this line.\n *\n * ⚠ So the general lesson stands, one level deeper than the obvious reading. A\n * claim about who OWNS the source object survives a hole upstream where a claim about\n * REACH does not — but neither one licenses a plain store under a key the author\n * did not choose, because the destination's chain is the third party to the\n * argument and belongs to the application.\n *\n * @internal\n */\nexport function admittedSearch<S extends SearchParams>(\n search: S,\n queryNames: readonly string[],\n onDropped?: (key: string) => void,\n): S {\n let admitted: Record<string, unknown> | undefined;\n let dropped = false;\n\n // `Object.entries` (own enumerable only) rather than `for…in` + `Object.hasOwn`\n // — the same idiom the deleted `separateChannels` used, and it keeps the guard branch\n // out of the file instead of leaving one no test can reach.\n for (const [key, value] of objectEntries(search)) {\n if (queryNames.includes(key)) {\n admitted ??= {};\n // ⚑ The key is one the ROUTE declares with `?`, so it is exactly the kind\n // of ordinary name an application puts on `Object.prototype` (#1852).\n // Measured on a plain `?page` route with an ambient accessor: `navigate`\n // rejected with `TypeError: Cannot set property page …` from this line.\n putField(admitted, key, value);\n } else {\n dropped = true;\n // The drop is silent in bare core; `validation-plugin` passes a reporter.\n // Reported from HERE rather than re-derived by the caller so the message\n // can never disagree with what was actually dropped, and so the scan\n // happens once. The callback is only ever supplied when a validator is\n // installed, so the default path stays a plain filter.\n onDropped?.(key);\n }\n }\n\n if (!dropped) {\n return search;\n }\n\n // Frozen, because this is the ONLY branch that hands back a bag the caller did\n // not already freeze: `search` arrives frozen from `mergeQueryChannel`, and the\n // no-drop branch returns it untouched. Before nav-pipeline Phase 2 the gap was\n // invisible — every consumer re-merged (and re-froze) downstream in the\n // then-separate `makeState`. `materialize` deliberately does not, so an\n // unfrozen `admitted` reached `state.search` verbatim and broke \"states are\n // deeply frozen\" for exactly the states the gate had touched. Phase 4 folded\n // `makeState` onto `canonicalize` + `materialize`, so there is no re-merge\n // left anywhere: this freeze is now the only one on the drop path.\n return freeze(admitted ?? EMPTY_SEARCH) as S;\n}\n","/**\n * Tree Building.\n *\n * Constructs the mutable tree structure from route definitions.\n *\n * @module builder/buildTree\n */\n\nimport type { RouteDefinition } from \"../types\";\n\n// =============================================================================\n// Mutable Node Type (internal)\n// =============================================================================\n\n/**\n * Mutable version of RouteTree used during building.\n * After building, this is converted to immutable RouteTree.\n */\nexport interface MutableRouteNode {\n name: string;\n path: string;\n absolute: boolean;\n children: MutableRouteNode[];\n parent: MutableRouteNode | null;\n}\n\n// =============================================================================\n// Building Functions\n// =============================================================================\n\n/**\n * Creates a mutable node from a route definition.\n *\n * @param definition - Route definition object\n * @param definition.name - Route name\n * @param definition.path - Route path\n * @param definition.children - Optional child routes\n * @param parent - Parent node (null for root)\n * @returns Mutable node\n */\nfunction createNode(\n definition: {\n name: string;\n path: string;\n children?: RouteDefinition[] | undefined;\n },\n parent: MutableRouteNode | null,\n): MutableRouteNode {\n const path = definition.path;\n const absolute = path.startsWith(\"~\");\n const stripped = absolute ? path.slice(1) : path;\n // #1407: normalize a missing leading \"/\". The trie, buildFullPath, and every\n // downstream layer assume a leading-\"/\" path; a non-absolute path without one\n // (`foo`, `:id`) fuses onto its parent across the segment boundary, and an\n // absolute `~foo` (slash-less) compiles a dead route (the trie scans from\n // index 1, dropping the first char). After the `~`-strip, prepend \"/\" to any\n // non-empty path that lacks it — `foo`/`~foo` → `/foo`, `:id` → `/:id`,\n // `foo?q` → `/foo?q` — leaving `/foo`/`~/foo` and the empty root untouched.\n // A query-only path (`?q`) has no leading path segment to slash (the `?` starts\n // the query), so it is skipped too. The path-matcher's \"I only see leading-'/'\n // paths\" invariant becomes correct-by-construction.\n const normalizedPath =\n stripped !== \"\" && !stripped.startsWith(\"/\") && !stripped.startsWith(\"?\")\n ? `/${stripped}`\n : stripped;\n\n const node: MutableRouteNode = {\n name: definition.name,\n path: normalizedPath,\n absolute,\n children: [],\n parent,\n };\n\n // Recursively add children\n if (definition.children) {\n for (const childDef of definition.children) {\n const childNode = createNode(childDef, node);\n\n node.children.push(childNode);\n }\n }\n\n return node;\n}\n\n/**\n * Builds the mutable tree structure from route definitions.\n *\n * Simplified single-pass algorithm:\n * - Creates root node\n * - Adds each route as direct child of root\n * - createNode() handles nested children recursively\n *\n * @param rootName - Root node name (typically \"\")\n * @param rootPath - Root node path (typically \"\")\n * @param routes - Route definitions\n * @returns Mutable root node\n */\nexport function buildTree(\n rootName: string,\n rootPath: string,\n routes: readonly RouteDefinition[],\n): MutableRouteNode {\n const root = createNode({ name: rootName, path: rootPath }, null);\n\n for (const route of routes) {\n const node = createNode(route, root);\n\n root.children.push(node);\n }\n\n return root;\n}\n","/**\n * URL Parameter Encoding.\n *\n * Encoding strategies for URL parameters.\n *\n * @module encoding\n */\n\n/**\n * Performance-critical code: string concatenation is 2x faster than template\n * literals/String.raw in V8 hot paths. See benchmarks for details.\n */\n/* eslint-disable prefer-template */\n\nimport type { URLParamsEncodingType } from \"./types\";\n\n// =============================================================================\n// Encoding Constants\n// =============================================================================\n\n/**\n * Optimized regex: only matches characters that ACTUALLY need encoding.\n * Excludes:\n * - A-Za-z0-9: alphanumeric (unreserved per RFC 3986)\n * - -._~: unreserved characters per RFC 3986\n * - !$'()*+,:;|: sub-delimiters we want to preserve\n *\n * Performance: This regex skips alphanumeric characters entirely,\n * avoiding unnecessary encodeURIComponent calls that return the same value.\n *\n * IMPORTANT: The 'u' (Unicode) flag makes the regex iterate by code point, so a\n * PAIRED surrogate (emoji, outside the BMP) coalesces into one code point that\n * encodeURIComponent accepts. An UNPAIRED (lone) surrogate is itself a single code\n * point that still matches the class and reaches encodeURIComponent, which throws\n * \"URI malformed\" on it — `encodeURIComponentExcludingSubDelims`'s slow path\n * catches that and sanitizes it to U+FFFD via a lone-surrogate regex, keeping\n * buildPath total (#1315).\n */\nconst NEEDS_ENCODING_REGEX = /[^\\w!$'()*+,.:;|~-]/gu;\n\n/**\n * Fast check regex (without global flag) to test if encoding is needed at all.\n * Used for pre-check optimization: if string contains only safe chars, skip encoding.\n *\n * IMPORTANT: The 'u' flag ensures proper handling of emoji/surrogate pairs.\n */\nconst NEEDS_ENCODING_TEST = /[^\\w!$'()*+,.:;|~-]/u;\n\n// =============================================================================\n// Encoding Helper Functions\n// =============================================================================\n\n/**\n * A lone (unpaired) surrogate — a high surrogate not followed by a low, or a low\n * not preceded by a high. A manual, lib-target-agnostic `String.prototype.toWellFormed`\n * (ES2024): consumers compile this `src` under their own `tsconfig` (whose `lib` may\n * predate es2024 — e.g. `hash-plugin`), so a regex `replace` avoids a `toWellFormed`\n * type error there while producing the identical result.\n */\nconst LONE_SURROGATE_RGX =\n /[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/g;\n\n/**\n * Wraps an encoder so a lone (unpaired) surrogate — the only input\n * `encodeURIComponent` / `encodeURI` reject (`URIError`) — is sanitized to U+FFFD\n * and re-encoded instead of throwing, keeping `buildPath` total (#1315). The\n * surrogate is already non-round-trippable garbage. `path-matcher` has zero deps, so\n * this mirrors search-params' `safeEncode` rather than importing it (a deliberate\n * twin, like the `getTypeDescription` copy in route-tree).\n */\nconst totalize =\n (encoder: (s: string) => string) =>\n (segment: string): string => {\n try {\n return encoder(segment);\n } catch {\n return encoder(segment.replaceAll(LONE_SURROGATE_RGX, \"�\"));\n }\n };\n\n// Only the slow path can throw a `URIError` — a lone surrogate always matches\n// `NEEDS_ENCODING_REGEX`, so it never reaches the all-safe fast path — hence the\n// try/catch sits here and the 29-57x fast path below pays nothing for it.\nconst encodeSlowPath = totalize((segment: string): string =>\n segment.replaceAll(NEEDS_ENCODING_REGEX, (match) =>\n encodeURIComponent(match),\n ),\n);\n\n/**\n * Encode a segment while preserving sub-delimiters.\n * Uses pre-check optimization: fast path for strings without special chars.\n *\n * Performance improvement over naive approach:\n * - Alphanumeric strings: 29-57x faster\n * - Strings with special chars: 2-3x faster\n */\nexport const encodeURIComponentExcludingSubDelims = (\n segment: string,\n): string => {\n // Fast path: if no special chars, return as-is\n // Stryker disable next-line BlockStatement: equivalent — pure optimization; `replaceAll(NEEDS_ENCODING_REGEX, ...)` below is a no-op on a string with no encodable chars, so emptying this early return yields the identical value. ConditionalExpression stays live (killable `->true` sibling returns unencoded strings).\n if (!NEEDS_ENCODING_TEST.test(segment)) {\n return segment;\n }\n\n return encodeSlowPath(segment);\n};\n\n// =============================================================================\n// Encoding Methods Map\n// =============================================================================\n\n/**\n * Encoding functions for each encoding type.\n *\n * Modes:\n * - `default` - encodeURIComponent preserving sub-delimiters (+, :, ', !, ,, ;, *)\n * - `uri` - encodeURI/decodeURI\n * - `uriComponent` - encodeURIComponent/decodeURIComponent\n * - `none` - no encoding/decoding\n */\nexport const ENCODING_METHODS: Record<\n URLParamsEncodingType,\n (param: string) => string\n> = {\n default: encodeURIComponentExcludingSubDelims,\n uri: totalize(encodeURI),\n uriComponent: totalize(encodeURIComponent),\n none: (val) => val,\n};\n\n/**\n * Decoding functions for each encoding type.\n */\nexport const DECODING_METHODS: Record<\n URLParamsEncodingType,\n (param: string) => string\n> = {\n default: decodeURIComponent,\n uri: decodeURI,\n uriComponent: decodeURIComponent,\n // `none` decoding is never reached through `match`: `SegmentMatcher` special-cases\n // `urlParamsEncoding === \"none\"` to `#decode = null` and skips `#decodeParams`\n // entirely (so a \"none\" route also skips %-validation — a deliberate behaviour, not\n // merely perf). This identity entry exists only for `Record` type completeness and is\n // exercised by the exempt `tests/property/encoding.properties.ts` round-trip.\n /* v8 ignore next -- unreachable via match (none → null); see the comment above */\n none: (val) => val,\n};\n\n// =============================================================================\n// Main Encoding Function\n// =============================================================================\n\n/**\n * Encodes a SPLAT URL parameter value: each `/`-delimited segment is encoded with the\n * strategy's encoder, preserving the `/` separators.\n *\n * Splat-only by design (#860): a NON-splat param is encoded by `ENCODING_METHODS[encoding]`\n * directly — `registration/buildParts.ts`'s `makeBuildParamSlot` routes only SPLAT slots\n * through here — so an `!isSpatParam` fast path here would be unreachable.\n *\n * @param param - The splat parameter value to encode\n * @param encoding - The encoding strategy to use\n * @returns The encoded splat value (each segment encoded, `/` preserved)\n *\n * @example\n * ```typescript\n * encodeParam('docs/readme.md', 'default'); // => 'docs/readme.md'\n * encodeParam('a/hello world', 'default'); // => 'a/hello%20world'\n * ```\n */\nexport const encodeParam = (\n param: string | number | boolean,\n encoding: URLParamsEncodingType,\n): string => {\n const encoder = ENCODING_METHODS[encoding];\n const str = String(param);\n\n // Encode each \"/\"-segment separately, preserving the separators.\n // H6 optimization: string concatenation is 2x faster than template literals\n const segments = str.split(\"/\");\n let result = encoder(segments[0]);\n\n for (let i = 1; i < segments.length; i++) {\n result += \"/\" + encoder(segments[i]);\n }\n\n return result;\n};\n","import type { SegmentNode } from \"./types\";\n\n// A trie node allocating its own `Object.create(null)` for `staticChildren`\n// pays for a V8 dictionary-mode object from birth (~192 B — own map + backing\n// store, ~3× a plain `{}`), and the leaf-majority (one node per registered\n// route) never gains a static child, so it would hold that empty object purely\n// to answer the match-path `key in node.staticChildren` read. Share ONE frozen\n// empty null-proto sentinel across every fresh node;\n// `processSegment` (registration/trie.ts) copies-on-write — swaps in a fresh\n// mutable null-proto object — before the first real write. The frozen shell\n// fails loud if a write ever skips that guard. Mirrors the #1009 `EMPTY_*`\n// sentinels in `registration/context.ts`.\nexport const EMPTY_STATIC_CHILDREN: Record<string, SegmentNode> = Object.freeze(\n Object.create(null) as Record<string, SegmentNode>,\n);\n\nexport function createSegmentNode(): SegmentNode {\n return {\n staticChildren: EMPTY_STATIC_CHILDREN,\n // Stryker disable next-line BooleanLiteral: equivalent — for a leaf splat node the `!hasChildren` fast path and the `#traverseFrom` fallback both set `params[name] = slice` and return `sn.route`; nodes that gain children overwrite this flag during registration. Proven: forcing `true` keeps the full suite green (it only un-covers the now-unreachable fast path).\n hasChildren: false,\n paramChild: undefined,\n splatChild: undefined,\n route: undefined,\n slashChildRoute: undefined,\n };\n}\n\nexport function normalizeTrailingSlash(path: string): string {\n if (path.length > 1 && path.endsWith(\"/\")) {\n return path.slice(0, -1);\n }\n\n return path;\n}\n\nexport function buildFullPath(parentPath: string, nodePath: string): string {\n // Stryker disable next-line StringLiteral,BlockStatement: equivalent — empty-operand fast path; `parentPath + nodePath` already yields the identical string when `parentPath === \"\"`, so emptying the block or never triggering it (sentinel comparand) changes no result. ConditionalExpression stays live (killable `->true` sibling).\n if (parentPath === \"\") {\n return nodePath;\n }\n\n // Stryker disable next-line StringLiteral,BlockStatement: equivalent — symmetric empty-operand fast path; `parentPath + nodePath` already equals `parentPath` when `nodePath === \"\"`. ConditionalExpression stays live (killable `->true` sibling).\n if (nodePath === \"\") {\n return parentPath;\n }\n\n // ⚑ ONE separator, not two (#2002). Joined naively, a parent path written\n // with a trailing slash gives every child a full path carrying `//`, and the\n // trie registers the route AT that doubled path — so the child builds a URL\n // its own `matchPath` refuses and the natural URL matches nothing:\n //\n // { path: \"/files/list/\", children: [{ path: \"/detail\" }] }\n // buildPath(\"p.c\") -> \"/files/list//detail\" (unmatchable)\n // matchPath(\"/files/list/detail\") -> undefined\n //\n // Ordinary nesting, no splat and no index involved — those were how it was\n // found (#1996's sibling), not its subject.\n //\n // ⚠ Repairing `isSlashChild` instead was measured and does NOT close it: that\n // predicate is not consulted for a non-index child, so ordinary children stay\n // broken. Collapsing here is the only candidate that fixes both.\n // ⚠ The second term is UNKILLABLE today and stays anyway, which is the\n // opposite of an equivalent mutant. `createNode` normalises a node path to a\n // leading `/` before this runs (#1407), so dropping the term leaves the whole\n // suite green — measured. It is load-bearing all the same: `slice(1)` assumes\n // the first character IS the separator, so without the term a slash-less\n // `nodePath` would lose its first character instead. It guards the hazard, not\n // a reachable branch.\n if (parentPath.endsWith(\"/\") && nodePath.startsWith(\"/\")) {\n return parentPath + nodePath.slice(1);\n }\n\n return parentPath + nodePath;\n}\n","function isHexCodePoint(code: number): boolean {\n return (\n (code >= 0x30 && code <= 0x39) ||\n (code >= 0x41 && code <= 0x46) ||\n (code >= 0x61 && code <= 0x66)\n );\n}\n\nexport function validatePercentEncoding(value: string): boolean {\n let i = 0;\n\n while (i < value.length) {\n if (value.codePointAt(i) === 0x25 /* % */) {\n // Stryker disable next-line ArithmeticOperator,BlockStatement: equivalent — a truncated \"%\" is independently rejected downstream by the `?? 0` fallback (codePointAt past end -> 0, never a hex code point), so weakening (`i - 2`) or emptying this early-exit changes no observable result. Proven: removing the whole block keeps the full unit+property+stress suite green. The ConditionalExpression/EqualityOperator variants here stay live (they have killable siblings).\n if (i + 2 >= value.length) {\n return false;\n }\n\n /* v8 ignore start -- @preserve: codePointAt cannot return undefined due to bounds check above */\n const hex1 = value.codePointAt(i + 1) ?? 0;\n const hex2 = value.codePointAt(i + 2) ?? 0;\n /* v8 ignore stop */\n\n if (!isHexCodePoint(hex1) || !isHexCodePoint(hex2)) {\n return false;\n }\n\n i += 3;\n } else {\n i++;\n }\n }\n\n return true;\n}\n","// Shared registration context: the `RegistrationState` interface and the frozen\n// empty-value sentinels (#1009). Leaf module (no sibling imports) so\n// `trie`/`trieNodes`/`buildParts` can depend on `RegistrationState` without\n// cycling back to the orchestrator.\n\nimport type {\n BuildParamSlot,\n CompiledRoute,\n ResolvedMatcherOptions,\n SegmentNode,\n} from \"../types\";\n\n// Shared frozen sentinels for the no-params common case — avoid a fresh empty\n// Set/array per route (#1009). All are ReadonlySet/[] and read-only on the\n// match/build hot paths.\nexport const EMPTY_STRINGS: readonly string[] = Object.freeze([]);\n\n// #1240 §5: freeze the Set shell too, so the \"Shared frozen sentinels\" claim\n// above holds for ALL of them and the #1009 sentinels are consistent with route-tree's\n// frozen `EMPTY_CHILDREN_MAP`. `Object.freeze` locks only the shell (not `.add`\n// — see route-tree INVARIANTS CC1), but these are `Readonly`-typed and never mutated.\nexport const EMPTY_STRING_SET: ReadonlySet<string> = Object.freeze(\n new Set<string>(),\n);\n\nexport const EMPTY_PARAM_SLOTS: readonly BuildParamSlot[] = Object.freeze([]);\n\nexport const EMPTY_PARAMS: Readonly<Record<string, unknown>> = Object.freeze(\n {},\n);\n\n/** Frozen empty query bag for precomputed query-less cached results (RFC-4 M2). */\nexport const EMPTY_SEARCH: Readonly<Record<string, unknown>> = Object.freeze(\n {},\n);\n\n// Shared frozen sentinel for a route whose every segment has an empty\n// paramTypeMap (all-static chain): `buildMeta` returns this instead of a fresh\n// per-route `{ [fullName]: {} }` record — with N distinct route names those\n// records degrade into N dictionary-mode objects (route-unique keys) while\n// carrying zero information. Consumers do keyed lookups (`meta[name]`) and\n// treat a missing entry as \"no params\", so empty-entry ≡ missing-entry; the\n// sentinel stays truthy for the route-found check in core's buildNavigateState.\nexport const EMPTY_ROUTE_META: Readonly<\n Record<string, Record<string, \"url\" | \"query\">>\n> = Object.freeze({});\n\nexport interface RegistrationState {\n readonly root: SegmentNode;\n readonly options: ResolvedMatcherOptions;\n readonly routesByName: Map<string, CompiledRoute>;\n readonly staticCache: Map<string, CompiledRoute>;\n readonly rootQueryParams: readonly string[];\n // The root node is deliberately absent from `matchSegments` (see SegmentMatcher\n // `registerTree`), so anything derived from a segment walk is root-blind. Its\n // QUERY declarations already had to be threaded separately (#1556); its PATH\n // slots need the same treatment for the build side (#1567).\n readonly rootUrlParams: readonly string[];\n}\n","// Build-path template compilation: turns a (constraint-stripped) route path into\n// `buildStaticParts` + `buildParamSlots` through the shared `parseSegment` tokenizer (Decision 1).\n\nimport { encodeParam, ENCODING_METHODS } from \"../encoding\";\nimport {\n parseSegment,\n splitPathSegments,\n type SegmentTokens,\n} from \"../parseSegment\";\nimport { EMPTY_PARAM_SLOTS } from \"./context\";\n\nimport type {\n BuildParamSlot,\n MatcherInputNode,\n URLParamsEncodingType,\n} from \"../types\";\n\n/**\n * Builds one `BuildParamSlot` from a param/splat token. The encoder is the single\n * `encodeParam` implementation the encoding unit/property suites assert (the splat\n * variant encodes each segment individually, preserving `/`), so prod and the\n * oracle can't drift (#860).\n *\n * The kind comes from the TOKEN the caller already narrowed, never from a set of\n * splat NAMES gathered across the ancestor chain (#1975). The two are not the\n * same question: the finality rule below drops a non-final splat before it can\n * become a slot, and a name set does not know about finality — so under a name\n * set a child's `:x` beneath a parent's `/*x` took the splat encoder, printed\n * its `/` raw, and `buildPath` emitted a URL the same matcher resolved to the\n * PARENT with the tail glued into the value. Reading the token asks the one\n * question a slot's encoder depends on, and cannot drift from the finality rule.\n *\n * ⚠ Observable in TWO of the four encodings, measured: under `uri` and `none`\n * the two encoders are IDENTICAL (`encodeURI` never escapes `/`, so the\n * per-segment split/join is a no-op; `none` is identity both ways), so there is\n * nothing for this choice to change and the mis-built URL persists there. That\n * is a property of those modes rather than of this function — a plain `:param`\n * carrying a `/` does not round-trip under them either — but no claim about\n * this fix holds unconditionally across encodings.\n */\nfunction makeBuildParamSlot(\n token: Extract<SegmentTokens, { kind: \"param\" | \"splat\" }>,\n encoding: URLParamsEncodingType,\n): BuildParamSlot {\n const isSplat = token.kind === \"splat\";\n\n return {\n paramName: token.name,\n encoder: isSplat\n ? (value: string): string => encodeParam(value, encoding)\n : ENCODING_METHODS[encoding],\n };\n}\n\nexport function compileBuildParts(\n normalizedPath: string,\n segments: readonly MatcherInputNode[],\n encoding: URLParamsEncodingType,\n rootUrlParams: readonly string[],\n): {\n buildStaticParts: readonly string[];\n buildParamSlots: readonly BuildParamSlot[];\n} {\n // `normalizedPath` carries the ROOT path as its prefix, but the root node is\n // absent from `segments` — so seeding these from the segment walk alone made a\n // root-declared slot invisible here, and the fast path below emitted the whole\n // path (`:tenant` included) as literal static text (#1567).\n const allUrlParams = new Set<string>(rootUrlParams);\n\n for (const segment of segments) {\n for (const param of segment.paramMeta.urlParams) {\n allUrlParams.add(param);\n }\n }\n\n // Stryker disable next-line BlockStatement: equivalent — fast path; the param-compile loop below yields [normalizedPath]/[] when allUrlParams is empty — identical output. Proven by injection.\n if (allUrlParams.size === 0) {\n return {\n buildStaticParts: [normalizedPath],\n buildParamSlots: EMPTY_PARAM_SLOTS,\n };\n }\n\n const parts: string[] = [];\n const slots: BuildParamSlot[] = [];\n let current = \"\";\n\n // Build the static-parts / param-slots template through the SAME tokenizer the\n // trie (L3) and `buildParamMeta` (L1) consume — `parseSegment`, not a parallel\n // `paramRgx` — so build's param NAME can no longer drift from the trie's\n // (#1050/#1150 build≠match closed structurally, not just by the round-trip\n // property). The path is already constraint-stripped (`matchPath`), so\n // `parseSegment` sees only name/optional; each `/` separator `splitPathSegments`\n // split away is re-added to the running static part.\n const pathSegments = splitPathSegments(normalizedPath);\n const lastIndex = pathSegments.length - 1;\n\n for (const [i, pathSegment] of pathSegments.entries()) {\n const token = parseSegment(pathSegment);\n\n // A splat binds ONLY as the final segment. The trie matches a splat node's\n // children at the splat's OWN position (INVARIANTS Matching #24: `/n/*rest`\n // + `/n/*rest/edit` → `/n/edit` resolves to the child), so a splat with\n // anything after it always captures the empty string. Emitting a slot for it\n // printed a URL `match` could not resolve — or, under a splat parent, one\n // that fell back to the parent's wildcard. It contributes neither text nor\n // separator. (#1568)\n if (!(\"error\" in token) && token.kind === \"splat\" && i !== lastIndex) {\n continue;\n }\n\n if (i > 0) {\n current += \"/\";\n }\n\n if (\"error\" in token || token.kind === \"static\") {\n // Static text — or a malformed segment, whose route is rejected at\n // `registerTree` before these buildParts are ever read (output moot).\n current += pathSegment;\n continue;\n }\n\n // param | splat: close the accumulated static part, emit a slot.\n parts.push(current);\n current = \"\";\n slots.push(makeBuildParamSlot(token, encoding));\n }\n\n parts.push(current);\n\n return { buildStaticParts: parts, buildParamSlots: slots };\n}\n","// Registration error factories: every `throw*` helper the pipeline raises, plus the\n// `throwSegmentGrammarError` code→message dispatcher (Decision 2-A). Pure leaf — each\n// builds a message and throws; none reads trie state.\n\nimport type { SegmentErrorCode } from \"../parseSegment\";\n\n/**\n * Guards against param-name aliasing in the segment trie (issue #736).\n *\n * A parametric (`:name`) or splat (`*name`) position in the trie is keyed by\n * **position**, not by name — but the captured value is written under the name\n * recorded on that position. When two *different* routes share a position under\n * *different* names (e.g. `/user/:id` and `/user/:slug/profile`),\n * first-registration wins the name, so the second route silently captures its\n * value under the wrong key. Through `rewritePathOnMatch` that silent key-swap\n * becomes a hard `start()` crash on a legitimate config — so we reject the\n * ambiguity loudly at registration instead of corrupting matches.\n *\n * The conflict is strictly **cross-route**. Under the 3-token grammar (M1, #1516)\n * insertion is a strict linear walk — a route never revisits a slot it created\n * (the former optional-omit fork, which could land two differently-named params\n * on one position within a single route, is gone). So any name mismatch at a\n * position is unconditionally a prior route's slot: `ensureParamChild` throws on\n * `name !== paramName` with no `ownNodes` exception.\n */\nexport function throwParamNameConflict(\n existingName: string,\n newName: string,\n marker: \":\" | \"*\",\n): never {\n throw new Error(\n `[SegmentMatcher.registerTree] Parameter name conflict at the same path ` +\n `position: '${marker}${existingName}' and '${marker}${newName}'. One ` +\n `position binds one name across every route that shares it. Rename one — ` +\n `e.g. use '${marker}${existingName}' in both.`,\n );\n}\n\n/**\n * A bare marker (`:` or `*` with no name) compiles to a phantom empty-named\n * slot: match captures the value under `\"\"`, buildPath emits the literal marker,\n * and buildParamMeta reports no param at all — a three-way match/build/meta\n * desync of the same class as #736/#738 (#858). Reject it at registration,\n * symmetrically for both markers, instead of corrupting the trie.\n */\nexport function throwEmptyParamName(): never {\n // Marker-agnostic: this fires for a bare ':'/'*' (`/x/:`, `/x/*`), a marker\n // carrying only a modifier char with no name (`/x/:?`, `/x/:<...>`), AND a\n // static segment with a trailing '?' (`/faq?`) — all routed here from\n // `processSegment`'s error backstop (#1998). So the message names both shapes\n // and pins neither to a specific marker — there is none for `/faq?` (#1241).\n throw new Error(\n `[SegmentMatcher.registerTree] Empty parameter name: a ':'/'*' marker must ` +\n `be followed by a name (e.g. ':id', '*rest'), and a segment cannot end in ` +\n `a bare '?'.`,\n );\n}\n\n/**\n * Rejects a `:`/`*` marker fused to a static prefix within a segment (`a:b`,\n * `x:id`, `a*b`, #1050). A marker is honoured only at segment start, so the\n * fused spelling has no reading `parseSegment` will give it.\n *\n * The route-tree gate catches this first with a route-contextual error; this is\n * the standalone registration backstop.\n */\nfunction throwFusedMarker(segment: string): never {\n throw new Error(\n `[SegmentMatcher.registerTree] Fused parameter marker in segment \"${segment}\": ` +\n `a ':'/'*' marker must begin a segment — write 'a/:b', not 'a:b'.`,\n );\n}\n\n/**\n * Rejects a param name ending in a bare marker (`:y*`, `:y:`, #1324). Gate and\n * backstop read the same `parseSegment`, which ends the name before a trailing\n * marker, so neither can admit a spelling the other refuses.\n *\n * The sibling of {@link throwEmptyParamName} (#858) / {@link throwFusedMarker}\n * (#1050) on the trailing-marker axis.\n */\nfunction throwTrailingMarker(segment: string): never {\n throw new Error(\n `[SegmentMatcher.registerTree] Trailing parameter marker in segment \"${segment}\": ` +\n `a param name cannot end in a bare ':' or '*'. Drop the stray marker.`,\n );\n}\n\n/**\n * `optional-removed` (M1): a `:x?`/`*x?` optional modifier. The backstop tier —\n * a short, path-free recipe (the route-tree gate's rich tier computes the two\n * concrete sibling paths). Optional params were dropped for zero corpus use +\n * the axis's largest bug cluster; the hierarchy already expresses optionality.\n */\nfunction throwOptionalRemoved(segment: string): never {\n throw new Error(\n `[SegmentMatcher.registerTree] Optional params are not supported: \"${segment}\" — ` +\n `declare two sibling routes instead, one with the segment and one without.`,\n );\n}\n\n/**\n * `constraint-removed` (M1): a `<re>` constraint or a stray `<`/`>`. The backstop\n * tier — a short recipe (the gate's rich tier names the offending segment). Regex\n * constraints were dropped; validate the value in a guard instead.\n */\nfunction throwConstraintRemoved(segment: string): never {\n throw new Error(\n `[SegmentMatcher.registerTree] Regex constraints are not supported: '<' and ` +\n `'>' are reserved in path segments (\"${segment}\"). Match it as a plain ` +\n `string and validate the value in a canActivate guard.`,\n );\n}\n\nexport function throwNonAsciiStatic(segment: string): never {\n throw new Error(\n `[SegmentMatcher.registerTree] Non-ASCII static segment \"${segment}\": match ` +\n `compares static keys raw and rejects non-ASCII input, so this route can ` +\n `never match. Percent-encode it (e.g. \"/caf%C3%A9\") or use a param.`,\n );\n}\n\n/**\n * Dispatches a `parseSegment` grammar-error code (the per-segment backstop) to the\n * matching matcher-level throw — the single place mapping the tokenizer's verdict\n * onto the message, so the reject reason stays byte-identical per code. The two\n * removed-form codes (M1) route to their short recipe throws; the route-tree gate\n * catches the same forms first with its richer route-contextual recipe.\n */\nexport function throwSegmentGrammarError(\n code: SegmentErrorCode,\n segment: string,\n): never {\n switch (code) {\n case \"name-less\": {\n return throwEmptyParamName();\n }\n case \"trailing-marker\": {\n return throwTrailingMarker(segment);\n }\n case \"fused-marker\": {\n return throwFusedMarker(segment);\n }\n case \"optional-removed\": {\n return throwOptionalRemoved(segment);\n }\n case \"constraint-removed\": {\n return throwConstraintRemoved(segment);\n }\n }\n}\n\nexport function throwDuplicateParamName(\n routeName: string,\n names: readonly string[],\n): never {\n const seen = new Set<string>();\n let duplicate = \"\";\n\n for (const name of names) {\n if (seen.has(name)) {\n duplicate = name;\n\n break;\n }\n\n seen.add(name);\n }\n\n // ⚠ No ':' prefix: the caller counts params AND splats, so a `/:x/*x` clash\n // arrives here with one position spelled `*x` (#1151).\n throw new Error(\n `[SegmentMatcher.registerTree] Duplicate parameter name '${duplicate}' in ` +\n `route \"${routeName}\": a name must be unique within a route — the second ` +\n `position overwrites the first. Rename one.`,\n );\n}\n\nexport function throwInvalidQueryParamName(\n routeName: string,\n name: string,\n): never {\n throw new Error(\n `[SegmentMatcher.registerTree] Invalid query-param declaration \"${name}\" in ` +\n `route \"${routeName}\": a query-param name cannot contain '<' or '>'. ` +\n `Rename it.`,\n );\n}\n\n/**\n * A `//` inside a declared path (#2010).\n *\n * ⚑ Same family as {@link throwNonAsciiStatic}: the route registers, `buildPath`\n * prints the path it was declared with, and the matcher then refuses to match\n * it — a dead route. Refused here rather than in the route-tree gate, which is\n * plugin-only and whose reject recipes deliberately stay out of the main chunk\n * (#1526).\n */\nexport function throwDoubleSlashInPath(path: string): never {\n throw new Error(\n `[SegmentMatcher.registerTree] Double slashes are not allowed in path ` +\n `\"${path}\": the route would build a URL its own matcher refuses. ` +\n `Remove the empty segment.`,\n );\n}\n\nexport function throwDuplicateRoutePath(\n existingName: string,\n newName: string,\n): never {\n throw new Error(\n `[SegmentMatcher.registerTree] Duplicate route path: routes \"${existingName}\" ` +\n `and \"${newName}\" resolve to the same URL — the later would shadow the ` +\n `earlier. Give them distinct paths.`,\n );\n}\n\n/**\n * ⚠ SPLAT parents only. An index under a `:param` parent is legal and registers\n * — the name says `Splat` because the rule does.\n */\nexport function throwIndexUnderSplatParent(\n routeName: string,\n parentPath: string,\n): never {\n throw new Error(\n `[SegmentMatcher.registerTree] Index route \"${routeName}\" (path \"/\") under the ` +\n `splat parent \"${parentPath}\" is unreachable: the wildcard match never ` +\n `reaches the index node. Give the index a distinct path, or make the ` +\n `parent static.`,\n );\n}\n","// Trie node construction: the param/splat child creators (#736). Consumed by\n// `trie` during insertion, which reads the segment TOKEN from `parseSegment`\n// itself (#1998) rather than through a name-extracting wrapper here.\n\nimport { createSegmentNode } from \"../pathUtils\";\nimport { throwParamNameConflict } from \"./errors\";\n\nimport type { SegmentNode } from \"../types\";\n\n/**\n * Returns the param child of `node`, creating it on first use. A pre-existing\n * child with a *different* name is a #736 conflict (two routes binding the same\n * trie position under different names). With the 3-token grammar (M1) insertion is\n * a strict linear walk, so a single route never revisits a slot it created — the\n * former optional-omit `ownNodes` exception is gone with optional params.\n */\nexport function ensureParamChild(\n node: SegmentNode,\n paramName: string,\n): SegmentNode {\n if (!node.paramChild) {\n node.paramChild = { node: createSegmentNode(), name: paramName };\n } else if (node.paramChild.name !== paramName) {\n throwParamNameConflict(node.paramChild.name, paramName, \":\");\n }\n\n return node.paramChild.node;\n}\n\n/** Splat counterpart of {@link ensureParamChild}. */\nexport function ensureSplatChild(\n node: SegmentNode,\n splatName: string,\n): SegmentNode {\n if (!node.splatChild) {\n node.splatChild = { node: createSegmentNode(), name: splatName };\n } else if (node.splatChild.name !== splatName) {\n throwParamNameConflict(node.splatChild.name, splatName, \"*\");\n }\n\n return node.splatChild.node;\n}\n","// Trie insertion + walking: the recursive `insertIntoTrieFrom` linear walk, slash-child\n// insertion, per-segment `processSegment`, and the `walkTrie` lookups. Builds the\n// segment trie from the node builders in `./trieNodes`.\n\nimport { parseSegment } from \"../parseSegment\";\nimport {\n createSegmentNode,\n EMPTY_STATIC_CHILDREN,\n normalizeTrailingSlash,\n} from \"../pathUtils\";\nimport {\n throwDuplicateRoutePath,\n throwEmptyParamName,\n throwNonAsciiStatic,\n throwIndexUnderSplatParent,\n} from \"./errors\";\nimport { ensureParamChild, ensureSplatChild } from \"./trieNodes\";\n\nimport type { CompiledRoute, SegmentNode } from \"../types\";\nimport type { RegistrationState } from \"./context\";\n\n/** Captured like the deciding seven, but this one BUILDS the guarantee (#2072). */\nconst objectCreate = Object.create;\n\n/** `/` — the trailing-slash scan in `insertSlashChildIntoTrie`. */\nconst SLASH = 47;\n\n/**\n * #1153: writes a terminal route, rejecting a second write by a DIFFERENT route —\n * two routes compiling to the same effective path (flat vs nested `/a/b`, or `/x`\n * vs `/x/`), where the later would silently shadow the earlier (its deep link\n * would resolve to the other route). A revisit by the SAME route is idempotent.\n * (With the 3-token grammar every terminal write is a full insertion — the former\n * WEAK optional-omit `??=` writes are gone with optional params.)\n */\nfunction writeTerminalRoute(node: SegmentNode, compiled: CompiledRoute): void {\n if (node.route !== undefined && node.route !== compiled) {\n throwDuplicateRoutePath(node.route.name, compiled.name);\n }\n\n node.route = compiled;\n}\n\n/**\n * #1154: whether a STATIC segment carries a code point outside ASCII (≥ U+0080).\n * A raw non-ASCII static (`café`) registers but never matches — match rejects\n * non-ASCII input and compares static keys raw. A per-code-point scan (`for…of`\n * iterates by code point, so surrogate pairs are handled).\n */\nfunction hasNonAsciiSegment(segment: string): boolean {\n // #1285: charCodeAt (code UNIT) index loop, not for-of code points. For a\n // \"has non-ASCII\" predicate the result is identical — any surrogate (≥ 0xD800) is\n // itself ≥ 0x80, so an astral char is still flagged — without the iterator +\n // code-point decoding cost per static segment of every registered route.\n for (let i = 0; i < segment.length; i++) {\n // eslint-disable-next-line unicorn/prefer-code-point -- charCodeAt (code unit) is intentional: a \"has non-ASCII\" test needs only units (a surrogate is itself >= 0x80), and it skips the code-point decoding that codePointAt does per index (#1285)\n if (segment.charCodeAt(i) >= 0x80) {\n return true;\n }\n }\n\n return false;\n}\n\nexport function insertIntoTrie(\n state: RegistrationState,\n compiled: CompiledRoute,\n fullPath: string,\n): void {\n const normalized = normalizeTrailingSlash(fullPath);\n\n if (normalized === \"/\") {\n writeTerminalRoute(state.root, compiled);\n\n return;\n }\n\n insertIntoTrieFrom(state, state.root, normalized, 1, compiled);\n}\n\nfunction insertIntoTrieFrom(\n state: RegistrationState,\n node: SegmentNode,\n path: string,\n start: number,\n compiled: CompiledRoute,\n): void {\n const length = path.length;\n\n // 3-token grammar (M1): every segment is `static | :param | *splat` — a single\n // linear walk down the trie (no optional take/skip fork, so no `visited` memo,\n // and — since a route never revisits a slot it created — no #736 `ownNodes` set).\n while (start <= length) {\n const end = path.indexOf(\"/\", start);\n const segmentEnd = end === -1 ? length : end;\n const segment = path.slice(start, segmentEnd);\n\n node = processSegment(state, node, segment);\n start = segmentEnd + 1;\n }\n\n writeTerminalRoute(node, compiled);\n}\n\nexport function insertSlashChildIntoTrie(\n state: RegistrationState,\n compiled: CompiledRoute,\n parentPath: string,\n): void {\n // #1242 §5.4: an index route (path \"/\") under a parent whose path ends in a\n // SPLAT is unreachable — `slashChildRoute` sits on the splat node, which\n // `#matchSplat`'s fast path never reads. A REQUIRED-param parent (`/users/:id`,\n // `/a/:b/c`) has a single form and its slash-child is coherent (existing\n // behaviour) — allowed. (The former OPTIONAL-param arm, #1294, is gone with\n // optional params — M1.)\n // ⚑ NORMALISED, and that is the fix rather than a tidy-up (#1996). The guard\n // must read the same string the walk walks: `walkTrieFrom` below normalises\n // the trailing slash, and `registerSlashChild` normalises again one line after\n // calling us, for the cache key. Reading the RAW path made this the one\n // consumer of three that did not — and for `\"/files/*rest/\"` the slice yields\n // `\"\"`, so the guard fell silent, the route registered, and the root splat\n // became the FINAL segment of the build path (a build slot the finality rule\n // in `buildParts.ts` would otherwise have dropped). Measured on the tree that\n // then registered: `buildPath` demanded a param the route never declared, and\n // `matchPath` refused the URL that param produced.\n //\n // ⚠ Tokenising the segment instead does NOT close it, measured rather than\n // reasoned: `parseSegment(\"\")` answers `{ kind: \"static\" }` and\n // `\"\".startsWith(\"*\")` is `false`, so the two spellings AGREE here. They part\n // only on a malformed splat (`*`, `*y:`), which the grammar pass refuses\n // before this guard is reached.\n // ⚠ EVERY trailing slash, not one: `normalizeTrailingSlash` strips exactly\n // one, so `\"/app/*rest//\"` needs the loop rather than a single strip.\n // ⚑ That doubled tail is not CONSTRUCTIBLE — the\n // matcher backstop refuses a `//` in a declared path, measured through both\n // doors (`createRouter` and `setRootPath`); a single `*rest/` still\n // registers, which is the shape this guard is really for. The loop stays as\n // a backstop rather than being narrowed to one slash: a path ending in\n // `*rest//` still ENDS IN A SPLAT, which is the only question asked here.\n //\n // ⚑ The `> 1` floor mirrors `normalizeTrailingSlash`'s own, and it is NOT\n // verdict-bearing here — measured, `> 0` leaves the whole suite green, because\n // a path of nothing but slashes yields an empty last segment either way and an\n // empty segment is not a splat. It stays because stopping at index 1 is what\n // the sibling helper does, not because a test would catch its removal.\n let end = parentPath.length;\n\n while (end > 1 && parentPath.codePointAt(end - 1) === SLASH) {\n end -= 1;\n }\n\n const normalizedParent = parentPath.slice(0, end);\n const lastSegment = normalizedParent.slice(\n normalizedParent.lastIndexOf(\"/\") + 1,\n );\n\n if (lastSegment.startsWith(\"*\")) {\n // The message keeps the caller's own spelling — that is what they wrote.\n throwIndexUnderSplatParent(compiled.name, parentPath);\n }\n\n const node = walkTrie(state, parentPath);\n\n node.slashChildRoute = compiled;\n}\n\nfunction walkTrie(state: RegistrationState, fullPath: string): SegmentNode {\n return walkTrieFrom(state, state.root, fullPath);\n}\n\nfunction walkTrieFrom(\n state: RegistrationState,\n startNode: SegmentNode,\n path: string,\n): SegmentNode {\n const normalized = normalizeTrailingSlash(path);\n\n /* v8 ignore start -- defensive: slash-child always passes valid path */\n if (normalized === \"/\" || normalized === \"\") {\n return startNode;\n }\n /* v8 ignore stop */\n\n let node = startNode;\n let start = 1;\n const length = normalized.length;\n\n // Slash-child re-walks an already-inserted path of the same route family —\n // names always match, so the conflict guard never fires.\n\n while (start <= length) {\n const end = normalized.indexOf(\"/\", start);\n const segmentEnd = end === -1 ? length : end;\n\n /* v8 ignore start -- defensive: indexOf always returns valid index for non-empty segments */\n if (segmentEnd <= start) {\n break;\n }\n /* v8 ignore stop */\n\n const segment = normalized.slice(start, segmentEnd);\n\n node = processSegment(state, node, segment);\n start = segmentEnd + 1;\n }\n\n return node;\n}\n\nfunction processSegment(\n state: RegistrationState,\n node: SegmentNode,\n segment: string,\n): SegmentNode {\n // ⚑ The TOKENIZER decides what this segment is, not its leading character\n // (#1998). This was the last site in `path-matcher` where \"is it a splat\"\n // was spelled twice — and the class had already produced two measured\n // defects: #1975 (`makeBuildParamSlot` derived splat-ness from a set of\n // NAMES, which the finality rule filtered differently — a silent wrong URL)\n // and #1996 one function above (the marker read off a sliced raw path, which\n // a trailing slash defeated).\n //\n // ⚑ It also removes a parse rather than adding one. The name-extracting\n // wrapper this replaces called `parseSegment` a SECOND time on a segment whose\n // kind `startsWith` had just decided; asking the tokenizer once answers both.\n // Measured on registration of a 60×4 tree — 0.586 / 0.575 ms against a\n // 0.591 / 0.614 ms baseline, inside the A/A spread. A static segment is parsed\n // where it was not, and it does not show.\n const token = parseSegment(segment);\n\n // `registerNode`'s per-segment grammar pass rejects every malformed segment\n // before trie insertion, so only `static | :param | *splat` reach here. Kept\n // as a typed backstop — the wrapper's own guard, inlined with it, minus its\n // `static` arm: static is a legitimate branch below rather than an error, once\n // the kind is ASKED instead of assumed from a leading character.\n /* v8 ignore start -- unreachable: registerNode's grammar pass rejects non-name segments first */\n if (\"error\" in token) {\n throwEmptyParamName();\n }\n /* v8 ignore stop */\n\n if (token.kind === \"splat\") {\n const child = ensureSplatChild(node, token.name);\n\n // Stryker disable next-line BooleanLiteral: equivalent — sets hasChildren on the node ACQUIRING a splat child; only a splat NODE's own hasChildren is read (in #matchSplat), and splat-of-splat is unreachable (splat is terminal-greedy). Proven by injection.\n node.hasChildren = true;\n\n return child;\n }\n\n if (token.kind === \"param\") {\n const child = ensureParamChild(node, token.name);\n\n node.hasChildren = true;\n\n return child;\n }\n\n // The segment does not start with a marker, so it compiles as a static literal.\n // A `:`/`*` fused to a static prefix within it (`a:b`, `x:id`, `a*b`, #1050) was\n // already rejected by the per-segment grammar pass in `registerNode` — a\n // fused-marker segment never reaches this literal compilation.\n\n // #1154: a raw non-ASCII code point in a STATIC segment (`/café`, `/меню`).\n // match rejects any input byte ≥ 0x80 (`#scanPath`) AND compares static trie\n // keys raw (never percent-decoded), so such a route registers but is\n // unmatchable — `buildPath` emits `/café`, which its own `match` rejects (a dead\n // route). Reject at registration with the percent-encode workaround. A non-ASCII\n // PARAM name or constraint is unaffected (only static text is compared raw).\n if (hasNonAsciiSegment(segment)) {\n throwNonAsciiStatic(segment);\n }\n\n const key = state.options.caseSensitive ? segment : segment.toLowerCase();\n\n if (!(key in node.staticChildren)) {\n // Copy-on-write off the shared frozen EMPTY_STATIC_CHILDREN sentinel: the\n // first static child this node gains earns it a fresh mutable null-proto map.\n if (node.staticChildren === EMPTY_STATIC_CHILDREN) {\n node.staticChildren = objectCreate(null) as Record<string, SegmentNode>;\n }\n\n node.staticChildren[key] = createSegmentNode();\n node.hasChildren = true;\n }\n\n return node.staticChildren[key];\n}\n","// Registration entry + orchestration: `registerNode` (the one public entry, re-exported\n// for `SegmentMatcher`) runs the per-node grammar pre-pass (Decision 2-A), compile, and trie\n// insertion, plus the route-meta / query-and-constraint collection helpers.\n// Concerns split into ./context ./errors ./trieNodes ./trie ./buildParts.\n\nimport { EMPTY_PARAM_META, INVALID_QUERY_NAME_RGX } from \"../buildParamMeta\";\nimport { parseSegment, splitPathSegments } from \"../parseSegment\";\nimport { buildFullPath, normalizeTrailingSlash } from \"../pathUtils\";\nimport { compileBuildParts } from \"./buildParts\";\nimport {\n EMPTY_PARAMS,\n EMPTY_SEARCH,\n EMPTY_ROUTE_META,\n EMPTY_STRINGS,\n EMPTY_STRING_SET,\n type RegistrationState,\n} from \"./context\";\nimport {\n throwDuplicateParamName,\n throwInvalidQueryParamName,\n throwDoubleSlashInPath,\n throwSegmentGrammarError,\n} from \"./errors\";\nimport { insertIntoTrie, insertSlashChildIntoTrie } from \"./trie\";\nimport {\n concealUnsafeKey,\n emptyRecord,\n publishRecord,\n} from \"../../../utils/ingest\";\n\nimport type { CompiledRoute, MatcherInputNode } from \"../types\";\n\n/** Captured like the deciding seven, but this one BUILDS the guarantee (#2073). */\nconst freeze = Object.freeze;\n\nexport type { RegistrationState } from \"./context\";\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 hasOwn = Object.hasOwn;\n\n/**\n * The double-slash rule (#2010), over the path as DECLARED.\n *\n * ⚠ Not over `paramMeta.pathPattern`, which the grammar pass below uses: that\n * one is query-stripped, so a `//` inside a query declaration slipped past the\n * backstop while the route-tree gate — which scans the declared string —\n * refused it. Same input, two verdicts, which is the asymmetry #2010 is about.\n */\nfunction assertNoDoubleSlash(declaredPath: string): void {\n const segments = splitPathSegments(declaredPath);\n\n for (const [index, segment] of segments.entries()) {\n // An EMPTY segment is `//` — but only between two others: the leading `/`\n // and a trailing one each produce one legitimately.\n if (segment === \"\" && index > 0 && index < segments.length - 1) {\n throwDoubleSlashInPath(declaredPath);\n }\n }\n}\n\n/**\n * Per-segment grammar backstop: every code `parseSegment` reports, over the\n * query-stripped pattern the tokenizer is written for.\n */\nfunction assertSegmentGrammar(rawNodePath: string): void {\n for (const segment of splitPathSegments(rawNodePath)) {\n const token = parseSegment(segment);\n\n if (\"error\" in token) {\n throwSegmentGrammarError(token.error, segment);\n }\n }\n}\n\nexport function registerNode(\n state: RegistrationState,\n node: MatcherInputNode,\n parentPath: string,\n segments: MatcherInputNode[],\n parentRoute: CompiledRoute | null,\n): void {\n const isRoot = node.fullName === \"\";\n\n if (!isRoot) {\n segments.push(node);\n }\n\n const isAbsolute = node.absolute;\n // The EMPTY_PARAM_META sentinel (fully-static node) carries pathPattern \"\";\n // its real pattern is the node's own path (sentinel is only installed when\n // the two were reference-equal).\n const pathPattern =\n node.paramMeta === EMPTY_PARAM_META\n ? node.path\n : node.paramMeta.pathPattern;\n const strippedPattern =\n isAbsolute && pathPattern.startsWith(\"~\")\n ? pathPattern.slice(1)\n : pathPattern;\n const rawNodePath = isAbsolute ? strippedPattern : pathPattern;\n\n // Per-segment grammar backstop: the trie's own grammar verdict reads the SAME\n // `parseSegment` tokenizer the route-tree gate reads (`findSegmentGrammarError`),\n // so backstop and gate cannot drift on a per-segment form. One pass over the RAW\n // path rejects every rejection form: name-less (#858), fused marker (#1050),\n // trailing marker (#1324), and the removed forms `optional-removed` /\n // `constraint-removed` (M1). So `processSegment` downstream sees only\n // grammatically valid `static | :param | *splat` segments — it asks the same\n // tokenizer for the kind rather than re-testing the leading character (#1998).\n assertNoDoubleSlash(node.path);\n assertSegmentGrammar(rawNodePath);\n\n // 3-token grammar (M1): no `<...>` constraint to strip before trie insertion.\n const nodePath = rawNodePath;\n\n const matchPath = isAbsolute ? nodePath : buildFullPath(parentPath, nodePath);\n\n const compileParentPath = isAbsolute ? \"\" : parentPath;\n\n const currentRoute: CompiledRoute | null = isRoot\n ? parentRoute\n : compileAndRegisterRoute(\n state,\n node,\n matchPath,\n compileParentPath,\n segments,\n parentRoute,\n );\n\n for (const child of node.children.values()) {\n registerNode(state, child, matchPath, segments, currentRoute);\n }\n\n if (!isRoot) {\n segments.pop();\n }\n}\n\nfunction compileAndRegisterRoute(\n state: RegistrationState,\n node: MatcherInputNode,\n matchPath: string,\n parentPath: string,\n segments: MatcherInputNode[],\n parentRoute: CompiledRoute | null,\n): CompiledRoute {\n const slashChild = isSlashChild(matchPath, parentPath);\n\n const frozenSegments = freeze([...segments]);\n const frozenMeta = buildMeta(frozenSegments);\n\n const normalizedPath = normalizeTrailingSlash(matchPath);\n\n // ⚑ Frozen for the same reason `frozenSegments` above it is (#2137): this\n // array is the ONE query-declaration registry, and `getDeclaredQueryParams`\n // publishes it by reference through `getInternals(...).routeGetStore()`. A\n // push into it makes an undeclared key declared for the channel guard and the\n // mode gate alike; sealing it here means every reader downstream — including\n // `queryParamsFor`'s subtraction — is handed something no caller can edit.\n const declaredQueryParams = freeze(\n collectDeclaredQueryParams(state.rootQueryParams, segments),\n );\n\n // Slash-child: use parent path for buildParts (not slash-child's path)\n const buildPath = slashChild\n ? normalizeTrailingSlash(parentPath)\n : normalizedPath;\n\n const { buildStaticParts, buildParamSlots } = compileBuildParts(\n buildPath,\n // Stryker disable next-line MethodExpression: equivalent — slash-child buildParts: dropping the last segment vs keeping it yields identical buildStaticParts here (no own params on the slash-child). Proven by injection (full suite green).\n slashChild ? segments.slice(0, -1) : segments,\n state.options.urlParamsEncoding,\n state.rootUrlParams,\n );\n\n // #1151: reject a duplicate param name within one route's full path (`/:id/:id`,\n // a param+splat clash `/:x/*x`, or a parent `/a/:x` + child `/:x`). buildParamSlots\n // keeps duplicates; the trie stores them at DIFFERENT positions under the SAME\n // name, so match's later capture silently overwrites the earlier and\n // rewritePathOnMatch then rewrites the user's URL from the single survivor. The\n // #736 conflict guard only fires on DIFFERENTLY-named params at ONE position, so\n // this same-name case slips through.\n const buildParamNames = buildParamSlots.map((slot) => slot.paramName);\n const buildParamNamesSet =\n buildParamNames.length === 0 ? EMPTY_STRING_SET : new Set(buildParamNames);\n\n if (buildParamNamesSet.size !== buildParamNames.length) {\n throwDuplicateParamName(node.fullName, buildParamNames);\n }\n\n // #1242 §5.1/§5.2/§5.3: validate query-param DECLARATIONS. A declared query name\n // must be a clean token — reject one carrying constraint/query metacharacters\n // (`:b?<\\d+>` declares query `<\\d+>`; `?tab=1` declares `tab=1`), and reject a name\n // never round-trips (`<`/`>` in the name). A name shared with a path param\n // (`/a/:tab?tab`) is legal under M2 — separate params/search channels (#1548).\n validateQueryParamDeclarations(node.fullName, declaredQueryParams);\n\n const compiled: CompiledRoute = {\n name: node.fullName,\n parent: parentRoute,\n matchSegments: frozenSegments,\n meta: frozenMeta,\n declaredQueryParams,\n declaredQueryParamsSet:\n declaredQueryParams.length === 0\n ? EMPTY_STRING_SET\n : new Set(declaredQueryParams),\n hasTrailingSlash: matchPath.length > 1 && matchPath.endsWith(\"/\"),\n buildStaticParts,\n buildParamSlots,\n buildParamNamesSet,\n // Initialized here (not added conditionally below) so static and param\n // routes share one hidden class — avoids a megamorphic CompiledRoute (#1009).\n cachedResult: undefined,\n };\n\n // Stryker disable next-line ConditionalExpression,EqualityOperator,BlockStatement: equivalent — cachedResult is a pure match() optimization; #buildResult recomputes the same value on a miss (proven: disabling the whole static cache keeps the unit+property suite green)\n if (node.paramMeta.urlParams.length === 0) {\n compiled.cachedResult = freeze({\n segments: compiled.matchSegments,\n params: EMPTY_PARAMS,\n search: EMPTY_SEARCH,\n meta: compiled.meta,\n });\n }\n\n state.routesByName.set(node.fullName, compiled);\n\n if (slashChild) {\n registerSlashChild(state, compiled, parentPath);\n } else {\n registerStandardRoute(state, compiled, matchPath, normalizedPath, node);\n }\n\n return compiled;\n}\n\nfunction buildMeta(\n segments: readonly MatcherInputNode[],\n): Readonly<Record<string, Record<string, \"url\" | \"query\">>> {\n let meta: Record<string, Record<string, \"url\" | \"query\">> | undefined;\n\n for (const segment of segments) {\n if (!hasAnyParam(segment.paramTypeMap)) {\n continue;\n }\n\n meta ??= emptyRecord<Record<string, \"url\" | \"query\">>();\n meta[segment.fullName] = segment.paramTypeMap;\n }\n\n // ⚑ `concealUnsafeKey` between the publish and the freeze (#1957). The keys\n // here are ROUTE NAMES and core accepts one spelled `__proto__` (#1801), so\n // `publishRecord`'s spread — which restores `Object.prototype` on purpose —\n // hands out a record whose own `\"__proto__\"` swaps the prototype of anything\n // that merges it. The value is a `paramTypeMap`, an object, so this one is a\n // genuine swap primitive rather than the entry-loss near-miss `buildParamMeta`\n // carries.\n //\n // ⚠ Withheld from ENUMERATION, not deleted, and the difference is measured:\n // this record is core's own working table (`segmentParamsEqual` reads\n // `meta[segmentName]` per navigation), so a delete sends that read to the\n // INHERITED accessor, which answers `Object.prototype` — an object with no\n // keys, i.e. \"params unchanged\". A `:id` change then stops re-activating the\n // segment.\n return meta === undefined\n ? EMPTY_ROUTE_META\n : freeze(concealUnsafeKey(publishRecord(meta)));\n}\n\n// Allocation-free emptiness probe for a segment's paramTypeMap (Object.keys\n// would allocate a fresh array per segment during registration).\nfunction hasAnyParam(\n paramTypeMap: Readonly<Record<string, \"url\" | \"query\">>,\n): boolean {\n for (const key in paramTypeMap) {\n if (hasOwn(paramTypeMap, key)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction registerSlashChild(\n state: RegistrationState,\n compiled: CompiledRoute,\n parentPath: string,\n): void {\n insertSlashChildIntoTrie(state, compiled, parentPath);\n\n const parentNormalized = normalizeTrailingSlash(parentPath);\n const cacheKey = state.options.caseSensitive\n ? parentNormalized\n : parentNormalized.toLowerCase();\n\n if (state.staticCache.has(cacheKey)) {\n state.staticCache.set(cacheKey, compiled);\n }\n}\n\nfunction registerStandardRoute(\n state: RegistrationState,\n compiled: CompiledRoute,\n matchPath: string,\n normalizedPath: string,\n node: MatcherInputNode,\n): void {\n insertIntoTrie(state, compiled, matchPath);\n\n // Stryker disable next-line ConditionalExpression,EqualityOperator,BlockStatement: equivalent — staticCache is a pure match() optimization; #traverse resolves the same route on a miss (proven: disabling the whole static cache keeps the unit+property suite green)\n if (node.paramMeta.urlParams.length === 0) {\n const cacheKey = state.options.caseSensitive\n ? normalizedPath\n : // Stryker disable next-line MethodExpression: equivalent — the case-insensitive cache key only governs a hit; a miss falls through to #traverse, which is also case-insensitive\n normalizedPath.toLowerCase();\n\n state.staticCache.set(cacheKey, compiled);\n }\n}\n\nfunction isSlashChild(matchPath: string, parentPath: string): boolean {\n const normalizedMatch = normalizeTrailingSlash(matchPath);\n const normalizedParent = normalizeTrailingSlash(parentPath);\n\n return normalizedMatch === normalizedParent;\n}\n\nfunction collectDeclaredQueryParams(\n rootQueryParams: readonly string[],\n segments: readonly MatcherInputNode[],\n): readonly string[] {\n // Stryker disable next-line ArrayDeclaration: equivalent — the array is populated then returned; a phantom seed element is never read back (declared-param loop skips absent keys, buildQueryString ignores it). Proven by injection.\n const queryParams: string[] = [];\n\n // Include query params declared on the root node (e.g., a root path like \"?mode\")\n if (rootQueryParams.length > 0) {\n queryParams.push(...rootQueryParams);\n }\n\n for (const segment of segments) {\n if (segment.paramMeta.queryParams.length > 0) {\n queryParams.push(...segment.paramMeta.queryParams);\n }\n }\n\n return queryParams.length === 0 ? EMPTY_STRINGS : queryParams;\n}\n\nfunction validateQueryParamDeclarations(\n routeName: string,\n queryParams: readonly string[],\n): void {\n // A path/query name collision (`/a/:tab?tab`) is legal under M2 — `tab` lives\n // in `state.params` AND `state.search` as separate channels (RFC-4 M2 /\n // #1548); the former \"declared as both\" rejection is gone. Only a query name\n // that can never round-trip (contains `<`/`>`) is still rejected.\n for (const name of queryParams) {\n if (INVALID_QUERY_NAME_RGX.test(name)) {\n throwInvalidQueryParamName(routeName, name);\n }\n }\n}\n","import { DECODING_METHODS, ENCODING_METHODS } from \"./encoding\";\nimport { createSegmentNode, normalizeTrailingSlash } from \"./pathUtils\";\nimport { validatePercentEncoding } from \"./percentEncoding\";\nimport { registerNode } from \"./registration\";\nimport { copyFields, putField } from \"../../utils/ingest\";\n\nimport type {\n BuildPathOptions,\n CompiledRoute,\n MatcherInputNode,\n MatchResult,\n ResolvedMatcherOptions,\n SegmentMatcherOptions,\n SegmentNode,\n URLParamsEncodingType,\n} from \"./types\";\n\n/**\n * The marker `search-params`' `requireStrategy` puts on the ONE error the parse\n * catch below must let through — see the note beside its declaration in\n * `engine/search-params/strategies/index.ts`.\n *\n * ⚑ Declared here rather than imported: this layer is a self-contained leaf and\n * the boundary rule in `eslint.config.mjs` refuses the import. `Symbol.for` is\n * what lets the two agree without coupling, so the STRING is the contract —\n * change it in one place and the config fault silently starts being swallowed,\n * which is why `query-strategy-formats-1796.test.ts` pins the rethrow with a\n * CONTROL that fails on exactly that.\n */\nconst CONFIG_FAULT = Symbol.for(\"real-router.searchParams.configFault\");\n\n/**\n * Intrinsics captured at module load: `hasOwn`, `objectKeys`,\n * `getOwnPropertyDescriptor` — the three declared below, and the list is the\n * whole of them.\n *\n * ⚑ A guard is only as strong as the intrinsic it reads WHEN IT RUNS, and an\n * application can re-point any of these AFTER boot — which is what this closes.\n * Measured on the uncaptured form: one naive `Object.hasOwn` polyfill walked\n * straight through five sibling readers while the single captured guard held.\n *\n * ⚠ The limit of what capture buys — and the shim order that defeats it — is\n * stated once, in `guards.ts`. Not restated here (#2091).\n */\nconst hasOwn = Object.hasOwn;\nconst objectKeys = Object.keys;\nconst getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;\n\n/**\n * Is this the config fault the parse catch below is allowed to rethrow?\n *\n * ⚑ The ask is itself wrapped, and that is not belt-and-braces. The value came\n * out of a `throw` inside caller-reachable code, so it can be anything — and\n * `SYMBOL in value` runs the `has` trap of a Proxy. Measured: an application\n * throwing `new Proxy(err, { has() { throw … } })` from an `Object.prototype`\n * setter made THIS CHECK throw, out of `matchPath`, which is the exact contract\n * the check exists to protect. A predicate that can throw is a fail-open default\n * wearing a different hat, which is why the ask is wrapped rather than narrowed:\n * narrowing what the predicate accepts leaves the throw where it is.\n *\n * If asking whether the error is ours throws, it is not ours.\n *\n * ⚠ The marker is a LABEL, not a capability. `Symbol.for` is a global registry,\n * so an application can obtain the same symbol and attach it to an error of its\n * own, which would then be rethrown (measured). That is accepted: forging it\n * takes a deliberate `Symbol.for` with this exact string, at which point the\n * application is asking to be rethrown. A private `Symbol()` would close it and\n * cannot cross the layer boundary, which is why the registry is used at all.\n */\nfunction isConfigFault(error: unknown): boolean {\n // ⚑ OWN, not inherited — `requireStrategy` attaches the marker with\n // `Object.defineProperty`, so a genuine fault always carries it as an own\n // property, while `SYMBOL in error` walks the prototype chain and consults a\n // Proxy `has` trap, i.e. says yes to two things the raiser cannot produce.\n //\n // ⚠ The escalation is what made that a defect and not a curiosity: ONE write\n // of this symbol to `Object.prototype` flips the whole catch fail-open, and\n // `match()` throws on plain user INPUT — a malformed `%`-sequence, the\n // class #737 exists to swallow — into `browser-plugin` / `hash-plugin` /\n // `navigation-plugin`, none of which catch. The loop closes on itself: a\n // polluted `Object.prototype` is the very thing the catch above is here for.\n //\n // ⚑ CAPTURED at module load, and that is the second half of the same lesson.\n // `in` is an OPERATOR — no application can re-point it. Every intrinsic this\n // predicate reaches is a mutable global, so reading one at call time trades a\n // hole that needs the attacker to know this file's exact `Symbol.for` string\n // for one that needs no knowledge of real-router at all. Measured on the\n // uncaptured forms: re-pointing the READER made `match()` rethrow a `URIError`\n // on ordinary input, and re-pointing the WRITER (`Object.defineProperty` at the\n // tag site) made a GENUINE fault look foreign and be swallowed — #1318's\n // symptom. Both are captured; the writer's capture lives beside the tag.\n //\n // ⚠ The intrinsic in question is `getOwnPropertyDescriptor`, not\n // `Object.hasOwn`: the predicate tests the DESCRIPTOR. A cell that tampers\n // `hasOwn` to attack this asserts untampered behaviour under a title naming\n // the capture. Name the intrinsic you actually read.\n //\n // ⚠ The `try` has THREE subjects, not one: `getOwnPropertyDescriptor(null, …)`\n // and `(undefined, …)` throw `TypeError`, and `throw null` is far commoner than\n // the revoked Proxy whose trap throws. Other primitives answer `undefined`\n // without throwing.\n //\n try {\n // ⚑ The DESCRIPTOR, not just presence. A genuine fault is tagged with\n // `Object.defineProperty(error, CONFIG_FAULT, { value: true })`, so its\n // descriptor is `configurable: false` — and a Proxy CANNOT report that for a\n // key its target does not own: the invariant check throws first (measured).\n // A lying `getOwnPropertyDescriptor` trap can forge `hasOwn` — it can say\n // `configurable: true` all day for a key the target does not own, and the\n // invariant permits it while the target is extensible — so presence alone\n // is forgeable.\n //\n // ⚠ This narrows the forgery surface; it does NOT close it. What the\n // descriptor rules out is a Proxy LYING about a key its target does not own\n // — the invariant check stops it from claiming non-configurability. What it\n // does not rule out is the obvious one:\n // `Object.defineProperty(err, Symbol.for(<this string>), { value: true })`,\n // which is the exact call core itself makes, against a symbol anyone can\n // pull out of the global registry.\n //\n // ⚑ That is ACCEPTED, deliberately, and pinned as accepted one file over —\n // `query-strategy-formats-1796.test.ts`: \"the marker is a LABEL, not a\n // capability\". Identity (a `WeakSet`) is what would close it, and the\n // eslint layer boundary between `path-matcher` and `search-params` is why\n // the registry is used instead. Saying \"closed\" here while the sibling test\n // pins \"forgeable\" is worse than either answer alone: it sends the next\n // reader looking in the wrong place.\n return (\n getOwnPropertyDescriptor(error, CONFIG_FAULT)?.configurable === false\n );\n } catch {\n return false;\n }\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\n/** Coerces a route param value (typed `unknown`, contractually primitive) to\n * the string the encoder receives. Objects are JSON-stringified. */\nfunction stringifyParamValue(value: unknown): string {\n // Stryker disable next-line BlockStatement: equivalent — String(value) === value for a string, so removing the early return is identical\n if (typeof value === \"string\") {\n return value;\n }\n\n if (typeof value === \"object\") {\n return JSON.stringify(value);\n }\n\n // eslint-disable-next-line @typescript-eslint/no-base-to-string -- route params are typed `unknown` but contractually primitive\n return String(value);\n}\n\n/** Shared frozen empty query object reused for every query-less match. */\nconst EMPTY_SEARCH: Readonly<Record<string, unknown>> = Object.freeze({});\n\n// =============================================================================\n// SegmentMatcher Class\n// =============================================================================\n\n/**\n * `strict` has no build form of its own — it means \"print exactly what the\n * compiled route declares\", which is the `always` / `never` that route implies\n * (#2017).\n *\n * ⚠ Keyed on the COMPILED `hasTrailingSlash`, never on the declared string:\n * that flag carries the `length > 1` term, so the root `/` resolves to `never`\n * and is then left alone by the `path !== \"/\"` guard in `#applyTrailingSlash`.\n */\nfunction resolveBuildTrailingSlash(\n mode: BuildPathOptions[\"trailingSlash\"],\n hasTrailingSlash: boolean,\n): Exclude<BuildPathOptions[\"trailingSlash\"], \"strict\"> {\n if (mode !== \"strict\") {\n return mode;\n }\n\n return hasTrailingSlash ? \"always\" : \"never\";\n}\n\nexport class SegmentMatcher {\n get options(): ResolvedMatcherOptions {\n return this.#options;\n }\n\n readonly #options: ResolvedMatcherOptions;\n\n readonly #root: SegmentNode = createSegmentNode();\n readonly #routesByName = new Map<string, CompiledRoute>();\n readonly #staticCache = new Map<string, CompiledRoute>();\n\n // H1: Reusable object eliminates tuple allocation per match() call\n readonly #prepared = {\n // Stryker disable next-line StringLiteral: equivalent — #prepared.cleanPath is overwritten by #preparePath before any read (dead initializer)\n cleanPath: \"\",\n // Stryker disable next-line StringLiteral: equivalent — #prepared.normalized is overwritten by #preparePath before any read (dead initializer)\n normalized: \"\",\n queryString: undefined as string | undefined,\n };\n\n // Stryker disable next-line ArrayDeclaration: equivalent — #rootQueryParams is overwritten by registerTree before any read (dead initializer)\n #rootQueryParams: readonly string[] = [];\n // Stryker disable next-line StringLiteral: equivalent — #scanTruncated is set by #scanPath before it is read (dead initializer)\n #scanTruncated = \"\";\n\n readonly #caseSensitive: boolean;\n readonly #decode: ((param: string) => string) | null;\n\n constructor(options: SegmentMatcherOptions) {\n // ⚠ `unknown`, not the declared union. A TS consumer cannot reach this\n // fallback at all — the union already forbids the typo — so every value that\n // does reach it came from a JS consumer or a runtime-assembled config, which\n // is exactly what #1811 is about. Typing it by the declaration made the\n // coercion below read as a no-op.\n const requestedEncoding: unknown = options.urlParamsEncoding ?? \"default\";\n\n // `Object.hasOwn` on the table, and a FALLBACK rather than a throw (#1811).\n //\n // Both encoder maps are plain object literals indexed by a string the\n // consumer supplies, so without the check an unrecognised value is\n // installed verbatim as the live encoder: an `Object.prototype` member makes\n // one — `\"toString\"` builds `/x/[object Object]` in both directions and\n // `\"constructor\"` passes the value through, printing a raw space — while an\n // ordinary typo yields `undefined` and defers a\n // `TypeError: slot.encoder is not a function` from inside `buildPath`,\n // naming nothing.\n //\n // ⚑ Bare core DEGRADES, it does not throw, because that is what its two\n // sibling enums already do: an unrecognised `trailingSlash` or\n // `queryParamsMode` keeps the router working instead of crashing. This\n // option was the odd one out — its `does NOT throw` test passed only\n // because the crash arrived later, from a different call.\n //\n // ⚠ \"Degrades\" is the shared property; \"falls back to the DEFAULT\" is not.\n // Measured, neither sibling lands on its own default:\n //\n // trailingSlash default \"preserve\" → matchPath(\"/a/\") keeps \"/a/\"\n // unrecognised → \"/a\", i.e. it behaves like \"never\"\n // queryParamsMode default \"loose\" → prints an undeclared query key\n // unrecognised → drops it, i.e. like \"default\"\n //\n // So the precedent this option follows is \"do not crash\", not \"land on the\n // default\" — which matters if anyone ever tries to normalise the three into\n // one rule.\n // Rejecting an invalid enum by name is `@real-router/validation-plugin`'s\n // job, and it already owns this exact list; throwing here would shadow its\n // better-worded message.\n //\n // ⚑ ONE check covers all three index sites: the decoder below, `encodeParam`\n // and `makeBuildParamSlot` all read `#options.urlParamsEncoding`, which this\n // line fixes once and `registerTree` hands down to registration.\n //\n // ⚑ And the check STORES THE KEY IT TESTED, not the caller's value. Both\n // `Object.hasOwn` and every one of those three index sites run\n // `ToPropertyKey` on what they are given, so admitting the value re-reads a\n // caller-owned object once per site: a `{ toString }` answering \"uri\" to the\n // guard and \"bogusTypo\" to the encoder was validated as one encoding and\n // used as another — reproducing verbatim the two failures this fallback\n // exists to remove (`slot.encoder is not a function`, and\n // `\"toString\"` printing `/x/[object Object]`). One coercion, here, is what\n // makes verdict and use inseparable. Symbols keep their behaviour:\n // `String(symbol)` is legal and yields a name no table owns, so they fall\n // back exactly as `Object.hasOwn` made them fall back before.\n // The declared union is precisely what cannot be trusted here — see the note\n // on the declaration above.\n const encodingKey =\n typeof requestedEncoding === \"string\"\n ? requestedEncoding\n : // This branch is reached only when the value is NOT a string; the\n // rule reads the declared union, which is what this guard distrusts.\n // eslint-disable-next-line unicorn/no-useless-coercion -- see above\n String(requestedEncoding);\n const urlParamsEncoding = hasOwn(ENCODING_METHODS, encodingKey)\n ? (encodingKey as URLParamsEncodingType)\n : \"default\";\n\n this.#options = {\n caseSensitive: options.caseSensitive ?? true,\n strictTrailingSlash: options.strictTrailingSlash ?? false,\n strictQueryParams: options.strictQueryParams ?? false,\n urlParamsEncoding,\n parseQueryString: options.parseQueryString,\n buildQueryString: options.buildQueryString,\n };\n\n this.#caseSensitive = this.#options.caseSensitive;\n this.#decode =\n this.#options.urlParamsEncoding === \"none\"\n ? null\n : DECODING_METHODS[this.#options.urlParamsEncoding];\n }\n\n registerTree(node: MatcherInputNode): void {\n this.#rootQueryParams = node.paramMeta.queryParams;\n registerNode(\n {\n root: this.#root,\n options: this.#options,\n routesByName: this.#routesByName,\n staticCache: this.#staticCache,\n rootQueryParams: this.#rootQueryParams,\n rootUrlParams: node.paramMeta.urlParams,\n },\n node,\n \"\",\n [],\n null,\n );\n }\n\n match(path: string): MatchResult | undefined {\n if (!this.#preparePath(path)) {\n return undefined;\n }\n\n const { cleanPath, normalized, queryString } = this.#prepared;\n\n const cacheKey = this.#caseSensitive\n ? normalized\n : // Stryker disable next-line MethodExpression: equivalent — the case-insensitive cache key only governs a hit; a miss falls through to #traverse (also case-insensitive), same result (proven by injection)\n normalized.toLowerCase();\n const cached = this.#staticCache.get(cacheKey);\n\n // Stryker disable next-line BlockStatement: equivalent — emptying the cache-hit block routes through #traverse, which resolves the same route (cache is a pure optimization)\n if (cached) {\n if (\n this.#options.strictTrailingSlash &&\n !this.#checkTrailingSlash(cleanPath, cached)\n ) {\n return undefined;\n }\n\n // Stryker disable next-line BlockStatement: equivalent — #buildResult recomputes cached.cachedResult identically (cache short-circuit)\n if (queryString === undefined && cached.cachedResult) {\n return cached.cachedResult;\n }\n\n return this.#buildResult(cached, {}, queryString);\n }\n\n const params: Record<string, string> = {};\n const route = this.#traverse(normalized, params);\n\n if (!route) {\n return undefined;\n }\n\n if (\n this.#options.strictTrailingSlash &&\n !this.#checkTrailingSlash(cleanPath, route)\n ) {\n return undefined;\n }\n\n // Decode the captured params. `match()` must never throw — a malformed\n // percent sequence (#737) makes this return false → the URL is unmatched.\n if (!this.#decodeParams(params)) {\n return undefined;\n }\n\n return this.#buildResult(route, params, queryString);\n }\n\n buildPath(\n name: string,\n params?: Record<string, unknown>,\n search?: Record<string, unknown>,\n options?: BuildPathOptions,\n ): string {\n const route = this.#routesByName.get(name);\n\n if (!route) {\n throw new Error(`[SegmentMatcher.buildPath] '${name}' is not defined`);\n }\n\n const path = this.#buildUrlPath(route, params);\n const finalPath = this.#applyTrailingSlash(\n path,\n resolveBuildTrailingSlash(options?.trailingSlash, route.hasTrailingSlash),\n );\n // Search-aware (RFC-4 M2 / #1548): when an explicit `search` bag is passed,\n // the query string is built from it; the path comes from `params`. So a\n // colliding name (`/items/:id?id` with `buildPath(\"items\", {id:5}, {id:7})`)\n // emits `/items/5?id=7` — path wins its slot, query wins its own (the killed\n // #843 precedence). A v1 caller passes no `search`, so `search ?? params`\n // falls back to extracting the query half from the single bag, unchanged.\n const queryString = this.#buildQueryStringForBuild(\n route,\n search ?? params,\n options?.queryParamsMode,\n );\n\n return finalPath + (queryString ? `?${queryString}` : \"\");\n }\n\n getSegmentsByName(name: string): readonly MatcherInputNode[] | undefined {\n // Derived from #routesByName — `compiled.matchSegments` is the same frozen\n // array a dedicated #segmentsByName index would store, so the index was\n // pure duplication (#1010).\n return this.#routesByName.get(name)?.matchSegments;\n }\n\n getMetaByName(\n name: string,\n ): Readonly<Record<string, Record<string, \"url\" | \"query\">>> | undefined {\n return this.#routesByName.get(name)?.meta;\n }\n\n /**\n * The route's declared query-param names — the SAME registry the query-string\n * build reads (`#buildQueryStringForBuild`), so a consumer classifying keys\n * into channels cannot drift from what this matcher actually prints (#1556).\n *\n * Unlike a walk over {@link getSegmentsByName}, this includes the ROOT node's\n * `?`-declarations (`setRootPath(\"?a&b\")` — how persistent-params declares its\n * keys): the root is captured in `#rootQueryParams` at `registerTree` and is\n * deliberately NOT part of `matchSegments`, so a segment walk silently misses\n * it. Path-slot collisions (`/items/:id?id`) are NOT filtered here — the\n * caller owns that policy (core subtracts its `urlParams`, #843 / #1549).\n */\n getDeclaredQueryParams(name: string): readonly string[] | undefined {\n return this.#routesByName.get(name)?.declaredQueryParams;\n }\n\n hasRoute(name: string): boolean {\n return this.#routesByName.has(name);\n }\n\n #buildUrlPath(\n route: CompiledRoute,\n params: Record<string, unknown> | undefined,\n ): string {\n const parts = route.buildStaticParts;\n const slots = route.buildParamSlots;\n\n // Stryker disable next-line BlockStatement: equivalent — the general loop returns parts[0] when slots is empty, identical to this fast path\n if (slots.length === 0) {\n return parts[0];\n }\n\n let result = parts[0];\n\n for (const [i, slot] of slots.entries()) {\n // `Object.hasOwn` before the read, not a bare `params[name]`: the slot name\n // comes from the ROUTE, so a route declaring `/:toString` (or any other\n // `Object.prototype` member) reads the inherited METHOD off an EMPTY bag,\n // the `undefined`/`null` test below never fires, and the required-param\n // guard is bypassed while the serialized function is printed into the path\n // (#1798). Same spelling as the `loose` arm in\n // `#buildQueryStringForBuild` and as `channels/`.\n //\n // ⚠ The nullish test covers `null` as well as `undefined`, and that is not\n // decoration: the alternative that looks equivalent is\n // `params?.[slot.paramName]`, and optional chaining is nullish-safe while\n // `Object.hasOwn` does\n // `ToObject` first and THROWS on `null`. Dropping `null` here turned a\n // named `Missing required param` into a bare\n // `TypeError: Cannot convert undefined or null to object`. The bag reaches\n // this line unnormalised only from a route's own `encodeParams`, whose\n // return value `RoutesNamespace` forwards verbatim — the facade's\n // `normalizeChannel` never yields `null`. The sibling read in\n // `#buildQueryStringForBuild` guards the same way (`if (!params)`).\n const value =\n params !== undefined &&\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- the STATIC type is narrower than the runtime: `RoutesNamespace` forwards a route's `encodeParams` return VERBATIM (its `encoded.params` is unvalidated user output), so `null` really arrives here. Same shape as `Router.ts`'s runtime guard for `navigate(null)`.\n params !== null &&\n hasOwn(params, slot.paramName)\n ? params[slot.paramName]\n : undefined;\n\n // 3-token grammar (M1): every param slot is required — no optional-omit\n // branch. A missing param is an error.\n if (value === undefined || value === null) {\n throw new Error(\n `[SegmentMatcher.buildPath] Missing required param '${slot.paramName}'`,\n );\n }\n\n // #740 item 3: an empty value collapses the segment, silently producing a\n // path that matches the parent route (`buildPath(\"u.p\", {id:\"\"})` →\n // `/users/` → matches `u`). Reject it like a missing param.\n if (value === \"\") {\n throw new Error(\n `[SegmentMatcher.buildPath] Missing required param '${slot.paramName}' (empty string)`,\n );\n }\n\n const encoded = slot.encoder(stringifyParamValue(value));\n\n result += encoded + parts[i + 1];\n }\n\n return result;\n }\n\n #applyTrailingSlash(\n path: string,\n mode: BuildPathOptions[\"trailingSlash\"],\n ): string {\n if (mode === \"always\" && !path.endsWith(\"/\")) {\n return `${path}/`;\n }\n\n /* v8 ignore next 3 -- @preserve: trailing slash may not appear in buildStaticParts; integration-tested via core */\n if (mode === \"never\" && path !== \"/\" && path.endsWith(\"/\")) {\n return path.slice(0, -1);\n }\n\n return path;\n }\n\n #buildQueryStringForBuild(\n route: CompiledRoute,\n params: Record<string, unknown> | undefined,\n queryParamsMode: BuildPathOptions[\"queryParamsMode\"],\n ): string {\n if (!params) {\n return \"\";\n }\n\n // Stryker disable next-line BlockStatement: equivalent — the downstream !hasKeys guard also returns '' for a route with no declared query params\n if (route.declaredQueryParams.length === 0 && queryParamsMode !== \"loose\") {\n return \"\";\n }\n\n const queryObj: Record<string, unknown> = {};\n // Stryker disable next-line BooleanLiteral: equivalent — buildQueryString({}) === '' so the hasKeys initial value is unobservable when no keys are added\n let hasKeys = false;\n\n for (const name of route.declaredQueryParams) {\n // `Object.hasOwn`, not `name in params`: the name comes from the ROUTE, so\n // `in` walks the PROTOTYPE and a route declaring `?toString` reads as \"the\n // caller already filled this slot\" on an EMPTY bag — printing the\n // serialized native method into the href while the committed `state.search`\n // stays empty, a state contradicting its own path (#1798). The `loose` arm\n // below already asks the identical question this way.\n // Stryker disable next-line BlockStatement: equivalent — buildQueryString strips undefined, so adding absent declared keys instead of continue changes nothing\n if (!hasOwn(params, name)) {\n continue;\n }\n\n // ⚑ The name is one the ROUTE declares, so an application that put an\n // accessor there hijacks the write (#1852). Measured: `buildPath` threw\n // `TypeError: Cannot set property tab …`, and the getter+setter shape\n // printed `/q` — the requested key silently gone from the URL.\n putField(queryObj, name, params[name]);\n hasKeys = true;\n }\n\n if (queryParamsMode === \"loose\") {\n // ⚑ `objectKeys`, one spelling across the engine (#1840). All three walks\n // here read it; none reads `for…in` + `Object.hasOwn`.\n //\n // ⚠ No behaviour rides on that HERE, and the honest version of this note\n // says so: every bag these three walks see is core-built or rebuilt by the\n // channel layer upstream, so a caller's Proxy never reaches them —\n // measured, a bag whose `getOwnPropertyDescriptor` trap reads through the\n // prototype chain while `ownKeys` does not produces the identical URL\n // under both spellings. The point is that the two forms ARE\n // distinguishable in general (#1854 measures where), so an engine that\n // carries both invites the next reader to assume the one in front of them\n // is safe because its neighbour is.\n for (const paramKey of objectKeys(params)) {\n if (\n route.declaredQueryParamsSet.has(paramKey) ||\n route.buildParamNamesSet.has(paramKey)\n ) {\n continue;\n }\n\n // Same rule, and here the key is the CALLER's (#1852).\n putField(queryObj, paramKey, params[paramKey]);\n hasKeys = true;\n }\n }\n\n // Stryker disable next-line BlockStatement: equivalent — buildQueryString({}) === '' so removing the !hasKeys early return yields the same ''\n if (!hasKeys) {\n return \"\";\n }\n\n return this.#options.buildQueryString(queryObj);\n }\n\n // H2: Single-pass scanner — replaces 4 separate scans (indexOf(\"#\"), regex unicode, indexOf(\"?\"), includes(\"//\"))\n #preparePath(path: string): boolean {\n if (path === \"\") {\n path = \"/\";\n }\n\n if (path.codePointAt(0) !== 0x2f /* / */) {\n return false;\n }\n\n const qIdx = this.#scanPath(path);\n\n if (qIdx === -2) {\n return false;\n }\n\n if (qIdx === -3) {\n path = this.#scanTruncated;\n }\n\n const pathPart = qIdx >= 0 ? path.slice(0, qIdx) : path;\n let queryString = qIdx >= 0 ? path.slice(qIdx + 1) : undefined;\n\n // #842: a fragment (`#…`) AFTER the query separator is not seen by\n // #scanPath (it returns at the first `?`), so it would otherwise be folded\n // into the query string and parsed into a param value (e.g. `?a=1#frag` →\n // `a=\"1#frag\"`). A fragment is everything after the first `#` in the whole\n // URL and must be stripped before query parsing — a native indexOf on the\n // (short) query substring, only when a query exists, is ~free (a `#` BEFORE\n // the `?` is already handled by #scanPath via the -3 truncation branch).\n if (queryString !== undefined) {\n const hashIdx = queryString.indexOf(\"#\");\n\n if (hashIdx !== -1) {\n queryString = queryString.slice(0, hashIdx);\n }\n }\n\n const normalized = normalizeTrailingSlash(pathPart);\n\n this.#prepared.cleanPath = pathPart;\n this.#prepared.normalized = normalized;\n this.#prepared.queryString = queryString;\n\n return true;\n }\n\n // Returns: qIdx >= 0 (found ?), -1 (no ? or #), -2 (invalid), -3 (truncated at #, result in #scanTruncated)\n #scanPath(path: string): number {\n let prevSlash = false;\n\n for (let i = 0; i < path.length; i++) {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- bounds-checked by loop condition\n const ch = path.codePointAt(i)!;\n\n if (ch === 0x23 /* # */) {\n this.#scanTruncated = path.slice(0, i);\n\n return -3;\n }\n\n if (ch === 0x3f /* ? */) {\n return i;\n }\n\n if (ch >= 0x80) {\n return -2;\n }\n\n if (ch === 0x2f /* / */) {\n if (prevSlash) {\n return -2;\n }\n\n prevSlash = true;\n } else {\n prevSlash = false;\n }\n }\n\n return -1;\n }\n\n #buildResult(\n route: CompiledRoute,\n params: Record<string, unknown>,\n queryString: string | undefined,\n ): MatchResult | undefined {\n let search: Readonly<Record<string, unknown>> = EMPTY_SEARCH;\n\n if (queryString !== undefined) {\n const parsed = this.#parseSearch(route, queryString);\n\n if (parsed === undefined) {\n return undefined;\n }\n\n // Query goes ONLY into its own channel now (RFC-4 M2 / #1548). `params`\n // stays path-only — the A2 back-compat fold into `params`, and with it\n // the query-overwrites-path precedence (#843), are gone.\n search = parsed;\n }\n\n return {\n segments: route.matchSegments,\n params,\n search,\n meta: route.meta,\n };\n }\n\n // Parses the query string into its OWN object — the query channel (RFC-4 M2 /\n // #1548). Returns undefined (→ match yields undefined) when the URL is\n // unmatchable: the injected parser threw, or strict mode saw an undeclared\n // key. The injected parser (searchParams.ts) already hardens `__proto__` as an\n // own key (#855/#1293), so the parsed object is returned directly — no per-key\n // re-hardening (that folding, and `assignQueryParam`, live on only for the A3\n // back-compat merge into `params`).\n #parseSearch(\n route: CompiledRoute,\n queryString: string,\n ): Record<string, unknown> | undefined {\n let search: Record<string, unknown>;\n\n try {\n search = this.#options.parseQueryString(queryString);\n } catch (error) {\n // The injected query parser decodes percent-encoding too, so the same\n // valid-hex/invalid-UTF-8 sequence that breaks path params (e.g.\n // `?x=%E0%41`) makes it throw a URIError. `match()` must never throw on\n // INPUT — treat the whole URL as unmatched so the router resolves to\n // UNKNOWN_ROUTE instead of crashing on start() (#737).\n // ⚑ Rethrow by ORIGIN, swallow everything else. The inverse — \"rethrow\n // anything that is not a `URIError`\" — reads as equivalent and is not: it\n // makes the default FAIL-OPEN, so the contract now depends on an\n // enumeration of every thrower reachable from here being complete. It\n // cannot be: the set is not fixed by this file, it depends on what every\n // callee does with a key taken from the URL, and closing one path does not\n // make the enumeration complete or keep it so.\n //\n // `match()` must never throw on INPUT — a link from anywhere would\n // otherwise crash a `popstate` handler, and the three URL plugins plus\n // `preload-plugin`'s hover path call this with no `catch` of their own.\n // The one thing that MUST escape is the config fault (#1796): swallowing\n // it is what turned \"your `queryParams` format is invalid\" into \"every URL\n // with a query resolves to UNKNOWN_ROUTE\". It carries a marker for exactly\n // this, so the two questions stay separate — what went wrong, and whose\n // fault it is.\n if (isConfigFault(error)) {\n throw error;\n }\n\n return undefined;\n\n // A CONFIG error is not that class, and swallowing it is what let #1318's\n // own reported symptom survive its fix: `requireStrategy` throws a\n // TypeError naming the offending `queryParams` field, and this catch turned\n // it into \"every URL with a query resolves to UNKNOWN_ROUTE\" — a routing\n // symptom pointing away from the config that caused it (#1796).\n //\n // The parser is core's own — `createMatcher` supplies `parseQuery`, and\n // `CreateMatcherOptions` exposes formats, not a custom parser — so no\n // consumer can inject a thrower here.\n //\n // ⚠ The narrow thrower set holds BY CONSTRUCTION, not by luck:\n // `assignParam` writes through `putField`, which DEFINES the key rather\n // than assigning it, so an inherited setter on a polluted\n // `Object.prototype` is never invoked and cannot throw inside this try\n // (#1852). Pinned by \"an inherited SETTER is never invoked, so it cannot\n // escape\" in `match-never-throws-on-input-1840.test.ts`. Rethrow-by-origin\n // does not depend on that continuing to hold.\n //\n // ⚠ Rethrowing an application fault, so it is not reported as \"no such\n // route\", is the WRONG trade here, and the measurement is why. The\n // rethrow is selected by INPUT, and the callers of this function do not\n // catch: the `matchUrl` extensions `browser-plugin` and `hash-plugin`\n // install, `navigation-plugin` at four sites, `preload-plugin`'s anchor\n // resolver (reached from a `mouseover` listener on `document`) and\n // `ssr-utils/getStaticPaths`. Measured: one hover raised an\n // uncaught `error` on `window`; and per #1819's own note, an\n // un-intercepted navigate event makes Chromium perform a full-document\n // reload. \"Never throw on input\" outranks \"attribute the fault\", because\n // the caller that would attribute it is a popstate handler.\n //\n // What is NOT swallowed is the config fault, and that is the whole point\n // of tagging it: #1796's complaint — an invalid `queryParams` format\n // reported as \"every URL with a query resolves to UNKNOWN_ROUTE\" — is\n // fixed by the marker above, not by the fail-open default.\n //\n // Pinned by the third-class cell in `query-strategy-formats-1796.test.ts`,\n // without which any predicate separating URIError from TypeError passes —\n // including this one's complement.\n // ⚠ The sibling `search-params/utils.ts` does NOT narrow on this\n // predicate — `safeEncode` asks `if (!(error instanceof URIError)) throw`,\n // which is the fail-OPEN shape argued against three lines above. Harmless\n // there (every value it catches is engine-produced), but do not read that\n // shape as this file's doctrine.\n }\n\n if (this.#options.strictQueryParams) {\n const declared = route.declaredQueryParamsSet;\n\n for (const key of objectKeys(search)) {\n // Same walk, same reason (#1840): `search` is a plain `{}` from\n // `parseQueryWith`, so an inherited enumerable is tested against\n // `declaredQueryParamsSet`, is of course not in it, and unmatches the\n // route. One ambient `Object.prototype.foo = 1` therefore made EVERY\n // query-bearing URL resolve to `UNKNOWN_ROUTE` under this mode.\n if (!declared.has(key)) {\n return undefined;\n }\n }\n }\n\n return search;\n }\n\n #checkTrailingSlash(cleanPath: string, route: CompiledRoute): boolean {\n const inputHasSlash = cleanPath.length > 1 && cleanPath.endsWith(\"/\");\n\n return inputHasSlash === route.hasTrailingSlash;\n }\n\n #traverse(\n path: string,\n params: Record<string, string>,\n ): CompiledRoute | undefined {\n if (path.length === 1) {\n /*\n * Ordinary traffic, not a corner: `\"/\"` is in #staticCache only when some\n * route NORMALISES to it, so every table without one arrives here. The\n * `?? route` arm is the cache-MISS fallback that every `Stryker disable`\n * reason arguing \"a miss falls through to #traverse\" rests on — deleting\n * it keeps this suite green and falsifies each of them (#2206).\n * `root-slash-traverse-2206.test.ts` owns that set.\n */\n return this.#root.slashChildRoute ?? this.#root.route;\n }\n\n return this.#traverseFrom(this.#root, path, 1, params);\n }\n\n // The core match hot loop. The #1288 param+splat junction is inlined here\n // rather than extracted into a helper, because a per-param helper call\n // regresses the common single-param path ~5% (spike-measured).\n // eslint-disable-next-line sonarjs/cognitive-complexity -- inlined #1288 junction (see above): the static/param/splat dispatch is deliberately one function for the hot path\n #traverseFrom(\n startNode: SegmentNode,\n path: string,\n start: number,\n params: Record<string, string>,\n ): CompiledRoute | undefined {\n let node = startNode;\n const length = path.length;\n const caseSensitive = this.#caseSensitive;\n\n while (start <= length) {\n const end = path.indexOf(\"/\", start);\n const segmentEnd = end === -1 ? length : end;\n const segment = path.slice(start, segmentEnd);\n\n const lookupKey = caseSensitive ? segment : segment.toLowerCase();\n let next: SegmentNode;\n\n if (lookupKey in node.staticChildren) {\n const staticChild = node.staticChildren[lookupKey];\n\n // #2006: the STATIC half of the #1288 junction below. Taking a static\n // child was unconditional and had no way back, so a walk that ran out of\n // path on a node carrying no route failed even though a splat sibling at\n // THIS node would have captured the whole remainder — `/app/x` beside\n // `/*rest` answered `undefined` for `/app`. Same shape as the param arm:\n // try the branch on a scratch object, commit only if it structurally\n // completes, otherwise let the splat capture.\n //\n // ⚑ Gated on the splat sibling EXISTING, so a static hop in a tree with\n // no splat at that node — the common case, and the hot path — pays one\n // `undefined` check and nothing else. The recursion is entered only where\n // the junction is real.\n if (node.splatChild !== undefined) {\n const childParams: Record<string, string> = {};\n\n const taken = this.#traverseFrom(\n staticChild,\n path,\n segmentEnd + 1,\n childParams,\n );\n\n if (taken !== undefined) {\n // ⚑ NOT `Object.assign` (#1852) — see the param arm below.\n copyFields(params, childParams);\n\n return taken;\n }\n\n return this.#matchSplat(node.splatChild, path, start, params);\n }\n\n next = staticChild;\n } else if (node.paramChild) {\n const pc = node.paramChild;\n\n // #1288: validated sub-traverse on a param+splat junction. The param\n // branch is tried on a scratch object and commits ONLY if it structurally\n // completes (\"param wins if its branch can complete\", INVARIANTS Matching #8);\n // otherwise the splat sibling captures. Junction-free param hops (no splat\n // sibling) never enter this block — the common single-param path below is\n // untouched.\n if (node.splatChild !== undefined) {\n const childParams: Record<string, string> = { [pc.name]: segment };\n\n const taken = this.#traverseFrom(\n pc.node,\n path,\n segmentEnd + 1,\n childParams,\n );\n\n if (taken !== undefined) {\n // ⚑ NOT `Object.assign` (#1852): it copies with `[[Set]]`, one key\n // at a time, so it carries the identical hazard in a form no\n // `dst[key] = …` census can see. The literal above is safe —\n // `{ [k]: v }` DEFINES — and this commit of it was not.\n copyFields(params, childParams);\n\n return taken;\n }\n\n return this.#matchSplat(node.splatChild, path, start, params);\n }\n\n next = pc.node;\n // ⚑ The param name comes from the ROUTE TABLE, and this throw escapes\n // `matchPath` with no catch above it (#1852). The getter+setter shape\n // was worse: the route still MATCHED and committed empty `params`\n // beside a non-empty `path`.\n putField(params, pc.name, segment);\n } else if (node.splatChild) {\n return this.#matchSplat(node.splatChild, path, start, params);\n } else {\n return undefined;\n }\n\n node = next;\n start = segmentEnd + 1;\n }\n\n return node.slashChildRoute ?? node.route;\n }\n\n #matchSplat(\n splatChild: { node: SegmentNode; name: string },\n path: string,\n start: number,\n params: Record<string, string>,\n ): CompiledRoute | undefined {\n const sn = splatChild.node;\n\n // Stryker disable next-line BlockStatement: equivalent — leaf-splat fast path; the #traverseFrom fallback returns the same route+params (proven via hasChildren injection)\n if (!sn.hasChildren) {\n putField(params, splatChild.name, path.slice(start));\n\n return sn.route;\n }\n\n const childParams: Record<string, string> = {};\n const specific = this.#traverseFrom(sn, path, start, childParams);\n\n // #1288: a structurally-complete specific child wins over the wildcard\n // capture; otherwise the splat captures the rest of the path.\n if (specific) {\n // `[[Set]]` per key, same as the junction commit above (#1852).\n copyFields(params, childParams);\n\n return specific;\n }\n\n putField(params, splatChild.name, path.slice(start));\n\n return sn.route;\n }\n\n #decodeParams(params: Record<string, string>): boolean {\n const decode = this.#decode;\n\n if (!decode) {\n return true;\n }\n\n for (const key of objectKeys(params)) {\n // ⚑ `params` is a plain `{}` minted by `match()`, so this walk sees every\n // ENUMERABLE\n // member of `Object.prototype` — which an ordinary library extension\n // (`Object.prototype.foo = 1`) puts there, no attacker required. Without\n // this gate the inherited value reaches `value.includes(\"%\")` two lines\n // down: a non-string throws `TypeError` straight out of `match()`, which\n // this file's own contract says must never throw on INPUT, and a string\n // that is a bad percent sequence makes `decode` fail so EVERY dynamic URL\n // silently stops matching (#1840).\n //\n // ⚑ The gate closes the ENUMERATION axis; the WRITE axis is closed too\n // now, by `putField` at the sibling `#traverseFrom` write and at every\n // other site of the class (#1852). Both halves rest on measurement: a\n // prototype-less `params` is the EXPENSIVE horn, not the cheap one,\n // because V8 pays dictionary mode on every later READ (figures in\n // `putField`'s docblock). ⚠ The writes are NOT individually redundant —\n // neutralising the sibling `#traverseFrom` write alone re-opens the\n // inherited setter, red in `match-never-throws-on-input-1840.test.ts`.\n const value = params[key];\n\n // Stryker disable next-line StringLiteral,BlockStatement: equivalent — includes('%') is a skip-optimization; decoding a %-free value is a no-op, so always-proceeding is identical\n if (!value.includes(\"%\")) {\n continue;\n }\n\n // Stryker disable next-line BlockStatement: equivalent — redundant with the try/catch below — decodeURIComponent throws on the same invalid-% input (proven by injection)\n if (!validatePercentEncoding(value)) {\n return false;\n }\n\n try {\n params[key] = decode(value);\n } catch {\n // `validatePercentEncoding` only checks `%XX` *syntax*. A sequence that\n // is syntactically valid but semantically invalid UTF-8 (e.g. `%E0%41`,\n // `%C0%80`, `%FF`) still makes `decodeURIComponent`/`decodeURI` throw a\n // URIError. `match()` must never throw — reject the path so the router\n // resolves to UNKNOWN_ROUTE instead of crashing on start() (#737).\n return false;\n }\n }\n\n return true;\n }\n}\n\nexport { createSegmentNode } from \"./pathUtils\";\n","/**\n * Cache Computation.\n *\n * Computes all pre-computed caches and freezes the tree.\n *\n * @module builder/computeCaches\n */\n\nimport { buildParamMeta, EMPTY_PARAM_META } from \"../path-matcher\";\n\nimport type { MutableRouteNode } from \"./buildTree\";\nimport type { RouteTree } from \"../types\";\n\n/** Captured like the deciding seven, but this one BUILDS the guarantee (#2073). */\nconst freeze = Object.freeze;\n\n// =============================================================================\n// Shared Sentinels (avoid per-node allocation for leaf nodes)\n// =============================================================================\n\nconst EMPTY_CHILDREN_MAP: ReadonlyMap<string, RouteTree> = Object.freeze(\n new Map<string, RouteTree>(),\n);\nconst EMPTY_CHILDREN_ARRAY: readonly RouteTree[] = Object.freeze(\n [] as RouteTree[],\n);\n\n// =============================================================================\n// Cache Computation Functions\n// =============================================================================\n\n/**\n * Computes the full dot-notation name for a node.\n * Parent fullName is always computed before children.\n *\n * @param node - Node to compute for\n * @returns Full name (e.g., \"users.profile\")\n */\nfunction computeFullName(node: RouteTree): string {\n if (!node.parent?.name) {\n return node.name;\n }\n\n // Parent fullName is always set before children are processed\n return `${node.parent.fullName}.${node.name}`;\n}\n\n// =============================================================================\n// Main Computation Function\n// =============================================================================\n\n/**\n * Computes children Map from mutable children array.\n *\n * Children are stored in definition order. Matching priority is handled\n * by the segment trie, not by iteration order.\n *\n * @param childrenArray - Array of processed child nodes\n * @returns ReadonlyMap of child name -> child node (in definition order)\n */\nfunction computeChildrenMap(\n childrenArray: RouteTree[],\n): ReadonlyMap<string, RouteTree> {\n const map = new Map<string, RouteTree>();\n\n for (const child of childrenArray) {\n map.set(child.name, child);\n }\n\n return map;\n}\n\n/**\n * Recursively processes child nodes and computes the children map.\n *\n * @param mutableChildren - Array of mutable child nodes\n * @param parent - Already-processed parent node\n * @returns Children map and non-absolute children array\n */\nfunction processChildren(\n mutableChildren: readonly MutableRouteNode[],\n parent: RouteTree,\n): {\n childrenMap: ReadonlyMap<string, RouteTree>;\n nonAbsoluteChildren: RouteTree[];\n} {\n const childrenArray: RouteTree[] = [];\n const nonAbsoluteChildren: RouteTree[] = [];\n\n for (const childMutable of mutableChildren) {\n const child = processNode(childMutable, parent);\n\n childrenArray.push(child);\n\n if (!child.absolute) {\n nonAbsoluteChildren.push(child);\n }\n }\n\n return {\n childrenMap: computeChildrenMap(childrenArray),\n nonAbsoluteChildren,\n };\n}\n\n/**\n * Recursively processes a mutable node into a frozen RouteTree.\n *\n * This creates a new object with all caches computed and freezes it for\n * immutability.\n *\n * @param mutable - Mutable node to process\n * @param parent - Already-processed parent node (null for root)\n * @returns Frozen RouteTree\n */\nfunction processNode(\n mutable: MutableRouteNode,\n parent: RouteTree | null,\n): RouteTree {\n const freshParamMeta = buildParamMeta(mutable.path);\n // Fully-static node: every collection is a #1009 sentinel and pathPattern is\n // reference-equal to the input path (no query was sliced off) — the wrapper\n // carries zero information, so retain the ONE shared frozen EMPTY_PARAM_META\n // instead of a fresh object per node. The swap happens here (the\n // retaining consumer), NOT inside buildParamMeta: the validation gate reads\n // `pathPattern` off fresh results and must keep seeing the real pattern.\n const paramMeta =\n freshParamMeta.urlParams.length === 0 &&\n freshParamMeta.queryParams.length === 0 &&\n freshParamMeta.pathPattern === mutable.path\n ? EMPTY_PARAM_META\n : freshParamMeta;\n const paramTypeMap = paramMeta.paramTypeMap;\n\n // Skeleton node: children and nonAbsoluteChildren are set after recursive\n // child processing, which requires a parent reference to this node.\n const node = {\n name: mutable.name,\n path: mutable.path,\n absolute: mutable.absolute,\n parent,\n children: undefined as unknown as ReadonlyMap<string, RouteTree>,\n paramMeta,\n nonAbsoluteChildren: undefined as unknown as RouteTree[],\n // Stryker disable next-line StringLiteral: equivalent — placeholder overwritten unconditionally on the next statement (`node.fullName = computeFullName(node)`); the initial value is never observed.\n fullName: \"\",\n paramTypeMap,\n };\n\n node.fullName = computeFullName(node);\n\n if (mutable.children.length === 0) {\n node.children = EMPTY_CHILDREN_MAP;\n node.nonAbsoluteChildren = EMPTY_CHILDREN_ARRAY as RouteTree[];\n } else {\n const { childrenMap, nonAbsoluteChildren } = processChildren(\n mutable.children,\n node,\n );\n\n node.children = childrenMap;\n node.nonAbsoluteChildren = nonAbsoluteChildren;\n\n freeze(node.nonAbsoluteChildren);\n freeze(node.children);\n }\n\n freeze(paramTypeMap);\n\n // Close the immutability contract on the nested paramMeta (#747): the node\n // is frozen, but its paramMeta object and arrays were left mutable, so a\n // tree reachable from the public API could be mutated. paramTypeMap is the\n // same ref frozen just above.\n freeze(paramMeta.urlParams);\n freeze(paramMeta.queryParams);\n freeze(paramMeta);\n\n freeze(node);\n\n return node;\n}\n\n/**\n * Computes all caches and freezes the tree.\n *\n * This is the final step in building a RouteTree — the result is completely\n * immutable.\n *\n * @param mutableRoot - Mutable root node\n * @returns Frozen RouteTree\n */\nexport function computeCaches(mutableRoot: MutableRouteNode): RouteTree {\n return processNode(mutableRoot, null);\n}\n","/**\n * Route Tree Builder.\n *\n * Creates an immutable RouteTree from route definitions.\n *\n * @module builder/createRouteTree\n */\n\nimport { buildTree } from \"./buildTree\";\nimport { computeCaches } from \"./computeCaches\";\n\nimport type { RouteDefinition, RouteTree } from \"../types\";\n\n/**\n * Creates an immutable RouteTree from route definitions.\n *\n * @param name - Root node name (typically empty string)\n * @param path - Root node path (typically empty string)\n * @param routes - Route definitions to add\n * @returns Immutable RouteTree\n *\n * @example\n * ```typescript\n * const tree = createRouteTree(\"\", \"\", [\n * { name: \"users\", path: \"/users\" },\n * { name: \"users.profile\", path: \"/:id\" },\n * ]);\n * ```\n */\nexport function createRouteTree(\n name: string,\n path: string,\n routes: readonly RouteDefinition[],\n): RouteTree {\n return computeCaches(buildTree(name, path, routes));\n}\n","/**\n * RouteTree to RouteDefinition Converter.\n *\n * Converts a RouteTree back to an array of RouteDefinition objects.\n * Used for serialization and router cloning.\n *\n * @module operations/routeTreeToDefinitions\n */\n\nimport { putField } from \"../../utils/ingest\";\n\nimport type { RouteDefinition, RouteTree } from \"../types\";\n\n/**\n * Converts a single RouteTree node to a RouteDefinition.\n *\n * If the node has absolute=true, the path is prefixed with '~'\n * to reconstruct the original definition format.\n *\n * @param node - RouteTree node to convert\n * @returns RouteDefinition with name, path, and optional children\n */\nexport function nodeToDefinition(node: RouteTree): RouteDefinition {\n // Reconstruct absolute path marker if needed\n const path = node.absolute ? `~${node.path}` : node.path;\n\n const def: RouteDefinition = {\n name: node.name,\n path,\n };\n\n if (node.children.size > 0) {\n // ⚑ `putField`, the same write rule the registration walk carries\n // (#1852 / #2139): `def` is a literal with `name` and `path` on it, so\n // `children` has no own slot here and a plain assignment walks the\n // prototype into an ambient accessor.\n putField(\n def as unknown as Record<string, unknown>,\n \"children\",\n Array.from(node.children.values(), nodeToDefinition),\n );\n }\n\n return def;\n}\n\n/**\n * Converts a RouteTree back to an array of RouteDefinition objects.\n *\n * This is the inverse of createRouteTree - it extracts the minimal\n * definition data (name, path, children) from the computed tree.\n *\n * Note: Extra properties stored in RouteDefinition (like canActivate,\n * forwardTo, encodeParams, etc.) are NOT preserved in RouteTree,\n * so they won't be present in the output.\n *\n * @example\n * ```ts\n * const tree = createRouteTree(\"\", \"\", [\n * { name: \"users\", path: \"/users\", children: [\n * { name: \"profile\", path: \"/:id\" }\n * ]}\n * ]);\n *\n * const definitions = routeTreeToDefinitions(tree);\n * // [{ name: \"users\", path: \"/users\", children: [{ name: \"profile\", path: \"/:id\" }] }]\n * ```\n *\n * @param tree - RouteTree to convert\n * @returns Array of RouteDefinition objects (top-level routes only)\n */\nexport function routeTreeToDefinitions(tree: RouteTree): RouteDefinition[] {\n return Array.from(tree.children.values(), nodeToDefinition);\n}\n","/**\n * Decoding functions for search-params.\n *\n * Extracted from encode.ts for better separation of concerns.\n *\n * @module search-params/decode\n */\n\nimport type { ResolvedStrategies } from \"./strategies\";\nimport type { DecodeResult } from \"./types\";\n\n// =============================================================================\n// Value Decoding\n// =============================================================================\n\n/**\n * Decodes a URL-encoded value, handling + as space.\n * Optimized: only replaces + if present, skips decodeURIComponent if not needed.\n *\n * Uses indexOf for checking presence - more mutation-resistant than includes\n * because indexOf(\"x\") returns -1 when not found, while includes(\"\") always returns true.\n */\nexport const decodeValue = (value: string): string => {\n // Stryker disable next-line StringLiteral: equivalent — indexOf(\"%\") -> indexOf(\"\") returns 0, forcing the full decode path; decodeURIComponent is a no-op on a %-free string, so the result is identical (proven by injection). See the indexOf rationale above.\n const percentIdx = value.indexOf(\"%\");\n // Stryker disable next-line StringLiteral: equivalent — indexOf(\"+\") -> indexOf(\"\") returns 0, forcing replaceAll(\"+\",\" \") which is a no-op on a +-free string (proven by injection).\n const plusIdx = value.indexOf(\"+\");\n\n // Fast path: no encoding needed (common case - most values are simple)\n if (percentIdx === -1 && plusIdx === -1) {\n return value;\n }\n\n // Only replace + if present (avoid regex overhead)\n const withSpaces = plusIdx === -1 ? value : value.replaceAll(\"+\", \" \");\n\n // Only decode if % is present\n return percentIdx === -1 ? withSpaces : decodeURIComponent(withSpaces);\n};\n\n// =============================================================================\n// Main Decode\n// =============================================================================\n\n/**\n * Decodes a query parameter value using resolved strategies.\n *\n * @param value - Raw value from query string (undefined for key-only params)\n * @param strategies - Pre-resolved format strategies\n * @returns Decoded value (string, boolean, or null)\n */\nexport const decode = (\n value: string | undefined,\n strategies: ResolvedStrategies,\n): DecodeResult => {\n // Handle undefined (key-only params like ?flag)\n if (value === undefined) {\n return strategies.boolean.decodeUndefined();\n }\n\n // Check raw value for boolean formats (e.g., \"true\"/\"false\")\n const rawResult = strategies.boolean.decodeRaw(value);\n\n if (rawResult !== null) {\n return rawResult;\n }\n\n // Decode URI and check decoded value for boolean formats (e.g., ✓/✗)\n const decoded = decodeValue(value);\n\n const numberResult = strategies.number.decode(decoded);\n\n if (numberResult !== null) {\n return numberResult;\n }\n\n return strategies.boolean.decodeValue(decoded);\n};\n","/**\n * Utility functions for search-params.\n *\n * Internalized from https://github.com/troch/search-params\n *\n * @module search-params/utils\n */\n\n// =============================================================================\n// Total Percent-Encoding\n// =============================================================================\n\n/**\n * A lone (unpaired) surrogate — a high surrogate not followed by a low, or a low\n * not preceded by a high. This is a manual, lib-target-agnostic\n * `String.prototype.toWellFormed` (ES2024): consumers compile this `src` under their\n * OWN `tsconfig` (whose `lib` may predate es2024 — e.g. `hash-plugin`), so a regex\n * `replace` avoids a `toWellFormed` type error in every consumer while producing the\n * identical result (verified by parity across the surrogate space).\n */\nconst LONE_SURROGATE_RGX =\n /[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/g;\n\n/**\n * Percent-encodes a value, staying TOTAL on a lone (unpaired) surrogate.\n *\n * `encodeURIComponent` throws `URIError` on an unpaired UTF-16 surrogate — the\n * only input it rejects. `parseQuery` accepts such a value (its non-percent decode\n * is an identity fast path), so `build(parseQuery(qs))` would throw and violate the\n * inverse-pair totality invariant `range(parseQuery) ⊆ dom(build)` (INVARIANTS\n * Parse/Build #12). We sanitize each lone surrogate to U+FFFD instead: the first\n * round-trip mutates the (already non-round-trippable) garbage, then stabilises;\n * well-formed inputs are untouched. Single source for BOTH encode sites — scalar/key\n * (`encode.ts`) and array element (`strategies/array.ts`) — so they cannot drift\n * (#1314).\n */\nexport const safeEncode = (value: string | number | boolean): string => {\n try {\n return encodeURIComponent(value);\n } catch (error) {\n // Only a lone surrogate (URIError) is sanitized. Anything else is a real error\n // the caller must see — e.g. a Symbol value throws `TypeError` here, and\n // `String(symbol)` would silently coerce it to \"Symbol(…)\" instead of rethrowing.\n if (!(error instanceof URIError)) {\n throw error;\n }\n\n // Lone surrogate → U+FFFD, keeping build total.\n return encodeURIComponent(\n String(value).replaceAll(LONE_SURROGATE_RGX, \"�\"),\n );\n }\n};\n","/**\n * Array encoding strategies.\n *\n * @module search-params/strategies/array\n */\n\nimport { safeEncode } from \"../utils\";\n\nimport type { NullStrategy } from \"./null\";\nimport type { ArrayFormat } from \"../types\";\n\n// =============================================================================\n// Strategy Interface\n// =============================================================================\n\n/**\n * Strategy for encoding array values.\n */\nexport interface ArrayStrategy {\n /**\n * Encodes an array as a query string segment.\n *\n * @param name - URL-encoded parameter name\n * @param values - Array values to encode\n * @param nullStrategy - Null strategy, so a `null` element encodes to the same\n * wire token a scalar null does (bare key under `default`, dropped under\n * `hidden`) — closing `range(parse) ⊆ dom(build)` (#1155)\n * @returns Query string segment (e.g., \"items=a&items=b\" or \"items=a,b\")\n */\n encodeArray: (\n name: string,\n values: unknown[],\n nullStrategy: NullStrategy,\n ) => string;\n\n /**\n * Splits a raw (URI-encoded) value into array parts during parsing.\n * Returns null if the value is not an array in this format.\n *\n * @param rawValue - Raw value before URI decoding\n * @returns Array of raw parts, or null if not an array\n */\n decodeValue?: (rawValue: string) => string[] | null;\n\n /**\n * When true, the parser orders bracketed elements (`a[n]`) by the numeric\n * index `n` rather than insertion order. Only `index` format sets this. (#856)\n */\n indexed?: boolean;\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\n// Encodes a non-null array element. `null` is handled per-format by the caller\n// (bare-key / skip); this throws only on genuinely unserialisable elements\n// (`undefined`, objects) — which `parse` never produces in an array.\nconst encodeValue = (value: unknown): string => {\n const type = typeof value;\n\n if (type !== \"string\" && type !== \"number\" && type !== \"boolean\") {\n // `null` is handled by the caller (bare-key / skip) and never reaches here,\n // so `type` names the offender directly (`undefined`, `object`, `symbol`, …).\n // ⚑ Bare `[router]`, not `[router.buildPath]`. This is a diagnosis of a\n // VALUE, and the URL build it sits in is reached from `navigate`,\n // `navigateToDefault` and `makeState` as well as from `buildPath` — FOUR\n // core doors, instrumented at the throw site. ⚠ Not six: `navigateToState`\n // never reaches it, and `getStaticPaths` lives in `@real-router/ssr-utils`\n // rather than core, so neither is a call here.\n // Core's own log then printed\n // `[router.navigate] Unexpected navigation error TypeError:\n // [router.buildPath] …`, two contradictory attributions on one line. The\n // sibling refusals name a call because they HAVE one; this one does not, so\n // it names none rather than naming the wrong one.\n throw new TypeError(\n `[router] Invalid query value: an array element must be a string, number, or boolean — received ${type}`,\n );\n }\n\n return safeEncode(value as string | number | boolean);\n};\n\n// Repeats `${name}${suffix}` keys joined by `&`. Shared between `none` (suffix\n// `\"\"`) and `brackets` (suffix `\"[]\"`). A `null` element encodes to the SAME\n// wire token a scalar null does via `nullStrategy` — the bare key\n// `${name}${suffix}` under `nullFormat: \"default\"`, or `\"\"` (dropped, filtered\n// below so no `&&` appears) under `\"hidden\"`. So `parseQuery(\"a&a=1\")` →\n// `{a:[null,\"1\"]}` round-trips to `\"a&a=1\"` instead of throwing (#1155).\nconst repeatKey = (\n name: string,\n values: unknown[],\n suffix: string,\n nullStrategy: NullStrategy,\n): string => {\n const key = `${name}${suffix}`;\n const parts: string[] = [];\n\n for (const value of values) {\n if (value === null) {\n const encoded = nullStrategy.encode(key);\n\n if (encoded) {\n parts.push(encoded);\n }\n } else {\n parts.push(`${key}=${encodeValue(value)}`);\n }\n }\n\n return parts.join(\"&\");\n};\n\n// =============================================================================\n// Strategy Implementations\n// =============================================================================\n\n/**\n * Repeated keys without brackets.\n * Example: items=a&items=b\n */\nexport const noneArrayStrategy: ArrayStrategy = {\n encodeArray: (name, values, nullStrategy) =>\n repeatKey(name, values, \"\", nullStrategy),\n};\n\n/**\n * Bracket notation without index.\n * Example: items[]=a&items[]=b\n */\nexport const bracketsArrayStrategy: ArrayStrategy = {\n encodeArray: (name, values, nullStrategy) =>\n repeatKey(name, values, \"[]\", nullStrategy),\n};\n\n/**\n * Indexed bracket notation.\n * Example: items[0]=a&items[1]=b\n */\nexport const indexArrayStrategy: ArrayStrategy = {\n encodeArray: (name, values, nullStrategy) => {\n const parts: string[] = [];\n\n for (const [i, value] of values.entries()) {\n const key = `${name}[${i}]`;\n\n if (value === null) {\n const encoded = nullStrategy.encode(key);\n\n if (encoded) {\n parts.push(encoded);\n }\n } else {\n parts.push(`${key}=${encodeValue(value)}`);\n }\n }\n\n return parts.join(\"&\");\n },\n\n indexed: true,\n};\n\n/**\n * Comma-separated values.\n * Example: items=a,b,c\n *\n * Comma has no per-element bare-key form (an empty part like `a=,` decodes to\n * the empty string, not `null`), so a `null` element is unrepresentable and\n * dropped. `parse` only yields null-in-array under `comma` via a bracketed\n * chunk (`a[]`) — a wire/format mismatch, never the comma-native path — so this\n * is a total-but-lossy edge. The `nullStrategy` arg is intentionally omitted\n * (a 2-arg impl satisfies the 3-arg interface).\n */\nexport const commaArrayStrategy: ArrayStrategy = {\n encodeArray: (name, values) => {\n const parts: string[] = [];\n\n for (const value of values) {\n if (value !== null) {\n parts.push(encodeValue(value));\n }\n }\n\n if (parts.length === 0) {\n return \"\";\n }\n\n return `${name}=${parts.join(\",\")}`;\n },\n\n decodeValue: (rawValue) => {\n // No unencoded comma → not an array (single value).\n // Encoded commas (%2C) are part of the value, not separators.\n if (!rawValue.includes(\",\")) {\n return null;\n }\n\n return rawValue.split(\",\");\n },\n};\n\n/**\n * Map of array format to strategy implementation.\n */\nexport const arrayStrategies: Record<ArrayFormat, ArrayStrategy> = {\n none: noneArrayStrategy,\n brackets: bracketsArrayStrategy,\n index: indexArrayStrategy,\n comma: commaArrayStrategy,\n};\n","/**\n * Boolean encoding/decoding strategies.\n *\n * @module search-params/strategies/boolean\n */\n\nimport type { DecodeResult, BooleanFormat } from \"../types\";\n\n// =============================================================================\n// Strategy Map\n// =============================================================================\n\n// =============================================================================\n// Strategy Interface\n// =============================================================================\n\n/**\n * Strategy for encoding/decoding boolean values.\n */\nexport interface BooleanStrategy {\n /**\n * Encodes a boolean value as a query string segment.\n *\n * @param name - URL-encoded parameter name\n * @param value - Boolean value to encode\n * @returns Query string segment (e.g., \"flag=true\" or just \"flag\")\n */\n encode: (name: string, value: boolean) => string;\n\n /**\n * Handles undefined value (key-only params like ?flag).\n *\n * @returns Decoded value for undefined\n */\n decodeUndefined: () => DecodeResult;\n\n /**\n * Decodes a raw (not URI-decoded) value before decodeValue() is called.\n * Used for formats that check raw values (e.g., \"auto\" checks \"true\"/\"false\").\n *\n * @param rawValue - Raw value before URI decoding\n * @returns boolean if matched, null to continue with URI decoding\n */\n decodeRaw: (rawValue: string) => boolean | null;\n\n /**\n * Decodes a URI-decoded value.\n *\n * @param decodedValue - Value after URI decoding\n * @returns Decoded value (boolean or the same string)\n */\n decodeValue: (decodedValue: string) => DecodeResult;\n}\n\n// =============================================================================\n// Strategy Implementations\n// =============================================================================\n\n/**\n * No special boolean handling - values are treated as strings.\n */\nexport const noneBooleanStrategy: BooleanStrategy = {\n encode: (name, value) => `${name}=${value}`,\n decodeUndefined: () => null,\n decodeRaw: () => null, // No raw value matching\n decodeValue: (decoded) => decoded, // Return as-is\n};\n\n/**\n * Auto-detect \"true\"/\"false\" strings as boolean values.\n * Checks raw value before URI decoding.\n */\nexport const autoBooleanStrategy: BooleanStrategy = {\n encode: (name, value) => `${name}=${value}`,\n decodeUndefined: () => null,\n decodeRaw: (raw) => {\n if (raw === \"true\") {\n return true;\n }\n if (raw === \"false\") {\n return false;\n }\n\n return null; // Continue with URI decoding\n },\n decodeValue: (decoded) => decoded, // Return decoded string\n};\n\n/**\n * Scalar true values are key-only (no =value); scalar false keeps an explicit\n * `=false`. Example: `?flag` for true, `?flag=false` for false. Array elements\n * always carry an explicit value (`?a=true&a=false`), so decoding must map both\n * `\"true\"`→true and `\"false\"`→false to round-trip booleans losslessly in either\n * position — a key-only param is `true` (decodeUndefined), and any explicit\n * `=true`/`=false` decodes back to the boolean (not the string).\n */\nexport const emptyTrueBooleanStrategy: BooleanStrategy = {\n encode: (name, value) => (value ? name : `${name}=false`),\n decodeUndefined: () => true, // Key-only means true\n decodeRaw: (raw) => {\n if (raw === \"true\") {\n return true;\n }\n if (raw === \"false\") {\n return false;\n }\n\n return null; // Continue with URI decoding\n },\n decodeValue: (decoded) => decoded, // Return as-is\n};\n\n/**\n * Map of boolean format to strategy implementation.\n */\nexport const booleanStrategies: Record<BooleanFormat, BooleanStrategy> = {\n none: noneBooleanStrategy,\n auto: autoBooleanStrategy,\n \"empty-true\": emptyTrueBooleanStrategy,\n};\n","/**\n * Null encoding/decoding strategies.\n *\n * @module search-params/strategies/null\n */\n\n// =============================================================================\n// Strategy Interface\n// =============================================================================\n\n/**\n * Strategy for encoding/decoding null values.\n */\n// =============================================================================\n// Strategy Map\n// =============================================================================\n\nimport type { NullFormat } from \"../types\";\n\nexport interface NullStrategy {\n /**\n * Encodes a null value as a query string segment.\n *\n * @param name - URL-encoded parameter name\n * @returns Query string segment (e.g., \"key\" or \"\")\n */\n encode: (name: string) => string;\n}\n\n// =============================================================================\n// Strategy Implementations\n// =============================================================================\n\n/**\n * Default null format - key only without value.\n * Example: ?key (no equals sign)\n */\nexport const defaultNullStrategy: NullStrategy = {\n encode: (name) => name,\n};\n\n/**\n * Null values are hidden (omitted from query string).\n * Example: (nothing)\n */\nexport const hiddenNullStrategy: NullStrategy = {\n encode: () => \"\",\n};\n\n/**\n * Map of null format to strategy implementation.\n */\nexport const nullStrategies: Record<NullFormat, NullStrategy> = {\n default: defaultNullStrategy,\n hidden: hiddenNullStrategy,\n};\n","/**\n * Number decoding strategies.\n *\n * @module search-params/strategies/number\n */\n\nimport type { NumberFormat } from \"../types\";\n\n// =============================================================================\n// Strategy Interface\n// =============================================================================\n\n/**\n * Strategy for decoding number values.\n */\nexport interface NumberStrategy {\n decode: (decodedValue: string) => number | null;\n}\n\n// =============================================================================\n// Strategy Implementations\n// =============================================================================\n\n/**\n * No special number handling - values remain strings.\n */\nexport const noneNumberStrategy: NumberStrategy = {\n decode: () => null, // passthrough\n};\n\n/**\n * Auto-detect numeric values and parse as numbers.\n * Matches integers and decimals via charCode scan (faster than regex for short strings).\n *\n * Recognizes an optional leading `-` so negatives round-trip symmetrically with the\n * values `navigate()`/`build()` produce (`build({ n: -5 })` → `\"n=-5\"`). Leading-zero\n * and unsafe-integer rejection apply to the magnitude regardless of sign. Exponent\n * notation stays a string — `build` never emits a canonical safe exponent, and unsafe\n * exponents would lose precision. (#742)\n */\nexport const autoNumberStrategy: NumberStrategy = {\n decode: (value) => {\n const length = value.length;\n\n if (length === 0) {\n return null;\n }\n\n // Optional leading minus; the magnitude (digits) begins at `start`.\n const start = value.codePointAt(0) === 45 ? 1 : 0; // '-'\n\n // A bare \"-\" has no magnitude.\n if (start === length) {\n return null;\n }\n\n // Leading zeros are not canonical numbers (\"00\", \"007\", \"-007\") — preserve as strings.\n // Allow \"0\" and \"0.x\" (single zero or decimal starting with 0).\n if (\n length - start > 1 &&\n value.codePointAt(start) === 48 &&\n value.codePointAt(start + 1) !== 46\n ) {\n return null;\n }\n\n let hasDot = false;\n\n for (let i = start; i < length; i++) {\n const ch = value.codePointAt(i);\n\n if (ch !== undefined && ch >= 48 && ch <= 57) {\n continue; // '0'-'9'\n }\n\n if (ch === 46 && !hasDot && i !== start && i !== length - 1) {\n hasDot = true;\n\n continue;\n }\n\n return null; // non-digit, non-dot, or invalid dot position\n }\n\n const num = Number(value);\n\n // The stated criterion is round-trip stability, enforced directly rather than\n // through per-case guards: `build` prints a number with String(), so a value\n // may be coerced only when String() reproduces the exact text we parsed. This\n // subsumes the former negative-zero guard (String(-0) === \"0\" ≠ \"-0\", #898)\n // and closes the decimal family the safe-integer check below deliberately\n // exempts — trailing zeros (\"2.0\" → 2 → \"2\") and precision loss\n // (\"9007199254740993.5\" → 9007199254740994), both of which rebuilt a URL\n // different from the one that was matched. (#1565)\n if (String(num) !== value) {\n return null;\n }\n\n // Unsafe integers pass the text check (2**53 prints back exactly) but lose\n // precision in arithmetic, so they stay strings. Decimals are exempt: a\n // fractional value is never a \"safe integer\", and the check above already\n // proves its text round-trips.\n if (!Number.isSafeInteger(num) && !hasDot) {\n return null;\n }\n\n return num;\n },\n};\n\n// =============================================================================\n// Strategy Map\n// =============================================================================\n\n/**\n * Map of number format to strategy implementation.\n */\nexport const numberStrategies: Record<NumberFormat, NumberStrategy> = {\n auto: autoNumberStrategy,\n none: noneNumberStrategy,\n};\n","/**\n * Search Params Strategies - Factory and Types.\n *\n * Provides a unified interface for format-specific encoding/decoding strategies.\n * Strategies are resolved once when options are created, avoiding repeated\n * format checks during encode/decode operations.\n *\n * @module search-params/strategies\n */\n\nimport { arrayStrategies, type ArrayStrategy } from \"./array\";\nimport { booleanStrategies, type BooleanStrategy } from \"./boolean\";\nimport { nullStrategies, type NullStrategy } from \"./null\";\nimport { numberStrategies, type NumberStrategy } from \"./number\";\n\nimport type { FinalOptions } from \"../types\";\n\n/**\n * Intrinsics captured at module load: `defineProperty`, `objectKeys`, `hasOwn`.\n *\n * ⚑ A guard is only as strong as the intrinsic it reads WHEN IT RUNS, and an\n * application can re-point any of these AFTER boot — which is what this closes.\n * Measured on the uncaptured form: one naive `Object.hasOwn` polyfill walked\n * straight through five sibling readers while the single captured guard held.\n *\n * ⚠ The limit of what capture buys — and the shim order that defeats it — is\n * stated once, in `guards.ts`. Not restated here (#2091).\n */\nconst defineProperty = Object.defineProperty;\nconst objectKeys = Object.keys;\nconst hasOwn = Object.hasOwn;\n// =============================================================================\n// Exports\n// =============================================================================\n\nexport type { ArrayStrategy } from \"./array\";\n\nexport type { BooleanStrategy } from \"./boolean\";\n\nexport type { NullStrategy } from \"./null\";\n\nexport type { NumberStrategy } from \"./number\";\n\n// =============================================================================\n// Resolved Strategies\n// =============================================================================\n\n/**\n * Pre-resolved strategies based on options.\n * Created once when makeOptions() is called, avoiding repeated lookups.\n */\nexport interface ResolvedStrategies {\n readonly boolean: BooleanStrategy;\n readonly null: NullStrategy;\n readonly number: NumberStrategy;\n readonly array: ArrayStrategy;\n}\n\n/**\n * `Object.hasOwn`, captured before any application code can run.\n *\n * ⚑ This is the RAISER of the config fault `SegmentMatcher`'s predicate exists\n * to recognise, so leaving it reading the mutable global made hardening that\n * predicate pointless: measured, `Object.hasOwn = () => true` after boot let an\n * invalid format through `createRouter` and every query URL then resolved to\n * `UNKNOWN_ROUTE` — the #1318 symptom, restored.\n */\n\nexport const CONFIG_FAULT: unique symbol = Symbol.for(\n \"real-router.searchParams.configFault\",\n);\n\n/**\n * Fail fast on an unknown format. A `queryParams` typo in a JS consumer (no TS to\n * forbid it) otherwise indexes the strategy map to `undefined`, deferring a cryptic\n * `TypeError` to first use — which the router's `SegmentMatcher.#mergeQueryParams`\n * catch-all then masks as `UNKNOWN_ROUTE` for EVERY query URL, with zero diagnostics\n * (#1318). TS consumers are unaffected (the union types already forbid the typo).\n */\nconst requireStrategy = <T>(\n table: Record<string, T>,\n // ⚠ `unknown`, not `string`. The declared type is exactly what this guard\n // cannot trust: a TS consumer is already forbidden the typo by the union, so\n // every value that reaches the throw came from a JS consumer or a\n // runtime-assembled config. Typing it `string` made `String(value)` look like\n // a no-op to the linter, which is the same false confidence in reverse.\n value: unknown,\n field: string,\n): T => {\n // `Object.hasOwn` on the table, NOT `=== undefined` on a lookup the caller\n // already performed (#1796). These tables are plain object literals indexed by\n // a string the consumer supplies, so for any of `Object.prototype`'s twelve own\n // members the lookup returns a MEMBER instead of `undefined` (eleven of the\n // twelve are functions; `__proto__` yields `Object.prototype` itself, which\n // fails one step later since it carries no `encode` / `encodeArray`): the\n // `undefined` test passed and that member was installed as the live strategy —\n // precisely the deferred\n // `TypeError` this guard exists to prevent, reached through the one value class\n // its predicate could not see.\n //\n // The guard OWNS the lookup for the same reason. A predicate handed the RESULT\n // of someone else's read cannot tell \"absent\" from \"inherited\"; asking the\n // container directly is what makes the two inseparable.\n //\n // ⚑ And it owns the KEY, for the same reason once more. Owning the lookup is\n // only half of it: `Object.hasOwn` and the `table[…]` below each run\n // `ToPropertyKey`, so passing the caller's VALUE through both reads it twice.\n // A `{ toString }` answering \"none\" to the guard and \"toString\" to the lookup\n // was admitted as one format and used as another, which is the deferred\n // `opts.strategies.array.encodeArray is not a function` this guard exists to\n // prevent — the same defect one layer out from the one it fixed. One\n // coercion, above the check, and verdict and use cannot disagree.\n // ⚠ `typeof` first, not a bare `String(value)` — and NOT as a hot-path term.\n // Resolution happens at matcher construction, four calls per ROUTER, not\n // twice per `matchPath` where an unconditional coercion measured +3.5 %. It\n // stays because for a real string it returns the value untouched,\n // which is simply the honest shape, and because the coercion is reserved for\n // exactly the values this guard exists to refuse.\n const key = typeof value === \"string\" ? value : String(value);\n\n // ⚠ One consequence worth naming: a SYMBOL now yields this named error instead\n // of `Cannot convert a Symbol value to a string`. The guard always detected it\n // — `Object.hasOwn` answered `false` — but building the message threw from the\n // template, so the named error never reached the caller for that one class.\n\n // ⚑ `[router.constructor]`, and the option's FULL PATH. A `[search-params]`\n // prefix names a layer that is not a package (#1510) and that the caller never\n // wrote; pairing it with the bare field points at neither a thing the user\n // typed nor a thing they could look up.\n //\n // ⚑ The prefix is `[router.constructor]` and not an invented\n // `[router.options]`, on two counts. Core has ELEVEN `[router.*]` prefixes and\n // every one of them names the CALL the user made, so a namespace there would\n // be the only exception. And `@real-router/validation-plugin` prints\n // `[router.constructor] Invalid \"queryParams.<key>\"` for this exact option —\n // agreeing with it is the whole point, since the hoist makes the plugin's\n // message unreachable for these four fields.\n //\n // ⚠ The objection this survives — that the hoist also runs from `cloneRouter`\n // and every matcher rebuild, so the prefix would be false on most doors — is\n // refuted by measurement: the snapshot and its container are both frozen, so\n // a rebuild has nothing left that can fail, and `cloneRouter` raises through\n // `new RouterClass(...)`. Both doors that can raise ARE the constructor.\n if (!hasOwn(table, key)) {\n const error = new TypeError(\n `[router.constructor] Invalid \"queryParams.${field}\": \"${key}\" — expected ${objectKeys(\n table,\n )\n .map((name) => `\"${name}\"`)\n .join(\" | \")}`,\n );\n\n // ⚑ TAGGED, because the parse catch must recognise this by ORIGIN and not by\n // class. `match()` must never throw on INPUT, and the catch around the parse\n // is that contract's backstop; narrowing it to \"rethrow anything that is not\n // a `URIError`\" inverted the default from fail-safe to fail-open, and the\n // enumeration of throwers behind that inversion was incomplete. `assignParam`\n // writes `params[name] = value` with the name taken from the URL, so an\n // application setter on `Object.prototype` for that name runs inside the\n // guarded `try`, and its error is neither a `URIError` nor ours. Measured: a\n // throwing setter for a key a URL supplies propagated out of `matchPath` —\n // into the popstate handlers of the three URL plugins and `preload-plugin`'s\n // hover path, none of which catch.\n //\n // A marker on OUR error lets the catch rethrow exactly what it means to\n // rethrow — the config fault #1796 refuses to swallow — and go on swallowing\n // everything else, which is what the contract says. A property rather than a\n // subclass: the message and the `TypeError` identity are what consumers see,\n // and neither moves.\n defineProperty(error, CONFIG_FAULT, { value: true });\n\n throw error;\n }\n\n return table[key];\n};\n\nexport const resolveStrategies = (\n arrayFormat: FinalOptions[\"arrayFormat\"],\n booleanFormat: FinalOptions[\"booleanFormat\"],\n nullFormat: FinalOptions[\"nullFormat\"],\n numberFormat: FinalOptions[\"numberFormat\"],\n): ResolvedStrategies => ({\n boolean: requireStrategy(booleanStrategies, booleanFormat, \"booleanFormat\"),\n null: requireStrategy(nullStrategies, nullFormat, \"nullFormat\"),\n number: requireStrategy(numberStrategies, numberFormat, \"numberFormat\"),\n array: requireStrategy(arrayStrategies, arrayFormat, \"arrayFormat\"),\n});\n\n// =============================================================================\n// Default Strategies\n// =============================================================================\n\n/**\n * Default strategies matching DEFAULT_OPTIONS.\n * Used when no custom options are provided.\n */\nexport const DEFAULT_STRATEGIES: ResolvedStrategies = Object.freeze({\n boolean: booleanStrategies.auto,\n null: nullStrategies.default,\n number: numberStrategies.auto,\n array: arrayStrategies.none,\n});\n","/**\n * Encoding functions for search-params.\n *\n * Uses Strategy pattern for format-specific encoding.\n * Strategies are resolved once when options are created.\n *\n * @module search-params/encode\n */\n\nimport {\n DEFAULT_STRATEGIES,\n resolveStrategies,\n type ResolvedStrategies,\n} from \"./strategies\";\nimport { safeEncode } from \"./utils\";\n\nimport type { FinalOptions, Options } from \"./types\";\n\n// =============================================================================\n// Default Query Params\n// =============================================================================\n\n/**\n * Default query parameter options. Single source of truth for all packages.\n */\nexport const DEFAULT_QUERY_PARAMS: FinalOptions = Object.freeze({\n arrayFormat: \"none\",\n booleanFormat: \"auto\",\n nullFormat: \"default\",\n numberFormat: \"auto\",\n});\n\n// =============================================================================\n// Options with Strategies\n// =============================================================================\n\n/**\n * Extended options with pre-resolved strategies.\n */\nexport interface OptionsWithStrategies extends FinalOptions {\n readonly strategies: ResolvedStrategies;\n}\n\n/**\n * Cached default options with strategies - avoids allocation when no options passed.\n *\n * ⚑ FROZEN, and both siblings above with it. `makeOptions` hands this exact object\n * back by reference — a pinned perf invariant, not an accident — so an unfrozen\n * one is a process-global (the #897 class: `LEVEL_CONFIGS` exported unfrozen\n * corrupted the global threshold). Nothing in the engine mutates any of the\n * three; they are read-only by intent, and now by construction.\n *\n * ⚠ Who actually reaches THIS object is narrower than \"every default router\", and\n * the distinction is worth keeping straight because the two singletons differ.\n * `OptionsNamespace` fills `queryParams` with `DEFAULT_QUERY_PARAMS`, whose four\n * fields are all DEFINED — so the all-undefined guard below does not fire and a\n * default-configured router gets a FRESH object. `DEFAULT_QUERY_PARAMS` is the\n * one every such router shares by reference; this one is reached by a caller that\n * passes nothing, or a bag with no format set.\n *\n * Frozen HERE, at its own site, because nothing else SEALS it: it is\n * `OptionsNamespace`'s default `queryParams`, and that freeze stops at the level\n * core owns (#1832).\n */\nconst DEFAULT_OPTIONS: OptionsWithStrategies = Object.freeze({\n ...DEFAULT_QUERY_PARAMS,\n strategies: DEFAULT_STRATEGIES,\n});\n\n/**\n * Creates options with defaults and pre-resolved strategies.\n * Returns cached DEFAULT_OPTIONS when no custom options are specified.\n */\nexport const makeOptions = (opts?: Options): OptionsWithStrategies => {\n if (\n !opts ||\n (opts.arrayFormat === undefined &&\n opts.booleanFormat === undefined &&\n opts.nullFormat === undefined &&\n opts.numberFormat === undefined)\n ) {\n return DEFAULT_OPTIONS;\n }\n\n // Avoid object spread - direct property assignment is faster\n const arrayFormat = opts.arrayFormat ?? DEFAULT_QUERY_PARAMS.arrayFormat;\n const booleanFormat =\n opts.booleanFormat ?? DEFAULT_QUERY_PARAMS.booleanFormat;\n const nullFormat = opts.nullFormat ?? DEFAULT_QUERY_PARAMS.nullFormat;\n const numberFormat = opts.numberFormat ?? DEFAULT_QUERY_PARAMS.numberFormat;\n\n return {\n arrayFormat,\n booleanFormat,\n nullFormat,\n numberFormat,\n strategies: resolveStrategies(\n arrayFormat,\n booleanFormat,\n nullFormat,\n numberFormat,\n ),\n };\n};\n\n// =============================================================================\n// Value Encoding\n// =============================================================================\n\n/**\n * Encodes a value for use in a URL query string.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument\nexport const encodeValue = (value: any): string => safeEncode(value);\n\n// =============================================================================\n// Main Encode\n// =============================================================================\n\n/**\n * Encodes a parameter name and value as a query string segment.\n *\n * Uses pre-resolved strategies for format-specific encoding.\n * Strategies handle all formats uniformly - no inline optimizations\n * to avoid equivalent mutants in mutation testing.\n *\n * @param name - Parameter name\n * @param value - Parameter value\n * @param opts - Options with pre-resolved strategies\n * @returns Query string segment (e.g., \"key=value\")\n */\nexport const encode = (\n name: string,\n value: unknown,\n opts: OptionsWithStrategies,\n): string => {\n const encodedName = encodeValue(name);\n\n // Handle each type using strategies\n switch (typeof value) {\n case \"string\":\n case \"number\": {\n return `${encodedName}=${encodeValue(value)}`;\n }\n case \"boolean\": {\n return opts.strategies.boolean.encode(encodedName, value);\n }\n case \"object\": {\n // Null check\n if (value === null) {\n return opts.strategies.null.encode(encodedName);\n }\n // Array check\n if (Array.isArray(value)) {\n return opts.strategies.array.encodeArray(\n encodedName,\n value,\n opts.strategies.null,\n );\n }\n\n // Fallback for other objects - treat as string\n return `${encodedName}=${encodeValue(value)}`;\n }\n default: {\n // Fallback for other types (undefined handled by caller)\n return `${encodedName}=${encodeValue(value)}`;\n }\n }\n};\n","/**\n * Search Params - Query String Parsing and Building.\n *\n * Internalized from https://github.com/troch/search-params (MIT License)\n * for better code control and optimization.\n *\n * @module search-params/searchParams\n */\n\nimport { decode, decodeValue } from \"./decode\";\nimport { encode, makeOptions } from \"./encode\";\nimport { putField } from \"../../utils/ingest\";\n\nimport type { OptionsWithStrategies } from \"./encode\";\nimport type { ResolvedStrategies } from \"./strategies\";\nimport type { Options } from \"./types\";\n\n/**\n * Intrinsics captured at module load: `hasOwn`, `objectKeys`.\n *\n * ⚑ A guard is only as strong as the intrinsic it reads WHEN IT RUNS, and an\n * application can re-point any of these AFTER boot — which is what this closes.\n * Measured on the uncaptured form: one naive `Object.hasOwn` polyfill walked\n * straight through five sibling readers while the single captured guard held.\n *\n * ⚠ The limit of what capture buys — and the shim order that defeats it — is\n * stated once, in `guards.ts`. Not restated here (#2091).\n */\nconst hasOwn = Object.hasOwn;\nconst objectKeys = Object.keys;\n// =============================================================================\n// Internal Helpers\n// =============================================================================\n\n/**\n * Assigns a parameter as an own data property.\n *\n * ⚑ The name comes straight out of the URL, so the whole prototype chain is in\n * play — not just `\"__proto__\"` (#1852), and `putField` is what covers the rest\n * of it. Measured on `matchPath('/q?tab=x')` with an ambient `tab`: a\n * getter-only or non-writable property made the URL STOP MATCHING (the throw is\n * swallowed by the documented fail-open `#737` catch, so the caller sees\n * `UNKNOWN_ROUTE` rather than an error), and a getter+setter pair stole the\n * value outright — `state.search` empty beside a `state.path` that still carried\n * the query.\n *\n * @internal\n */\nfunction assignParam(\n params: Record<string, unknown>,\n name: string,\n value: unknown,\n): void {\n putField(params, name, value);\n}\n\n/**\n * Adds a decoded value to params object, handling array accumulation.\n *\n * Collisions are detected via `Object.hasOwn`, not `params[name] !== undefined`:\n * a query key that shadows an `Object.prototype` member (`valueOf`, `constructor`,\n * `toString`, …) would otherwise read the inherited function and be mistaken for\n * a pre-existing value, corrupting the result into `[<fn>, value]`. (#855)\n *\n * @internal\n */\nfunction addToParams(\n params: Record<string, unknown>,\n decodedName: string,\n decodedValue: unknown,\n hasBrackets: boolean,\n): void {\n if (!hasOwn(params, decodedName)) {\n assignParam(\n params,\n decodedName,\n hasBrackets ? [decodedValue] : decodedValue,\n );\n\n return;\n }\n\n const currentValue = params[decodedName];\n\n if (Array.isArray(currentValue)) {\n currentValue.push(decodedValue);\n } else {\n assignParam(params, decodedName, [currentValue, decodedValue]);\n }\n}\n\n/**\n * Reads the non-negative integer index from a bracketed name (`a[12]`).\n *\n * `open` points at the `[`; digits up to the matching `]` form the index. Returns\n * `null` for `[]`, non-digit content (`a[x]`), or a missing `]` — those fall back\n * to insertion-order accumulation. (#856)\n *\n * @internal\n */\nfunction bracketIndex(\n searchPart: string,\n open: number,\n limit: number,\n): number | null {\n let i = open + 1;\n let value = 0;\n let hasDigit = false;\n\n while (i < limit) {\n const ch = searchPart.codePointAt(i);\n\n if (ch === 93) {\n // ']' — a numeric index only if at least one digit preceded it (\"[]\" → null)\n return hasDigit ? value : null;\n }\n\n if (ch !== undefined && ch >= 48 && ch <= 57) {\n value = value * 10 + (ch - 48);\n hasDigit = true;\n i++;\n\n continue;\n }\n\n return null; // non-digit inside brackets — not a numeric index\n }\n\n return null; // no closing ']' (incl. \"[\" at end) — malformed, fall back\n}\n\n/**\n * A single parsed query chunk: the source string plus the boundary offsets and\n * decoded name that `processParamChunk` computes once. Bundled into one\n * descriptor so the indexed-format collector reuses them without a long\n * parameter list (#856).\n *\n * @internal\n */\ninterface ParsedChunk {\n searchPart: string;\n /** Offset of `[` (bracket notation), or the name terminator. */\n nameEnd: number;\n /** Offset where the raw name ends (`=` for valued chunks, else `end`). */\n nameSourceEnd: number;\n /**\n * The chunk's raw value slice, or `undefined` for a key-only chunk (#1982).\n * Carried instead of a `hasValue` boolean: the boolean was derivable from\n * `eqPos` and `end`, which every consumer already had, so it was the same fact\n * spelled twice — and the comma arm then cut the string a SECOND time to get\n * what this field already holds.\n */\n rawValue: string | undefined;\n decodedName: string;\n}\n\n/**\n * Collects a bracketed chunk into the index-format group, to be sorted by index\n * after the full pass. Returns `false` when the bracket is not a numeric index\n * (`a[]`, `a[x]`, `a[`), so the caller falls back to insertion-order push. (#856)\n *\n * @internal\n */\nfunction collectIndexedChunk(\n chunk: ParsedChunk,\n strategies: ResolvedStrategies,\n indexedGroups: Map<string, [number, unknown][]>,\n): boolean {\n const { searchPart, nameEnd, nameSourceEnd, rawValue, decodedName } = chunk;\n const index = bracketIndex(searchPart, nameEnd, nameSourceEnd);\n\n if (index === null) {\n return false;\n }\n\n const value = decode(rawValue, strategies);\n const group = indexedGroups.get(decodedName);\n\n if (group === undefined) {\n indexedGroups.set(decodedName, [[index, value]]);\n } else {\n group.push([index, value]);\n }\n\n // Stryker disable next-line BooleanLiteral: equivalent — returning false makes the caller ALSO push via insertion order, but indexedGroups was already populated above and parseIntoInternal overwrites the key with the index-sorted result, so the final params are identical (proven by injection).\n return true;\n}\n\n/**\n * Processes a single query parameter chunk and adds to params.\n *\n * `indexedGroups` is supplied only for `arrayFormat: \"index\"`: bracketed chunks\n * with a numeric index are collected there (to be sorted by index after the full\n * pass) instead of pushed in insertion order. (#856)\n *\n * @internal\n */\nfunction processParamChunk(\n searchPart: string,\n start: number,\n end: number,\n params: Record<string, unknown>,\n strategies: ResolvedStrategies,\n eqPos: number,\n indexedGroups?: Map<string, [number, unknown][]>,\n): void {\n // `eqPos` is the position of the next `=` at or after `start`, resolved once by\n // the caller's monotonic cursor (#1316) — never re-scanned here. `eqPos < end`\n // means it falls inside THIS chunk (so the chunk has a value); otherwise the\n // chunk is key-only.\n const rawValue =\n eqPos !== -1 && eqPos < end ? searchPart.slice(eqPos + 1, end) : undefined;\n\n const nameSourceEnd = rawValue === undefined ? end : eqPos;\n let nameEnd = nameSourceEnd;\n let hasBrackets = false;\n\n for (let i = start; i < nameSourceEnd; i++) {\n if (searchPart.codePointAt(i) !== 91) {\n continue;\n }\n\n // '['\n nameEnd = i;\n hasBrackets = true;\n\n break;\n }\n\n const decodedName = decodeValue(searchPart.slice(start, nameEnd));\n\n // Index array format: order by the bracket index, not insertion. A non-numeric\n // bracket (`a[]`, `a[x]`) returns false → falls through to insertion-order push.\n if (\n indexedGroups !== undefined &&\n hasBrackets &&\n collectIndexedChunk(\n { searchPart, nameEnd, nameSourceEnd, rawValue, decodedName },\n strategies,\n indexedGroups,\n )\n ) {\n return;\n }\n\n // Comma array decode: split raw value before individual element decoding\n if (!hasBrackets && rawValue !== undefined && strategies.array.decodeValue) {\n const parts = strategies.array.decodeValue(rawValue);\n\n if (parts) {\n for (const part of parts) {\n addToParams(params, decodedName, decode(part, strategies), true);\n }\n\n return;\n }\n }\n\n const decodedValue = decode(rawValue, strategies);\n\n addToParams(params, decodedName, decodedValue, hasBrackets);\n}\n\n// =============================================================================\n// Parse\n// =============================================================================\n\n/**\n * Parse an ALREADY-EXTRACTED query string (no path prefix, no leading \"?\") into\n * an object of parameters.\n *\n * The input must already be split at the first \"?\" — `SegmentMatcher.#preparePath`\n * does this before the DI call, so route-tree wires `parseQuery` (not a\n * path-accepting wrapper) as its query parser: re-splitting the input here would\n * break at a \"?\" *inside* a query value (legal per RFC 3986), silently dropping\n * the param (and unmatching the whole URL under `strictQueryParams`). (#1292)\n *\n * @example\n * ```typescript\n * parseQuery(\"page=1&sort=name\");\n * // => { page: 1, sort: \"name\" }\n *\n * parseQuery(\"items[]=a&items[]=b\", { arrayFormat: \"brackets\" });\n * // => { items: [\"a\", \"b\"] }\n * ```\n */\nexport const parseQuery = (\n search: string,\n opts?: Options,\n): Record<string, unknown> =>\n // makeOptions(undefined) returns the cached DEFAULT_OPTIONS (auto) — the same\n // defaults `build` uses — so parseQuery(build(x)) === x even without options. (#744)\n parseQueryWith(search, makeOptions(opts));\n\n/**\n * `parseQuery` over ALREADY-RESOLVED options.\n *\n * The split exists so a long-lived consumer can resolve once and keep the result\n * (#1796 follow-up): `resolveStrategies` is what refuses an invalid `queryParams`\n * format. Resolved per call, that refusal arrives from inside `matchPath` — on\n * the parse path, where `SegmentMatcher`'s `#737` catch swallows it and where\n * consumers call from popstate and `navigate`-event handlers that have nobody to\n * catch for them. `createMatcher` resolves at construction instead, so a config\n * error surfaces from `createRouter` and this function cannot raise one.\n *\n * Second effect, measured: resolved per call with a customised format,\n * `makeOptions` re-resolves all four strategies on EVERY parse and allocates a\n * fresh options object each time.\n */\nexport const parseQueryWith = (\n search: string,\n options: OptionsWithStrategies,\n): Record<string, unknown> => {\n // Fast path: empty query string\n if (search === \"\" || search === \"?\") {\n return {};\n }\n\n const params: Record<string, unknown> = {};\n\n parseIntoInternal(search, params, options.strategies);\n\n return params;\n};\n\n/**\n * Internal function to parse a query string into a target object.\n * The shared parse engine behind `parseQuery`.\n *\n * @internal\n */\nfunction parseIntoInternal(\n searchPart: string,\n params: Record<string, unknown>,\n strategies: ResolvedStrategies,\n): void {\n // `index` format orders by the bracket index; collect (index, value) pairs and\n // sort after the pass. `undefined` for every other format (no overhead). (#856)\n const indexedGroups = strategies.array.indexed\n ? new Map<string, [number, unknown][]>()\n : undefined;\n\n let start = 0;\n const length = searchPart.length;\n\n // Monotonic cursor for the next `=`. Its position only ever moves forward with\n // `start`, so the whole parse does a single amortised O(n) scan for `=` —\n // replacing `processParamChunk`'s former per-chunk `indexOf(\"=\", start)`, which\n // scanned to the end of the string on every key-only chunk and made `parse`\n // O(n²) on `\"a&a&…\"` (#1316). `-2` = not yet searched; `-1` = no `=` remains.\n let eqCache = -2;\n\n while (start < length) {\n let end = searchPart.indexOf(\"&\", start);\n\n if (end === -1) {\n end = length;\n }\n\n // Skip empty chunks — a `&&`, a leading `&`, or a trailing `&` produces a\n // zero-length span that carries no name and no value. Processing it would\n // decode the empty name to `\"\"` and the missing value to `null`, injecting a\n // junk `{ \"\": null }` param (and `[null, …]` on repeats) (#1156). An\n // intentional empty-key chunk always carries an `=` (`\"=1\"` → `end > start`),\n // so it is unaffected.\n if (end > start) {\n // Advance the cursor only when the cached `=` is behind the current chunk;\n // once it reports `-1` (no `=` left in the string) it is final.\n if (eqCache !== -1 && eqCache < start) {\n eqCache = searchPart.indexOf(\"=\", start);\n }\n\n processParamChunk(\n searchPart,\n start,\n end,\n params,\n strategies,\n eqCache,\n indexedGroups,\n );\n }\n\n start = end + 1;\n }\n\n if (indexedGroups !== undefined) {\n for (const [name, pairs] of indexedGroups) {\n // Stable sort by index (V8 sort is stable) → equal indices keep arrival order.\n pairs.sort((left, right) => left[0] - right[0]);\n assignParam(\n params,\n name,\n pairs.map((pair) => pair[1]),\n );\n }\n }\n}\n\n// =============================================================================\n// Build\n// =============================================================================\n\n/**\n * Build a querystring from an object of parameters.\n *\n * Note: Empty arrays produce an empty string, so `parseQuery(build({ items: [] }))`\n * will not contain the `items` key. This is expected behavior for all array\n * formats including `comma` — the key is erased uniformly (INVARIANTS Parse/Build #9).\n *\n * @example\n * ```typescript\n * build({ page: 1, sort: \"name\" });\n * // => \"page=1&sort=name\"\n *\n * build({ items: [\"a\", \"b\"] }, { arrayFormat: \"brackets\" });\n * // => \"items[]=a&items[]=b\"\n * ```\n */\nexport const build = (\n params: Record<string, unknown>,\n opts?: Options,\n): string => buildWith(params, makeOptions(opts));\n\n/** `build` over ALREADY-RESOLVED options — see {@link parseQueryWith}. */\nexport const buildWith = (\n params: Record<string, unknown>,\n options: OptionsWithStrategies,\n): string => {\n // Fast path for empty params (common case)\n const keys = objectKeys(params);\n\n if (keys.length === 0) {\n return \"\";\n }\n\n // Optimized: single loop instead of filter().map().filter().join()\n // Avoids creating 3 intermediate arrays\n const parts: string[] = [];\n\n for (const key of keys) {\n const value = params[key];\n\n // Skip undefined values (not serialisable)\n if (value === undefined) {\n continue;\n }\n\n const encoded = encode(key, value, options);\n\n // Skip empty strings (e.g., from nullFormat: \"hidden\")\n if (encoded) {\n parts.push(encoded);\n }\n }\n\n return parts.join(\"&\");\n};\n","/**\n * Matcher Factory.\n *\n * Creates a pre-configured path matcher with search-params DI baked in.\n * This is the public API for creating matchers — SegmentMatcher class\n * and search-params functions are internal implementation details.\n *\n * @module route-tree/createMatcher\n */\n\nimport { SegmentMatcher } from \"./path-matcher\";\nimport { buildWith, makeOptions, parseQueryWith } from \"./search-params\";\n\nimport type { Options } from \"./search-params\";\n\n// =============================================================================\n// Public Types (route-tree's own — no path-matcher or search-params types leak)\n// =============================================================================\n\n/**\n * Query string formatting options.\n *\n * Controls how arrays, booleans, nulls, and numbers are serialized in query strings.\n */\nexport type QueryParamsConfig = Readonly<Options>;\n\n/**\n * Options for creating a path matcher.\n */\nexport interface CreateMatcherOptions {\n readonly caseSensitive?: boolean;\n readonly strictTrailingSlash?: boolean;\n readonly strictQueryParams?: boolean;\n readonly urlParamsEncoding?: \"default\" | \"uri\" | \"uriComponent\" | \"none\";\n readonly queryParams?: QueryParamsConfig;\n}\n\n/**\n * Path matcher instance type.\n *\n * Opaque type — consumers use methods (match, buildPath, hasRoute, etc.)\n * without knowing the underlying SegmentMatcher implementation.\n */\nexport type Matcher = SegmentMatcher;\n\n// =============================================================================\n// Factory\n// =============================================================================\n\n/**\n * Creates a path matcher with search-params DI baked in.\n *\n * @param options - Matcher configuration\n * @returns Configured matcher instance\n *\n * @example\n * ```typescript\n * const matcher = createMatcher({\n * strictTrailingSlash: true,\n * queryParams: { booleanFormat: \"auto\" },\n * });\n * matcher.registerTree(tree);\n * const result = matcher.match(\"/users/123\");\n * ```\n */\nexport function createMatcher(options?: CreateMatcherOptions): Matcher {\n const qp = options?.queryParams;\n\n // Resolve the four query-string strategies ONCE, here, at construction.\n //\n // `resolveStrategies` is what refuses an invalid `queryParams` format.\n // Resolved per call, that refusal arrives from inside `matchPath` — i.e. on\n // the parse path, where `SegmentMatcher`'s `#737` catch swallows it into\n // `UNKNOWN_ROUTE` (#1318's own symptom, #1796) and where the URL plugins call\n // from popstate and `navigate`-event handlers that have nobody to catch for\n // them. Hoisting it means a config error surfaces from `createRouter`, named,\n // and `match()` cannot raise one at all.\n //\n // It also makes the refusal unconditional. Resolved per call, a router\n // configured with a bogus format runs cleanly until the first URL that\n // happens to carry a query key — because both directions short-circuit on an\n // EMPTY query before reaching a strategy at all. ⚠ That short-circuit is\n // `SegmentMatcher`'s own (`#parseSearch`, and `#buildQueryStringForBuild`'s\n // `if (!hasKeys) return \"\"`), not the exported `parseQuery` / `build`: those\n // two resolve in argument position, so `parseQuery(\"\", { arrayFormat:\n // \"bogus\" })` throws rather than answering `{}`. (A mis-spelled FIELD still\n // answers `{}` — only a bad VALUE throws.)\n //\n // ⚑ ACCEPTED, not overlooked. The other answer costs a test for\n // `search === \"\" || search === \"?\"` in the wrapper — a second copy of the\n // predicate that already lives in `parseQueryWith`, which is the duplication\n // this whole class of defect is made of. Nothing in `src` calls the wrappers\n // (the matcher takes `parseQueryWith` / `buildWith`), the layer barrel is not\n // on the package's `exports` map, and eager refusal is what the hoist is FOR.\n // So the change is real, contained to the layer barrel, and deliberate.\n //\n // ⚠ FOUR input classes stay outside this guard because they never reach\n // `resolveStrategies`: a nullish format value (the snapshot reports it as\n // absence, and `makeOptions`' `??` then supplies the default — ⚠ this held by\n // accident of the `??` alone until the snapshot started coercing, at which\n // point `null` became the STRING `\"null\"` and WAS refused for four commits;\n // the guard is explicit now and pinned for both halves of nullish), a\n // mis-spelled FIELD (all four known fields read\n // `undefined`, so the cached defaults are returned), and a `queryParams`\n // CONTAINER that is not an object at all, and a format spelled on a ROUTE\n // rather than on the router (measured: all four build `/s?a=x&a=y`, i.e. the\n // default, in silence). ⚠ Only a TRUTHY non-object — a\n // string, a non-zero number — reads `undefined` through those four probes;\n // `null`, `0` and `\"\"` never reach them, because `makeOptions` opens with\n // `!opts` and returns the cached defaults on the spot. The outcome is the same\n // either way, which is why the distinction went unnoticed: all four classes\n // are silent, and THREE of them are `@real-router/validation-plugin`'s to\n // report — measured, with a positive control. A nullish value, a mis-spelled\n // field and a non-object container each draw\n // `[router.constructor (retrospective)] Invalid …`; a format spelled on a\n // ROUTE draws nothing, because a route-level `queryParams` is accepted as a\n // #951 custom field.\n //\n // ⚠ The count above and the LIST it summarises move together, and the class\n // easiest to drop from the count is the one that matters: a format spelled on\n // a ROUTE is the only one of the four that nobody reports.\n const queryOptions = makeOptions(qp);\n\n // Conditional spread: exactOptionalPropertyTypes forbids setting optional\n // properties to undefined — only include properties that are defined.\n return new SegmentMatcher({\n ...(options?.caseSensitive !== undefined && {\n caseSensitive: options.caseSensitive,\n }),\n ...(options?.strictTrailingSlash !== undefined && {\n strictTrailingSlash: options.strictTrailingSlash,\n }),\n ...(options?.strictQueryParams !== undefined && {\n strictQueryParams: options.strictQueryParams,\n }),\n ...(options?.urlParamsEncoding !== undefined && {\n urlParamsEncoding: options.urlParamsEncoding,\n }),\n // qs is ALREADY the query substring (SegmentMatcher split at the first \"?\");\n // parseQuery parses it verbatim — a path-accepting wrapper would re-split at a\n // \"?\" inside a query value and drop the param (#1292).\n parseQueryString: (qs: string) => parseQueryWith(qs, queryOptions),\n buildQueryString: (params: Record<string, unknown>) =>\n buildWith(params, queryOptions),\n });\n}\n","// packages/core/src/guards.ts\n\nimport { events } from \"./constants\";\nimport { validateRouteType } from \"./engine\";\nimport { SEAM } from \"./internals\";\nimport { emptyRecord, putField } from \"./utils/ingest\";\n\nimport type { LoggerConfig, LogLevelConfig, Route } from \"./types\";\nimport type { RouterValidator } from \"./types/RouterValidator\";\n\n/**\n * Intrinsics captured at module load (#1971).\n *\n * ⚑ A guard is only as strong as the intrinsic it reads WHEN IT RUNS, and an\n * application can re-point any of these AFTER boot — which is what this closes.\n * Measured on the uncaptured form: one naive `Object.hasOwn` polyfill walked\n * straight through five sibling readers while the single captured guard held.\n *\n * ⚠ It does NOT close a shim evaluated BEFORE this module — the ordinary\n * polyfill order. Measured: a naive `Object.hasOwn` imported ahead of core\n * reproduces #1798 verbatim (`buildPath` prints the native method into the\n * URL).\n *\n * ⚠ So do not write \"before any application code can run\" anywhere: the shim\n * order above refutes it, and it is the sentence a reader would trust.\n *\n * ⚑ **The doctrine is DERIVED, not remembered (#1971).** A rule that lives in\n * headers is scattered discipline, and scattered discipline is precisely what\n * this header's own \"five sibling readers\" measurement says does not hold. So\n * `tests/functional/captured-intrinsics-authority-1971.test.ts` walks core and\n * `shared/` for any call to one of the seven DECIDING intrinsics outside a\n * capture, and requires a written reason for anything that survives.\n *\n * ⚑ `shared/` is in that walk deliberately, and it is where the convention pays\n * most: measured there, three of its raw reads FAIL OPEN — a re-pointed\n * `getPrototypeOf` admits a `Date` into `state.params`, `values` admits a nested\n * function, `keys` skips option validation entirely. Core's raw reads mostly\n * degrade toward refusal; that half flipped the verdict to \"valid\".\n *\n * ⚑ **The doctrine covers two categories (#2072 / #2073).** The seven above\n * DECIDE — each answers \"what is on this object\", so a re-pointed one changes a\n * VERDICT. `Object.create` and `Object.freeze` answer nothing; they BUILD the\n * object every one of those answers is about, so a re-pointed one removes the\n * guarantee instead. Measured: a shimmed `Object.create` sends `emptyRecord`'s\n * table back to `Object.prototype` and loses a declared `__proto__` param\n * (#1825), and a shimmed `Object.freeze` leaves `matcherOptions` writable so a\n * swapped `queryParams` throws out of `add()` (#1839). Both categories are\n * DERIVED by the same authority suite; only the BUILD half is scoped to calls\n * that RUN AFTER BOOT, because a module-scope one is evaluated before any\n * application code and a capture buys nothing there.\n */\nconst objectKeys = Object.keys;\nconst objectValues = Object.values;\nconst hasOwn = Object.hasOwn;\nconst getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;\nconst getPrototypeOf = Object.getPrototypeOf;\nconst ObjectCtor = Object;\n\n// ============================================================================\n// Structural invariant guards (dependencies + route-tree shape)\n// ============================================================================\n\n/**\n * Refuses a route name that is not a string, naming the DOOR (#1896 / #1888).\n *\n * A route name reaches core's tables as a property key, so `ToPropertyKey`\n * coerces anything else — which makes a non-string a call into application code\n * and, where two stores disagree about the key, a registration that reports as\n * present and never runs.\n *\n * The wording is `@real-router/validation-plugin`'s `validateRouteName`, byte\n * for byte, including its `typeof` quirks (`typeof null === \"object\"`), so the\n * no-plugin error matches the with-plugin one. Pinned by that package's\n * `bare-core-message-parity` suite.\n */\n/**\n * The seven names the emitter can ever dispatch, derived from the constant that\n * declares them — not a second hand-written list (#1888).\n */\nconst VALID_EVENT_NAMES: ReadonlySet<string> = new Set(objectValues(events));\n\n/**\n * Refuses an event name outside that set (#1888).\n *\n * The emitter keys its listener map by whatever it is handed, so a name nothing\n * emits — an object, or a typo'd string — registers cleanly and never fires,\n * and the door returns an unsubscribe either way. Unlike a route name, the\n * valid set is CLOSED and core declares it, so membership is the predicate and\n * it closes the typo too.\n *\n * The wording mirrors `@real-router/validation-plugin`'s `validateEventName`\n * byte for byte, so the no-plugin error matches the with-plugin one.\n */\nexport function assertEventNameIsValid(eventName: unknown): void {\n if (!VALID_EVENT_NAMES.has(eventName as string)) {\n throw new TypeError(\n `[router.addEventListener] Invalid event name: ${String(eventName)}. Must be one of: ${[...VALID_EVENT_NAMES].join(\", \")}`,\n );\n }\n}\n\n/**\n * Refuses an interceptor core cannot run (#2088). An always-on guard, and it\n * meets BOTH halves of the criterion `CLAUDE.md` states for one.\n *\n * `addInterceptor` keys its map by whatever it is handed and nothing ever wraps\n * an entry under a name no seam reads, so a typo registers cleanly, never fires,\n * and hands back a working `Unsubscribe` — silent corruption in what the\n * application ships, with a green suite (a). A non-function is worse-shaped: it\n * is admitted here and thrown from whichever navigation reaches the seam first,\n * which is the deferred crash (b). Both live on one call, so the guard takes\n * both; refusing the name alone would leave the same door half-open.\n *\n * ⚑ Membership is asked of {@link SEAM}, the object the wrappers take their own\n * names from — so the set that decides is the set that acts, and there is no\n * second list to keep in step.\n *\n * ⚠ Nothing here COERCES the name, and both halves of that are load-bearing.\n * `hasOwn` performs `ToPropertyKey`, so without the `typeof` term an object\n * whose `toString` returns `\"forwardState\"` would be ADMITTED as that seam; and\n * the message renders a non-string by its type rather than through `String()`,\n * which would call the same `toString` one line later. A diagnostic must not be the\n * thing that runs application code.\n *\n * Core is the only publisher of this refusal. A mirror in `validation-plugin`\n * would be a second copy of a set core owns at RUNTIME, with nothing holding the\n * two together — the shape #2088 exists to remove.\n */\n/**\n * Refuses a non-function event listener (#2088).\n *\n * The name half of this door is {@link assertEventNameIsValid}; this is the\n * other argument, and it fails LATER rather than louder: the emitter stores\n * whatever it is handed, isolates the call, and logs `cb is not a function` on\n * every emit of that event for the life of the router — a registration that\n * reported success and never works. Refusing here turns a permanent per-emit log\n * into one error at the line that caused it.\n *\n * The wording mirrors `@real-router/validation-plugin`'s `validateListenerArgs`\n * byte for byte, the same convention the event-name half follows, and\n * `bare-core-message-parity.test.ts` pins the pair.\n */\nexport function assertListenerIsFunction(cb: unknown): void {\n if (typeof cb !== \"function\") {\n throw new TypeError(\n `[router.addEventListener] callback must be a function, got ${typeof cb}`,\n );\n }\n}\n\nexport function assertInterceptableSeam(method: unknown, fn: unknown): void {\n if (typeof method !== \"string\" || !hasOwn(SEAM, method)) {\n throw new TypeError(\n `[router.addInterceptor] Invalid method: ${\n typeof method === \"string\" ? `\"${method}\"` : typeof method\n }. Must be one of: ${objectKeys(SEAM).join(\", \")}`,\n );\n }\n\n if (typeof fn !== \"function\") {\n throw new TypeError(\n `[router.addInterceptor] interceptor must be a function, got ${typeof fn}`,\n );\n }\n}\n\nexport function assertRouteNameIsString(\n name: unknown,\n methodName: string,\n): asserts name is string {\n if (typeof name !== \"string\") {\n throw new TypeError(\n `[router.${methodName}] Route name must be a string, got ${typeof name}`,\n );\n }\n}\n\nexport function guardDependencyShape(deps: unknown): void {\n if (!deps || typeof deps !== \"object\") {\n throw new TypeError(\"dependencies must be a plain object\");\n }\n\n // ⚑ The PROTOTYPE, not `deps.constructor` (#1858). `constructor` is an\n // ordinary dependency name — `set(\"constructor\", v)` stores it and `has`/`get`\n // agree — so a predicate reading it back depends on a name the CALLER\n // controls, and `cloneRouter` re-guards the bag it rebuilds. Such a predicate\n // is forgeable both ways: `Object.assign(Object.create(null),\n // { constructor: Object })` would pass it while a bare `Object.create(null)`\n // would not, so it admits neither exactly the plain objects nor exactly the\n // others.\n //\n // The instance is what the caller writes to; the PROTOTYPE is not, so asking\n // it the same question is out of reach of an ordinary dependency name.\n //\n // `null` is admitted deliberately: `Object.create(null)` is a plain bag with\n // no prototype to inherit through, and the dependency store itself is built\n // that way. Refusing it was an accident of the old spelling.\n //\n // ⚠ The two rows above are the INTENDED differences, not the only ones.\n // \"Differs on exactly two rows and agrees on the rest\" holds over ten\n // hand-picked shapes and is false over the family. The others:\n //\n // Object.setPrototypeOf([1, 2], null) refused -> ACCEPTED\n // array / Map / class instance whose OWN\n // `constructor` is forged to `Object` accepted -> REFUSED\n // Proxy answering `get` and `getPrototypeOf`\n // inconsistently moves in BOTH directions\n //\n // The middle row is a tightening and the top one is harmless (only own\n // enumerable string keys are ever copied), but none of them was intended, and\n // a comment that under-reports its own blast radius is worse than one that\n // says nothing. What the change really does is move the caller-controlled lie\n // from the `get` trap to the `getPrototypeOf` trap.\n //\n // ⚠ This predicate DISAGREES with its sibling: `engine/validation/route-batch`\n // asks `proto !== Object.prototype && proto !== null` for the same question\n // about route objects, so `Object.create({ … })` is a plain object here and is\n // not one there. Deliberate, and it is the reason the sibling's spelling was\n // not reused: it would refuse the bag #1799 / #1823 need to REACH the copy\n // loop, where an inherited key is dropped rather than the bag rejected. If the\n // two are ever unified, that is the constraint to unify around.\n //\n // ⚠ `getPrototypeOf` and `Object` are both captured; `Object.prototype` needs\n // no capture — it is `writable: false, configurable: false`, and neither\n // `Reflect.setPrototypeOf` nor `__proto__` assignment can move it. But the\n // read below is `proto.constructor`, which resolves through\n // `Object.prototype.constructor` — writable, configurable, and NOT closeable\n // without comparing prototype identity, which the paragraph above rules out.\n // Re-point it and every plain bag is refused. That hole is open, in both this\n // spelling and the one it replaced, and it is stated here rather than left for\n // the next reader to find.\n const proto = getPrototypeOf(deps) as { constructor?: unknown } | null;\n\n if (proto !== null && proto.constructor !== ObjectCtor) {\n throw new TypeError(\"dependencies must be a plain object\");\n }\n // ⚑ The walk and the check must answer about the SAME property set (#1799).\n // `for…in` enumerates inherited names; `getOwnPropertyDescriptor` answers\n // `undefined` for every one of them, so a `for…in` walk paired with this\n // check would iterate exactly the names it cannot judge and pass a forbidden\n // getter straight through. Own-only here, which is also the\n // supported-input boundary: an inherited key is not supported input, so it is\n // not a dependency at all and there is nothing to refuse. The copy loops\n // enforce the same rule, so such a name never reaches the store either.\n}\n\n/**\n * The ONE door a caller-supplied dependency bag goes through (#1860 / #1861).\n *\n * Three call sites take such a bag — the constructor, `cloneRouter` and\n * `setAll` — and before this they applied three different rules: the constructor\n * refused a non-plain-object and a getter, `cloneRouter` merged the argument\n * into a fresh literal BEFORE the guard could see it (so the check was\n * structurally vacuous with respect to the value it judged), and `setAll`\n * reached no structural check at all. Measured across all three: a `Map` became\n * `{}` at two doors and threw at the third, i.e. every dependency the caller\n * passed vanished with no error — on `cloneRouter`, which is the per-request SSR\n * path `angular/providersFactory` forwards an application-authored bag into.\n *\n * ⚑ **Judge and copy are ONE walk, and that is the fix for #1861 rather than a\n * tidy-up.** As two `Object.keys` calls on the same object, one after the\n * other, they agree for an ordinary object and the verdict covers what is\n * installed; for a `Proxy` they need not, because `ownKeys` is a trap and a trap\n * may answer differently on its second invocation. Measured: a\n * bag answering `[]` then `[\"evil\"]` passed the judge and installed `evil` —\n * unjudged, with the caller's `get` trap invoked once. Here the descriptor is\n * asked and the value is read for the SAME key inside the SAME iteration, so\n * \"installed but not judged\" is unconstructible rather than guarded against.\n *\n * ⚠ All three doors share this one walk, and that is why the parity fix and the\n * single-walk fix are one edit: a door given a judge while its own copy loop is\n * left alone carries the very defect the paragraph above rules out.\n *\n * ⚠ **The getter ban's limit is honest, not closed.** A `Proxy` that answers\n * `getOwnPropertyDescriptor` with a data descriptor and runs code from its `get`\n * trap gets that code run, because the copier must read the value to install it.\n * Measured: a bag with a STABLE `ownKeys` defeats the ban exactly as well as a\n * drifting one, so the single walk is not what stands between a caller and their\n * own code running. What the ban does enforce, it enforces against ordinary\n * objects, and `packages/core/CLAUDE.md` \"Supported Input Shapes\" is where the\n * boundary is written down.\n *\n * ⚑ **One WALK, but the SHAPE is asked twice at the constructor — measured, and\n * kept.** `Router` calls `guardDependencyShape` before `guardRouteStructure` so\n * \"is this even an object\" stays the first thing a caller hears about, and this\n * function asks again. For a `Proxy` that is two `getPrototypeOf` trap\n * invocations against one `ownKeys` — and both answers must pass, so a bag that\n * lies about its prototype is refused in EITHER order, where a single ask would\n * admit the one that lies on its first answer. Deleting the \"duplicate\" would\n * lose that; `dependency-door-parity-1860.test.ts` reds if it goes.\n *\n * `install` receives only keys that passed the ban and values that are not\n * `undefined` — `set(name, undefined)` is a documented no-op (INVARIANTS\n * \"getDependenciesApi (CRUD)\" #8) and the batch doors have always agreed.\n */\nexport function ingestDependencies(\n source: unknown,\n install: (key: string, value: unknown) => void,\n): void {\n guardDependencyShape(source);\n\n const bag = source as Record<string, unknown>;\n const staged: [string, unknown][] = [];\n\n // ⚑ PREPARE, then COMMIT — the idiom route-CRUD already uses, and the reason\n // is the same: a refusal must leave the store untouched. Judging and\n // installing in the SAME iteration reds `setall-reentrancy-1859` —\n // `{ a: 1, get b() {…} }` installs `a`, then throws about `b`, a partial write\n // on a live store. Only `setAll` shows it: the constructor door hides it (a\n // throwing constructor discards its router) and so does `cloneRouter` (it\n // stages into a local), which is why the doors have to be read together.\n //\n // ⚠ Still ONE walk of the CALLER's bag, which is the whole point of #1861.\n // `staged` is core's own array, so replaying it runs no trap and asks the\n // caller nothing.\n for (const key of objectKeys(bag)) {\n if (getOwnPropertyDescriptor(bag, key)?.get) {\n throw new TypeError(`dependencies cannot contain getters: \"${key}\"`);\n }\n\n const value = bag[key];\n\n if (value !== undefined) {\n staged.push([key, value]);\n }\n }\n\n for (const [key, value] of staged) {\n install(key, value);\n }\n}\n\n/**\n * The one walk of a registration batch: judges the CALLER's definitions and\n * returns core's snapshot of them (#1899 / #1911 / #2139).\n *\n * Read per consumer instead, registration reads each definition many times —\n * measured, `route.name` seven times for one `add`: the reserved-prefix walker,\n * the dotted-name walker, `walkRouteNames` twice, `sanitizeRoute`,\n * `registerAllRouteHandlers`, and the `Object.entries` that collects custom\n * fields. Every read is an independent question, so a definition whose `name` is\n * an accessor is VALIDATED under one answer and REGISTERED under another.\n * Snapshot first and every existing guard becomes correct by construction, which\n * is the one thing hardening each reader separately cannot do.\n *\n * ⚑ **Judging and copying are ONE visit, and that is the contract** — not an\n * ordering convention between two functions. The object-shape questions must see\n * the caller's value, because a spread answers all of them the same way whatever\n * it was made from: `{...null}`, `{...42}`, `{...true}` and `{...undefined}` are\n * all `{}`, `{...\"ab\"}` is `{0:\"a\",1:\"b\"}`, and `{...[x]}` is `{0:x}`. Every\n * reader BELOW must see the snapshot, or a definition that answers differently\n * per read is validated under one callback and registered with another. A guard\n * pass followed by a snapshot pass satisfies both and still asks the caller's\n * CONTAINER twice, which is the window #2139 closed.\n *\n * ⚠ So there is no \"run the snapshot after the guard\" rule left to get wrong,\n * and no second walker to keep in step. `guardRouteCallbacks` runs on what this\n * returns.\n */\n/* eslint-disable @typescript-eslint/no-explicit-any -- accepts any Route type */\nexport function guardRouteStructure<T extends Route<any>>(routes: T[]): T[] {\n /* eslint-enable @typescript-eslint/no-explicit-any */\n const batch: T[] = [];\n\n for (const route of routes) {\n const routeValue: unknown = route;\n\n if (\n routeValue === null ||\n typeof routeValue !== \"object\" ||\n Array.isArray(routeValue)\n ) {\n throw new TypeError(\"route must be a non-array object\");\n }\n\n // ⚑ The OBJECT-shape questions run HERE, on the caller's value, because a\n // snapshot answers all of them the same way whatever it was made from\n // (#1911). `validateRouteType` owns them; this door owns the position.\n //\n // ⚠ `\"addRoute\"` for every door, deliberately: the plugin reports that name\n // for `replace` batches too, so bare core and the plugin surface one string.\n validateRouteType(routeValue, \"addRoute\");\n\n // ⚑ Judged and copied in the SAME visit, which is why this walk returns the\n // batch instead of only asserting about it (#2139). Two walks meant two\n // questions of one CALLER-owned container, and the answers are free to\n // differ: a `Proxy` array reports an ordinary data descriptor — so the\n // accessor ban above never reaches it — and simply answered a legal element\n // to the guard and an accessor-backed one to the snapshot. Measured on all\n // three registration doors: `has(\"kid\") === false`, `has(\"evil\") === true`,\n // for an element the same doors refuse outright when it does not drift.\n //\n // ⚠ A spread, deliberately: own enumerable keys are exactly the supported\n // input surface (`packages/core/CLAUDE.md`, \"Supported Input Shapes\"), so\n // this drops nothing core was contracted to read. It also DEFINES rather\n // than assigns, so a custom field literally named `\"__proto__\"` survives as\n // data.\n const snapshot = { ...(routeValue as T) };\n // ⚑ Off the SNAPSHOT, not off `route`. The spread above has already asked\n // the caller for this key, and asking again would be the second question\n // this walk exists to remove — `registration · route.children` in\n // `read-count-authority` stood at 2 and stands at 1 now. An INHERITED\n // `children` still arrives: the spread copies no own key for it, so the\n // read below walks the prototype exactly as before.\n const children = snapshot.children;\n\n if (children) {\n // ⚑ `putField`, not `snapshot.children = …` (#1852). This is the\n // primitive's only write under a key a route config chose, and a route\n // that INHERITS `children` gives the spread above no own key to overwrite\n // — so a plain assignment walked the prototype and reached an ambient\n // accessor. Measured: with a setter, the batch went into it and never\n // into the snapshot; with a getter alone, the assignment threw\n // `Cannot set property children of #<Object> which has only a getter` and\n // a legal `createRouter` became an error.\n //\n // ⚠ Reached for any TRUTHY `children`, which keeps a malformed non-array\n // failing exactly where it fails today — in this walk's own `for…of`,\n // with the message it already produces — rather than being laundered into\n // a plain object by the copy.\n putField(\n snapshot as unknown as Record<string, unknown>,\n \"children\",\n guardRouteStructure(children),\n );\n }\n\n batch.push(snapshot);\n }\n\n return batch;\n}\n\n/**\n * The validator's per-route CALLBACK guards, walked over a batch.\n *\n * ⚑ Separate from {@link guardRouteStructure} because the two need different\n * operands (#1911). The structural check must see the CALLER's value — a spread\n * turns every shape it exists to refuse into a plain object — while these read\n * the route's own keys and must therefore see the SNAPSHOT, or a definition that\n * answers differently per read is validated under one callback and registered\n * with another. Run this on what `guardRouteStructure` RETURNS, never on the\n * caller's array.\n */\n/* eslint-disable @typescript-eslint/no-explicit-any -- mirrors guardRouteStructure's variance */\nexport function guardRouteCallbacks(\n routes: readonly Route<any>[],\n validator?: RouterValidator | null,\n): void {\n /* eslint-enable @typescript-eslint/no-explicit-any */\n if (!validator) {\n return;\n }\n\n for (const route of routes) {\n validator.routes.guardRouteCallbacks(route as Route);\n validator.routes.guardNoAsyncCallbacks(route as Route);\n\n const children = (route as Route).children;\n\n if (children) {\n guardRouteCallbacks(children, validator);\n }\n }\n}\n\n// ============================================================================\n// Logger config assertion (RealRouter-specific)\n// ============================================================================\n\nconst VALID_LEVELS_SET = new Set<string>([\n \"all\",\n \"warn-error\",\n \"error-only\",\n \"none\",\n]);\n\nfunction isValidLevel(value: unknown): value is LogLevelConfig {\n return typeof value === \"string\" && VALID_LEVELS_SET.has(value);\n}\n\nfunction formatValue(value: unknown): string {\n if (typeof value === \"string\") {\n return `\"${value}\"`;\n }\n if (typeof value === \"object\") {\n return JSON.stringify(value);\n }\n\n // eslint-disable-next-line @typescript-eslint/no-base-to-string\n return String(value);\n}\n\n/**\n * Validates a caller's logger config and hands back CORE'S OWN copy of it\n * (#1814 / #1842).\n *\n * ⚑ It RETURNS rather than only asserting, and that is the fix rather than a\n * signature preference. Asserting only leaves the caller's bag read by TWO\n * independent readers — this guard, then `RouterLogger.configure` — free to\n * disagree about own-ness (`in` against `hasOwn`) and free to be handed\n * different values on their two reads, so the `typeof` gate here need not cover\n * what `configure` stores. Returning collapses them to one reader.\n * `logger-config-read-once-1814.test.ts` owns the read count;\n * `packages/core/CLAUDE.md` \"Supported Input Shapes\" settles the own-ness\n * question, own-enumerable-only.\n *\n * The rule applied is core's own, from `src/engine/CLAUDE.md`: *a guard that\n * admits by a computed key must hand the KEY downstream, never the value it\n * computed it from.* Here it hands the whole validated record.\n */\n/** Own-only, and the ONE membership predicate the whole guard uses (#1814). */\nfunction assertNoUnknownKeys(obj: Record<string, unknown>): void {\n for (const key of objectKeys(obj)) {\n if (\n key !== \"level\" &&\n key !== \"callback\" &&\n key !== \"callbackIgnoresLevel\"\n ) {\n throw new TypeError(`Unknown logger config property: \"${key}\"`);\n }\n }\n}\n\n/** One read, validated; `undefined` and absence both mean \"not set\". */\nfunction readLoggerLevel(\n obj: Record<string, unknown>,\n): LogLevelConfig | undefined {\n if (!hasOwn(obj, \"level\")) {\n return undefined;\n }\n\n const level = obj.level;\n\n if (level === undefined) {\n return undefined;\n }\n\n if (!isValidLevel(level)) {\n throw new TypeError(\n `Invalid logger level: ${formatValue(level)}. Expected: \"all\" | \"warn-error\" | \"error-only\" | \"none\"`,\n );\n }\n\n return level;\n}\n\n/** One read, validated; `undefined` and absence both mean \"not set\". */\nfunction readCallbackIgnoresLevel(\n obj: Record<string, unknown>,\n): boolean | undefined {\n if (!hasOwn(obj, \"callbackIgnoresLevel\")) {\n return undefined;\n }\n\n const flag = obj.callbackIgnoresLevel;\n\n if (flag === undefined) {\n return undefined;\n }\n\n if (typeof flag !== \"boolean\") {\n throw new TypeError(\n `Logger callbackIgnoresLevel must be a boolean, got ${typeof flag}`,\n );\n }\n\n return flag;\n}\n\nexport function assertLoggerConfig(config: unknown): Partial<LoggerConfig> {\n if (typeof config !== \"object\" || config === null) {\n throw new TypeError(\"Logger config must be an object\");\n }\n\n const obj = config as Record<string, unknown>;\n\n assertNoUnknownKeys(obj);\n\n // ⚑ A record with NO prototype (#2138), because an ambient member sits on BOTH\n // sides of it: three slots are written here and `RouterLogger.configure` reads\n // all three back. `putField` would close the write and leave the read open —\n // measured, and `ambient-write-logger-2138` owns every cell of that table.\n //\n // ⚠ Nothing publishes this record — `logger` is stripped from the router\n // options above and a consumer gets `getConfig()`'s own literal — so the\n // `publishRecord` half of \"build private, publish plain\" has no call site.\n const normalized = emptyRecord<unknown>() as Partial<LoggerConfig>;\n const level = readLoggerLevel(obj);\n\n if (level !== undefined) {\n normalized.level = level;\n }\n\n // ⚠ `callback` is the one field where PRESENCE differs from definedness:\n // `configure({ callback: undefined })` CLEARS the sink, which is documented\n // and tested. So the key is carried even when the value is `undefined`, and\n // `configure` asks `hasOwn` of THIS record — the same question it would ask\n // of the caller's bag, only of an object core owns.\n if (hasOwn(obj, \"callback\")) {\n const callback = obj.callback;\n\n if (callback !== undefined && typeof callback !== \"function\") {\n throw new TypeError(\n `Logger callback must be a function, got ${typeof callback}`,\n );\n }\n\n normalized.callback = callback as LoggerConfig[\"callback\"];\n }\n\n const flag = readCallbackIgnoresLevel(obj);\n\n if (flag !== undefined) {\n normalized.callbackIgnoresLevel = flag;\n }\n\n return normalized;\n}\n","// packages/core/src/limits.ts\n\nimport { DEFAULT_LIMITS } from \"./constants\";\n\nimport type { LimitsConfig } from \"./types\";\nimport type { Limits } from \"./types/internal\";\n\n/** Captured like the deciding seven, but this one BUILDS the guarantee (#2073). */\nconst freeze = Object.freeze;\n\n/**\n * Merges user limits with the defaults; returns a frozen-by-type value.\n *\n * Its own module rather than a corner of `helpers.ts`, where it sat until the\n * applicability audit: resource limits share nothing with that file's subject —\n * path/query channels, the default merge, value comparison, the state shape —\n * and a reader scanning `helpers.ts` for the channel model had to step over it.\n * The two places it could have gone instead both cost more than they save:\n * `types/limits.ts` is a TYPES module (and is re-exported into the public\n * `@real-router/core/types` entry, where runtime code has no business, least of\n * all under the two-phase dts build the augmentation invariant depends on), and\n * `constants.ts` exports no functions at all.\n *\n * @internal\n */\nexport function createLimits(userLimits: Partial<LimitsConfig> = {}): Limits {\n const merged = { ...DEFAULT_LIMITS, ...userLimits };\n\n // ⚑ Coerce here, once, and hand NUMBERS downstream (#1875). The spread above\n // already materialises an accessor on the bag, but it copies a VALUE by\n // reference — so a `{ valueOf() }` limit survived it and was re-coerced at\n // every use site. `EventEmitter` compares with `size >= maxListeners`, which\n // runs `ToPrimitive`, so that meant calling into application code once per\n // listener REGISTRATION, unboundedly, for the life of the router — and a\n // drifting `valueOf` silently moved the cap while it did.\n //\n // ⚠ Coercion only: a value that will not become a usable number is NOT\n // refused (owner decision, #1875). `undefined` and a non-numeric string both\n // become `NaN`, which `size >= NaN` reads as \"no cap\"; `Infinity` stays\n // `Infinity`; and `null` becomes `0`, the documented spelling of \"no cap\" —\n // not the refusal an unguarded `size >= null` would make of every\n // registration. A `valueOf` that THROWS still throws — the\n // caller's own error, now from the constructor instead of from an unrelated\n // `subscribe()`, which is the point of reading once.\n // ⚠ The five names are written out rather than looped over, and that is not\n // style: the repo's semgrep gate (`unguarded-computed-key-write`) blocks a\n // computed-key write inside a walk, and it blocks BOTH loop forms — over\n // `Object.keys(merged)` AND over a core-owned literal key tuple. Measured\n // against `.semgrep/rules.yml` itself, because the tuple is the form a reader\n // would reach for and its keys can never be `\"__proto__\"`; the gate does not\n // draw that distinction. Same shape `snapshotQueryParams` uses for\n // `queryParams`' four fields.\n //\n // ⚠ What catches a sixth limit added without reaching here is `tsc`, not a\n // mirror test: the return would miss a required field of\n // `Readonly<LimitsConfig>` and fail TS2741 (verified).\n // `type-mirror-authority.test.ts` does NOT cover `Limits` — its relation\n // table names none of them.\n // ⚠ Read through an `unknown` view, and the cast is load-bearing twice over.\n // Typed as declared, `Number(x)` is flagged a no-op by\n // `no-unnecessary-type-conversion` — correctly, for the DECLARED type, which\n // is precisely the type this distrusts; and a wrapper that widens it is\n // flagged by `prefer-native-coercion-functions`. Widening the SOURCE says the\n // same thing once, with no helper and no rule silenced.\n const raw = merged as Record<keyof LimitsConfig, unknown>;\n\n // ⚠ FROZEN — the docstring above has always promised it, but \"frozen-by-type\"\n // was true of the TYPE alone. This object is handed out BY REFERENCE in two\n // places: `getCloneState().limits`, which `cloneRouter` reads, and the\n // dependencies store. Without the freeze a consumer holding either could move\n // the cap a clone inherits while the base keeps the one its emitter was wired\n // with — measured, mutating the handed-out object left the base capped at 50\n // and the clone at 2, which is exactly the base/clone divergence #1880 exists\n // to prevent, reached through the slot #1880 added.\n return freeze({\n maxDependencies: Number(raw.maxDependencies),\n maxPlugins: Number(raw.maxPlugins),\n maxListeners: Number(raw.maxListeners),\n warnListeners: Number(raw.warnListeners),\n maxLifecycleHandlers: Number(raw.maxLifecycleHandlers),\n });\n}\n","import { DEFAULT_LIMITS } from \"../../constants\";\nimport { ingestDependencies } from \"../../guards\";\n\nimport type { DefaultDependencies } from \"../../types\";\nimport type { Limits } from \"../../types/internal\";\n\n/** Captured like the deciding seven, but this one BUILDS the guarantee (#2072). */\nconst objectCreate = Object.create;\n\nexport interface DependenciesStore<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> {\n dependencies: Partial<Dependencies>;\n limits: Limits;\n}\n\n/**\n * The write that installs ONE judged dependency into the store.\n *\n * ⚑ A plain assignment, and the store's PROTOTYPE is what makes it right: the\n * destination is built with `Object.create(null)` below, so there is no\n * inherited setter for `\"__proto__\"` to dispatch into and the key lands as an\n * ordinary own property. That is the exemption the SAST rule's own message\n * names, and it is load-bearing rather than incidental — `set(\"__proto__\", v)`\n * is a supported call whose value `has`/`get` return, and `getAll()` is the\n * door that withholds it on the way out (#1823).\n *\n * ⚑ It is a FUNCTION so that the paragraph above has ONE owner. Both doors that\n * take a judged pair into this store — `createDependenciesStore` below and\n * `setAll` in `getDependenciesApi` — write through it, so the reasoning and the\n * suppression exist once instead of once per site (#2091), and\n * `computed-key-write-authority-1852` pins the resulting SET of sites.\n *\n * ⚠ `putField` is deliberately not used, and the axis is the TARGET rather than\n * the door. Its define branch tests `key in target`, which is false for every\n * name where there is no prototype chain — so on this store the branch cannot\n * run, and routing through it costs 1.26–1.34× on the write for a guarantee the\n * store already holds by construction (#2116). `cloneRouter` takes the same\n * judged pairs and DOES call `putField`, because it stages into a plain `{}`\n * where that branch is live.\n *\n * ⚠ `setDependency`'s single-name door is not this function: it types its\n * target `Record<PropertyKey, unknown>` because a symbol name reaches it\n * untouched, and narrowing to `string` there would be false about symbols.\n */\nexport function storeDependency(\n target: Record<string, unknown>,\n key: string,\n value: unknown,\n): void {\n // nosemgrep: unguarded-computed-key-write\n target[key] = value;\n}\n\nexport function createDependenciesStore<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n initialDependencies: Partial<Dependencies> = {},\n): DependenciesStore<Dependencies> {\n const dependencies = objectCreate(null) as Partial<Dependencies>;\n\n // ⚑ `Object.keys`, and read ONCE (#1816 / #1823 / #1799).\n //\n // The walk is THE SAME ONE the judge uses — literally the same loop since\n // #1861, not merely the same spelling — and that is the point:\n // an intermediate draft walked `for…in` and filtered with `Object.hasOwn`,\n // which enumerates the same set for a plain object but NOT for a Proxy —\n // `for…in` asks `ownKeys` plus the chain, `hasOwn` asks the\n // `getOwnPropertyDescriptor` trap, and a bag that answers those two\n // differently gets a key past the copy loop that the guard never judged.\n // Measured: a forbidden getter reached the store and ran. Walking `ownKeys`\n // once leaves nothing for the two halves to disagree about. It is also\n // faster — measured −18 % at one key and −25 % at twenty.\n //\n // Read ONCE: reading each key twice — the `!== undefined` test and the value\n // stored — admits a key on one value and STORES another. Neither shape needs\n // inheritance to fire: a Proxy is enough.\n //\n // ⚑ The walk itself now lives in `ingestDependencies` (#1860 / #1861), which\n // is the ONE door all three dependency-bag entry points share — and it judges\n // and copies in a SINGLE pass, so this is also the only enumeration of the\n // caller's bag a router construction performs. Two walks — the constructor's\n // guard, then this loop — let a `Proxy` whose `ownKeys` answers differently\n // between them install a key nobody judged.\n const source = initialDependencies as Record<string, unknown>;\n const target = dependencies as Record<string, unknown>;\n\n ingestDependencies(source, (key, value) => {\n storeDependency(target, key, value);\n });\n\n return {\n dependencies,\n limits: DEFAULT_LIMITS,\n };\n}\n","// packages/core/src/namespaces/OptionsNamespace/constants.ts\n\nimport { DEFAULT_QUERY_PARAMS } from \"../../engine\";\n\nimport type { Options } from \"../../types\";\n\n/**\n * Default options for the router.\n */\n// No explicit `: Options` annotation — that would widen every field to the\n// union `Options<DefaultDependencies>` declares, including the CALLBACK arm of\n// `defaultRoute` / `defaultParams` / `defaultSearch`. `Options` is generic over\n// the dependency map now, and a callback typed against `object` does not flow\n// into an `Options<Deps>`. `satisfies` keeps the check while letting the\n// inferred (callback-free) literal types stay assignable to any instantiation.\nexport const defaultOptions = {\n defaultRoute: \"\",\n defaultParams: {},\n defaultSearch: {},\n trailingSlash: \"preserve\",\n caseSensitive: true,\n queryParamsMode: \"loose\",\n queryParams: DEFAULT_QUERY_PARAMS,\n urlParamsEncoding: \"default\",\n allowNotFound: true,\n rewritePathOnMatch: true,\n} satisfies Options;\n","// packages/core/src/namespaces/OptionsNamespace/validators.ts\n\n/**\n * Minimal crash guard for options.\n * Full DX validation moved to @real-router/validation-plugin (retrospective pattern).\n */\nexport function validateOptionsIsObject(\n options: unknown,\n): asserts options is Record<string, unknown> {\n if (!options || typeof options !== \"object\" || Array.isArray(options)) {\n throw new TypeError(\"[router.constructor] options must be a plain object\");\n }\n}\n","// packages/core/src/namespaces/OptionsNamespace/OptionsNamespace.ts\n\nimport { defaultOptions } from \"./constants\";\nimport { validateOptionsIsObject } from \"./validators\";\nimport { dropUnsafeKey } from \"../../helpers\";\n\nimport type { DefaultDependencies, Options } from \"../../types\";\n\n/**\n * Captured at module load: `freeze`.\n *\n * ⚑ A guard is only as strong as the intrinsic it reads WHEN IT RUNS, and an\n * application can re-point `Object.freeze` after boot — a freeze that reads the\n * re-pointed one silently does nothing.\n *\n * ⚠ It does NOT close a shim evaluated BEFORE this module, the ordinary\n * polyfill order.\n */\nconst freeze = Object.freeze;\n\nexport class OptionsNamespace<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> {\n readonly #options: Readonly<Options<Dependencies>>;\n\n constructor(initialOptions: Partial<Options<Dependencies>> = {}) {\n // ⚑ `dropUnsafeKey` on the spread, before the freeze (#1957). The caller's\n // bag reaches this literal by a spread, which `[[Define]]`s — so an own\n // `\"__proto__\"` from `JSON.parse` or\n // `Object.fromEntries(new URLSearchParams(…))` lands as an own key on the\n // object core is about to hand to every plugin through\n // `getPluginApi(router).getOptions()` and to every clone through\n // `getCloneState().options`. There it is a prototype-swap primitive for any\n // consumer that merges it.\n //\n // Nothing is lost: `Options` has a closed shape and `\"__proto__\"` names none\n // of it, so unlike the dependency store one door over there is no value\n // here to withhold from — only an unknown key that would have been dead\n // config.\n //\n // ⚠ A THROW would be the wrong shape. `validateOptionsIsObject` accepts any\n // object and unknown option keys are tolerated everywhere else, so refusing\n // this one name while ignoring its eleven `Object.prototype` siblings would\n // be a rule with no reason a caller could infer. (`options.logger` throws on\n // it, but that is an allow-listed SUB-bag with a closed key set — a\n // different contract, not a precedent for the parent.)\n // ⚑ The freeze reaches THIS level and no further (#1832). The literal is\n // core's own — the spread minted it — while everything one level down is\n // the caller's object under the one-level copy model (#1958), and core\n // writes to none of it.\n //\n // ⚠ A DEEP freeze here would be illusory: deciding depth by asking each\n // nested bag for its `constructor` leaves an array inside a FROZEN bag\n // writable, and moving it moves what the router navigates to.\n // `options-ownership-1832.test.ts` owns the shape list.\n this.#options = freeze(\n dropUnsafeKey({\n ...defaultOptions,\n ...initialOptions,\n }),\n );\n }\n\n static validateOptionsIsObject(\n options: unknown,\n ): asserts options is Record<string, unknown> {\n validateOptionsIsObject(options);\n }\n\n get(): Readonly<Options<Dependencies>> {\n return this.#options;\n }\n}\n","// packages/core/src/namespaces/OptionsNamespace/helpers.ts\n\nimport type {\n DefaultDependencies,\n Options,\n Params,\n SearchParams,\n} from \"../../types\";\n\n/**\n * Resolves an option value that can be static or a callback.\n * If the value is a function, calls it with getDependency and returns the result.\n * Otherwise, returns the value as-is.\n */\nexport function resolveOption<D extends DefaultDependencies>(\n value: Options<D>[\"defaultRoute\"],\n getDependency: (name: string) => unknown,\n): string;\n\nexport function resolveOption<D extends DefaultDependencies>(\n value: Options<D>[\"defaultParams\"],\n getDependency: (name: string) => unknown,\n): Params;\n\nexport function resolveOption<D extends DefaultDependencies>(\n value: Options<D>[\"defaultSearch\"],\n getDependency: (name: string) => unknown,\n): SearchParams;\n\nexport function resolveOption<D extends DefaultDependencies>(\n value:\n | Options<D>[\"defaultRoute\"]\n | Options<D>[\"defaultParams\"]\n | Options<D>[\"defaultSearch\"],\n getDependency: (name: string) => unknown,\n): string | Params | SearchParams {\n if (typeof value === \"function\") {\n // Runtime getDependency is (name: string) => unknown, but DefaultRouteCallback<object>\n // expects <K extends keyof object>(name: K) => object[K] where keyof object = never.\n // Cast needed to bridge generic constraint mismatch.\n return value(getDependency as never);\n }\n\n return value;\n}\n","// packages/core/src/pipeline/canonicalize.ts\n\nimport { admittedSearch, withholdFilledSlots } from \"../channels\";\nimport { EMPTY_PARAMS, EMPTY_SEARCH } from \"../constants\";\nimport {\n mergePathChannel,\n mergeQueryChannel,\n normalizeChannel,\n} from \"../helpers\";\n\nimport type { RouteResolver } from \"./port\";\nimport type { Canonical } from \"./types\";\nimport type { Params, SearchParams } from \"../types\";\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/**\n * Options for {@link canonicalize}. Both flags are opt-in, and both are read as\n * a ROLE rather than inferred from the shape of the call — the reasons differ\n * per flag and are recorded on each.\n */\nexport interface CanonicalizeOptions {\n /**\n * Run stage ① (`forwardTo` resolution through the interceptor seam)?\n *\n * Defaults to `true`. `false` is the LITERAL form: the intent canonicalises\n * against the route the caller NAMED, without following its `forwardTo` chain\n * and without entering the seam. The entry points that ask a question about a\n * literal route rather than producing a destination take it — `buildPath`\n * (A.5: `buildPath(\"src\")` stays `/src`, deliberately asymmetric with\n * `navigate`), `isActiveRoute`'s literal arm, and `makeState`.\n *\n * ⚠ The literal form also skips the seam's channel CHECK. The seam does not\n * SEPARATE channels — stage ② was deleted (#1548), so the resolving form\n * REFUSES a mis-channelled bag while the literal form does not look HERE.\n *\n * ⚠ That is about this function, not about the door. Since #1927 every door\n * that PUBLISHES a State — `navigate`, `makeState`, `buildNavigationState`,\n * `matchPath` — checks the canonical bag it is about to ship, literal form or\n * not. The two that do not are `buildPath` (returns a string) and\n * `isActiveRoute` (returns a boolean): nothing ships, and #1572 / #1581 record\n * that the render-path predicates are deliberately not instrumented.\n * Either way nothing is moved: a caller who rides a declared query key in the\n * `params` bag keeps it there, and the URL build prints from the query channel\n * alone. That is what makes channel-correctness the producer's contract.\n */\n resolveForward?: boolean;\n\n /**\n * Run the opt-in undeclared-key diagnostic (#1579)? Defaults to `false`.\n *\n * Explicit rather than inferred from `resolveForward`, and the difference is\n * not cosmetic: `canNavigateTo` DOES resolve `forwardTo` (so it shares the\n * form with `navigate`) yet is a PREDICATE that runs on every `<Link>` render.\n * Keying the diagnostic on the form warned from it — measured, not reasoned —\n * which is exactly the per-render flood the channel guard avoids by not\n * instrumenting predicates at all (RFC rev. 29 §5). Only the points that\n * COMMIT or hand back a state a developer will keep opt in.\n */\n diagnoseUndeclared?: boolean;\n}\n\n/**\n * The undeclared-key diagnostic (#1579 — the params half of #1553), lifted out of\n * {@link canonicalize} so the fast path (#1589) fits the cognitive-complexity\n * budget beside it.\n *\n * BOTH diagnostics presuppose that the route EXISTS — they answer \"does route X\n * declare this key?\", and for a route that is not a route the honest answer is\n * that the question does not apply (#1584). `queryNames` / `pathNames` answer\n * `[]` for a real route with no declarations, so `[]` cannot say \"no such route\";\n * `pathNames` carries the `undefined` arm that can. Reporting a nonexistent route\n * blamed the params for a typo in the ROUTE name — the most misleading direction\n * available — and burnt a de-dup slot per key, silencing the genuine warning if\n * that name later became real. The committing producers still refuse the\n * navigation on their own (`undefined` from `buildNavigationState`,\n * `ROUTE_NOT_FOUND` from `navigate`); only the diagnostic was wrong.\n *\n * Called only when the sink is present, so bare core never reaches the\n * `pathNames` lookup or the bag walk.\n */\nexport function diagnoseUndeclaredKeys(\n port: RouteResolver,\n resolvedName: string,\n pathBag: Params,\n declaredQuery: readonly string[],\n report: (routeName: string, key: string) => void,\n): void {\n const declaredPath = port.pathNames(resolvedName);\n\n if (declaredPath === undefined) {\n return;\n }\n\n for (const key of objectKeys(pathBag)) {\n if (!declaredQuery.includes(key) && !declaredPath.includes(key)) {\n report(resolvedName, key);\n }\n }\n}\n\n/**\n * THE single producer of {@link Canonical}: one pass over stage ① (resolve the\n * `forwardTo` chain) and stage ③ (merge each channel's route default UNDER the\n * caller's value). There is no separating stage ② — channels arrive correct by\n * the producer contract, and the port's `resolveForward` is wired to the seam\n * that REFUSES a mis-channelled bag rather than repairing one.\n *\n * Ordering is forced by the data, not by discipline: ③ needs the RESOLVED name\n * (target defaults cannot be read before `forwardTo` resolves), so ① always\n * precedes it.\n *\n * `undefined` is absence on both sides of the merge (`mergeDefined`,\n * #1550/#1551) — an explicitly-`undefined` caller value leaves the default in\n * place, and a default carrying `undefined` behaves like no entry.\n *\n * ⚠ **The two channels are frozen by different OWNERS (#1598 / #1928).** \"Channels are\n * frozen here, at merge time — NOT in `materialize`\" is true of one of them and\n * false of the other:\n *\n * - `query` is frozen HERE, by {@link mergeQueryChannel} — a PERF-gated choice,\n * not a correctness one: moving it to `materialize` beside `params` leaves the\n * suite green, and what holds the split is a re-measured +6.3 % on\n * `isActiveRoute-parent` (see that function's docblock);\n * - `path` is NOT, because `materialize` freezes it at the publication boundary.\n * A second freeze here certified nothing observable and split what an\n * interceptor sees by route shape (#1928).\n *\n * Neither is the {@link materializePending} / {@link materialize} split, which\n * governs the state OBJECT so the navigate path can overwrite `transition` at\n * the commit — it never defers a channel.\n *\n * ⚠ The option bags at the call sites are INLINE LITERALS on purpose (#1589).\n * Hoisting them to shared frozen module constants measures worse:\n * `buildPath` and `isActiveRoute` slow 6.6–10.5 % while sites that pass no\n * options move 2 %. The literal in a small hot function is not an allocation at\n * all — V8 inlines the function, escape analysis removes the object, and the flag\n * folds to a constant. A shared frozen object replaces that with a property read\n * off the heap. Do not \"optimise\" these back.\n */\nexport function canonicalize(\n port: RouteResolver,\n name: string,\n params: Params,\n search?: SearchParams,\n opts?: CanonicalizeOptions,\n): Canonical {\n // ① — forwardTo resolution + source-route default layering, through the\n // interceptor seam (plugins inject here). The literal form skips it entirely:\n // no chain, no seam, no channel check — the caller's bags stand as given.\n // ⚑ ONE `ToPropertyKey`, for every producer that reaches this terminal\n // (#1883). A route name is used as a property key downstream, so the caller's\n // `name` was coerced again by each consumer of `canonical.name` — and the\n // results could disagree. Measured on bare core, `makeState(bag, {}, {}, \"/x\")`\n // returned a State whose `name` was the caller's OBJECT beside the coerced\n // route's `defaultParams`: one value naming two different things, which is the\n // `ARCHITECTURE.md` criterion's \"an object whose own fields disagree\".\n //\n // ⚑ It also closes what #1889 declared open one door over. Coercing per use,\n // `buildPath` would read four times, throw `'A' is not defined` about a route\n // that EXISTS, and run the caller's `encodeParams` on the way to that\n // guaranteed refusal; a drift could additionally split the encoder read from\n // the matcher read. With one read there is no second answer to disagree with,\n // and the door ANSWERS.\n //\n // ⚠ A COERCION, not a gate, and the difference is load-bearing rather than\n // terminological. #1881 gated three doors and #1897 reverted them; a gate here\n // would have turned `isActiveRoute`'s `true` into `false`, re-introducing one\n // of them through the back door. Measured on both fixtures: this changes\n // `isActiveRoute` not at all (its forwardTo arm reads the name ABOVE this\n // terminal), and `navigate` / `canNavigateTo` never arrive — they refuse on a\n // `Map` miss at zero reads. Pinned in\n // `tests/functional/canonical-name-read-once-1883.test.ts`.\n //\n // ⚠ The `unknown` hop is not style. `name` is DECLARED `string`, and on the\n // strength of that declaration `lint --fix` deletes the coercion — three times\n // in this family already (#1882 twice, #1889's neighbour once). The declared\n // type is the CONTRACT; trusting it is the defect.\n const raw: unknown = name;\n // eslint-disable-next-line unicorn/no-useless-coercion -- the declared `string` is a contract, not a runtime guarantee (#1883)\n const canonicalName = String(raw);\n const forwarded =\n opts?.resolveForward === false\n ? { name: canonicalName, params, search }\n : port.resolveForward(canonicalName, params, search);\n const resolvedName = forwarded.name;\n\n // Path-channel entry guard: drops `undefined`-valued keys and collapses an\n // empty bag onto the EMPTY_PARAMS singleton (#1027), so the zero-params hot\n // path allocates nothing downstream.\n const pathBag = normalizeChannel(forwarded.params, EMPTY_PARAMS);\n\n // The undeclared-key diagnostic (#1579 — the params half of #1553). A key the\n // route declares NOWHERE stays in `state.params` as app-level data, which is\n // correct and documented — but it never reaches the URL, so the state does not\n // round-trip through its own `state.path`. Core does not change that; it only\n // offers to SAY it.\n //\n // Three things keep this from costing anything it should not:\n // - gated on the sink being present, so bare core pays one `undefined` check\n // and never walks the bag (the port member is a GETTER that returns\n // `undefined` until `validation-plugin` installs the validator — a plain\n // closure would always be truthy and the gate would be decorative);\n // - opted in EXPLICITLY by the committing producers, so every predicate stays\n // silent — including `canNavigateTo`, which resolves `forwardTo` and would\n // therefore be caught by a form-based test while still running on every\n // `<Link>` render (measured: it warned before the flag was made explicit);\n // - read from the CALLER's bag, before route defaults are merged in, so a\n // deliberate arbitrary `defaultParams` entry is not reported as a mistake.\n const reportUndeclared =\n opts?.diagnoseUndeclared === true\n ? port.reportUndeclaredParamKey\n : undefined;\n\n if (reportUndeclared) {\n // Read HERE rather than once at the top (#1589): the fast path below does not\n // use the `?`-declared names, only the fact that there are none, and hoisting\n // the read made every predicate pay a port hop for an array it discarded. The\n // diagnostic is opt-in and off on every predicate, so this read is now paid\n // only by the committing producers that asked for it.\n diagnoseUndeclaredKeys(\n port,\n resolvedName,\n pathBag,\n port.queryNames(resolvedName),\n reportUndeclared,\n );\n }\n\n // The route's OWN defaults. Each slot IS its channel — no split (#1549 routed\n // them by the route's declaration for one release; #1548 retired that\n // along with the rest of stage ②). `defaultParams` is the path channel,\n // `defaultSearch` the query channel, and the router never moves a key between\n // them: the two meet only when the URL is printed. A `defaultParams` naming a\n // `?`-declared key is refused at REGISTRATION (`assertRouteDefaultChannels`),\n // so nothing mis-channelled can reach this merge and there is nothing here to\n // repair. Splitting here is what would let a config the router itself\n // accepted survive its own always-on channel guard.\n // FAST PATH (#1589): nothing to merge and nothing to gate. A route with no\n // defaults on either slot, called without a query bag, cannot have a default\n // applied, cannot have a slot withheld, and cannot have a key dropped by the\n // mode gate — whatever the mode, `admittedSearch` would keep every key of a\n // query channel that is empty anyway. So the whole tail below is provably\n // identity, and this returns without it. Note what is NOT in that list: how\n // many names the route declares with `?`. See the gate's own comment.\n //\n // This is the `buildPath/warm-static` case, and it was the most diagnostic\n // number in the regression: a static route — no params, no query, no defaults —\n // paid the full pass and came out 2.6x slower than before the pipeline.\n //\n // ⚠ **No freeze happens in this arm**, which is not the same as handing back\n // something unfrozen: `normalizeChannel` contains zero `freeze` calls, so what\n // it returns is what this returns — the frozen `EMPTY_PARAMS` singleton when\n // nothing survives its walk, a fresh unfrozen object otherwise.\n //\n // What makes the arm correct is the OWNER, not a freeze here: `materialize`\n // freezes `params` at the publication boundary (#1598), and since #1928 it is\n // the only owner, so this arm and the merged one both leave the freeze to it.\n // `query` is the asymmetric one — `EMPTY_SEARCH` is the shared frozen\n // singleton here, and `mergeQueryChannel` freezes on the other arm, because\n // that split is perf-gated rather than required (see `mergeQueryChannel`).\n //\n // ⚠ The query test accepts the EMPTY_SEARCH singleton as well as `undefined`,\n // and that is not cosmetic: `isActiveRoute` and the `forwardState` seam both\n // hand down the singleton rather than nothing, so a test for `undefined` alone\n // left the two render-path predicates — the whole point of the exercise — on\n // the slow path. A fresh `{}` is deliberately NOT accepted: telling an empty\n // literal from a non-empty one costs a key walk, so the two call sites that\n // would otherwise mint one pass the singleton instead.\n // ⚠ TWO facts, one from each side, and between them stage ③ and the mode gate\n // are provably identity (#1589):\n //\n // 1. the CALLER brought no query bag, so the mode gate has nothing to filter\n // and the query merge has nothing on its left;\n // 2. the ROUTE carries no default on either slot, so neither merge has\n // anything on its right.\n //\n // The merged query bag has exactly those two sources, so both being empty is\n // the whole condition. What is NOT in it: how many names the route declares\n // with `?`. Such a term would be redundant against fact 1, because an empty\n // bag has nothing to drop however many names are declared — established, not\n // argued: a 33-probe × 3-mode matrix over a `?`-declaring route with no\n // defaults is byte-identical without it (#1589).\n // Leaving it out costs one port hop less per call\n // (`queryNames` is ~12 ns on its own — `getQueryParams` is a four-frame chain to\n // a cached Map, not a Map read) and widens the fast path to routes that declare\n // query params but carry no defaults.\n //\n // Which leaves the two defaults, and they are read ABOVE the gate on purpose:\n // they are the gate's own route half AND the slow path's first input, so the\n // fast path pays two hops and the slow path pays nothing extra. The alternative\n // — one `port.mergesNothing()` predicate here, defaults re-read below — buys the\n // fast path one more hop (measured: `isActiveRoute-exact` 101 vs 111 ns) at the\n // cost of a FOURTH hop on the defaults path, which measures +6.5 % there.\n // Both are measured; the symmetric one wins because it regresses nothing.\n const defaultPath = port.defaultParams(resolvedName);\n const defaultQuery = port.defaultSearch(resolvedName);\n\n if (\n (forwarded.search === undefined || forwarded.search === EMPTY_SEARCH) &&\n defaultPath === undefined &&\n defaultQuery === undefined\n ) {\n // Annotated rather than asserted: the literal's inferred `query` type is the\n // empty singleton's `Record<string, never>`, too narrow for `Canonical` to\n // overlap, and an inline `as SearchParams` is redundant to the receiver.\n const fastPath: { name: string; path: Params; query: SearchParams } = {\n name: resolvedName,\n path: pathBag,\n query: EMPTY_SEARCH,\n };\n\n // The FAST path's cast to the brand — one of the TWO this function holds\n // (#1968), the slow path's closing the literal at the bottom. Not one act\n // stamped twice: the two paths reach the brand through different work.\n return fastPath as Canonical;\n }\n\n // Below the gate: the SLOW path is the only consumer of the declared names, so\n // the read moved here from the top of the function (#1589) — hoisting it made\n // every predicate pay a port hop for an array it discarded. The two consumers\n // left on this side — the default split and the mode gate — share this one\n // local, so they still cannot disagree about which registry decided the channel\n // (#1556); the diagnostic further up reads its own, through the same accessor,\n // so the one-registry invariant is unchanged.\n const declaredQuery = port.queryNames(resolvedName);\n\n // ③ — route defaults UNDER the routed value, each channel independent. Read\n // per channel (not as one `{ params, search }` bag from a combined `defaults()`\n // accessor) so the merge itself allocates nothing on the zero-defaults hot\n // path — the `Canonical` literal below is this function's only allocation.\n // (`MaterializeOptions` is dissolved into a positional `path` since #1976, so\n // `materialize`'s call site adds no second bag and the count stays one.)\n // In the LITERAL form no seam runs, so nothing has enforced #1570's rule that\n // a default is never applied to a slot the caller already filled — in EITHER\n // bag. Apply it here: the query default and a caller's params-twin land in\n // DIFFERENT channels, where no merge ranks them, and the default would win by\n // construction. `buildPath(\"x\", { page: \"9\" })` on `defaultSearch { page: \"5\" }`\n // would print `?page=5` — the caller's value silently replaced by the default,\n // which is the §1.1 inversion this whole split exists to remove. Nothing is\n // rerouted: the caller's key stays in the bag they chose (and, being in the\n // path channel, is simply not printed — that IS the single-bag retirement),\n // only the default is withheld.\n // ⚠ Scoped to `declaredQuery`, and the scope is load-bearing: only a DECLARED\n // query name can HAVE a params-bag twin. Withholding on a key the route\n // declares nowhere (`/u` + `defaultSearch { theme }`), or on one that owns a\n // path slot beside its query twin (`/items/:id?id`, the #843/#1549 carve-out),\n // takes a default no caller was competing for — and left `buildPath` the only\n // producer out of agreement, printing an href this very route's `matchPath`\n // rewrote on the spot. That is the #1552/#1578 class, re-opened.\n // The mode gate's sink, resolved ONCE — the read IS the gate. The router\n // implements this member as a GETTER returning `undefined` while no validator\n // is installed (`wiring/wireNamespaces.ts`), so in bare core `dropSink` is\n // genuinely absent and the drop path below skips the `pathNames` existence\n // lookup entirely. Hoisting also keeps the getter from being re-invoked per\n // dropped key. (Wired as a plain closure it would be always truthy, so the\n // check reads as taken and bare core pays that lookup with no sink behind it;\n // both sinks report their absence honestly instead.)\n //\n // Read BELOW the fast path (#1589): a route with nothing to gate cannot drop a\n // key, so it has no use for the sink and should not pay the getter.\n const dropSink = port.reportDroppedQueryKey;\n\n const queryDefaults =\n opts?.resolveForward === false\n ? withholdFilledSlots(defaultQuery, pathBag, declaredQuery)\n : defaultQuery;\n\n // ⚑ Normalised BEFORE the merge, exactly as the path bag one branch below.\n // On the non-forwarding arm `forwarded.search` IS the caller's bag, and the\n // merge returns its argument untouched when the route declares no\n // `defaultSearch` — so this is where an `undefined`-valued key or `__proto__`\n // stops. The path channel arrives here already normalised; this makes the two\n // channels agree.\n const searchBag = normalizeChannel(forwarded.search, EMPTY_SEARCH);\n const query = mergeQueryChannel(queryDefaults, searchBag);\n\n return {\n name: resolvedName,\n // ⚑ Two channels, two freeze owners, and each says so in its own name.\n // `mergeQueryChannel` freezes — perf-gated, see its docblock, and NOT\n // required for correctness; `mergePathChannel` does not (`materialize` freezes\n // at the publication boundary, #1598). A second freeze on the path channel bought\n // nothing observable and split what an interceptor sees by route shape —\n // live on a route with no defaults, frozen on every other (#1928).\n //\n // Symmetry towards LIVE rather than towards frozen: `addInterceptor` is a\n // plugin right and the chain is handed the real bag by contract, the same\n // contract `decodeParams` has. A write there is the plugin's business, and\n // the seam sits ABOVE the diagnostics below, so what it writes is reported.\n //\n // ⚑ Both bags are `normalizeChannel`'s own fresh objects — never its input —\n // which is what lets either channel skip the defensive copy\n // {@link adoptForeignBag} makes for a bag the router does not own. ⚠ It is\n // the bag's ROUTE through `normalizeChannel` that earns the claim, not\n // which channel it is: BOTH say it, not the path channel alone (#1812).\n path: mergePathChannel(defaultPath, pathBag),\n // The mode gate (#1575), applied AFTER the default merge so a `defaultSearch`\n // for an undeclared key is dropped with it — under `default`/`strict` that\n // config is dead by the same rule, not a back door around it. Runs on the\n // merged bag rather than the caller's, because that is the bag ⑤a prints\n // from, and the invariant is about those two agreeing.\n query: port.admitsUndeclaredQuery()\n ? query\n : admittedSearch(query as SearchParams, declaredQuery, (key) => {\n // Same existence precondition as the params-bag diagnostic above\n // (#1584): the DROP is always-on and correct either way, but saying\n // \"key `q` is not declared on route `nope`\" about a route that does\n // not exist blames the query for a route-name typo. Found by sweeping\n // this file's port consumers after fixing the sibling — the two\n // diagnostics read the same `[]`-means-nothing answer.\n // The sink is checked FIRST: it is the cheap half, and it is the one\n // that is absent in bare core, so the `pathNames` lookup stays off the\n // path of a router with no validator installed.\n if (\n dropSink !== undefined &&\n port.pathNames(resolvedName) !== undefined\n ) {\n dropSink(resolvedName, key);\n }\n }),\n // The SLOW path's cast to the brand — one of TWO, both in this function\n // (#1968). The other is `return fastPath as Canonical` near the top, and\n // nothing else in the codebase casts to `Canonical`;\n // `canonical-brand-authority-1968` is what notices if that changes.\n } as Canonical;\n}\n","// packages/core/src/pipeline/buildURL.ts\n\nimport type { RouteResolver } from \"./port\";\nimport type { Canonical } from \"./types\";\n\n/**\n * Stage ⑤a — the URL of a canonical intent. Accepts nothing but a\n * {@link Canonical}, so \"print a URL out of un-defaulted channels\" cannot be\n * expressed: the query string is printed from `canonical.query` alone, never from a\n * `search ?? params` fallback.\n *\n * The actual URL assembly stays in the engine and is reached through the port.\n * Nothing interceptable sits between the two (#1938): a plugin acts on the\n * channels ABOVE the route-default merge, at the `forwardState` seam, which is\n * the one every door runs — so what it injects reaches `state.search` and this\n * URL together instead of one of them.\n */\nexport function buildURL(canonical: Canonical, port: RouteResolver): string {\n return port.buildPath(canonical.name, canonical.path, canonical.query);\n}\n","// packages/core/src/pipeline/materialize.ts\n\nimport { DEFAULT_TRANSITION } from \"../constants\";\nimport { freezeStateShell } from \"../helpers\";\n\nimport type { Canonical } from \"./types\";\nimport type { Params, SearchParams, State } from \"../types\";\n\n// ⚑ Captured at module load, for the reason `helpers.ts` states over its own\n// three: an application that re-points `Object.freeze` after boot must not be\n// able to un-freeze what core publishes. This is the ONLY freeze the pipeline\n// performs on `state.params` (#1928) — `mergePathChannel` performs none — so\n// the whole guarantee rests on the capture, which is why\n// `query-strategy-formats-1796` pins it with the global neutered.\nconst freeze = Object.freeze;\n\n/**\n * THE shape of a router State — the pipeline's own, and the one the other five\n * constructors are measured against. ⚠ Not the ONLY place core spells it out:\n * `state-freeze-authority`'s census counts SIX State constructors across five\n * files, and the census is the authority on that number, not this docblock.\n *\n * The shape is inlined here rather than shared through a helper — ⑤b IS \"make\n * the state object\", so it belongs to the primitive that owns the stage rather\n * than to a helper one import away, which would have a single caller and no\n * justification left once `makeState` folded onto the pipeline.\n *\n * `context` is a fresh empty object, intentionally NOT frozen — plugins publish\n * into it via `claim.write(state, value)` after creation.\n *\n * ⚑ `transition` is attached HERE, unconditionally, and that is what lets this\n * literal be ANNOTATED rather than cast (#1976). Spread in behind a deferral\n * flag, the pending shape would be missing a field its own return type declares\n * required, with an `as State<P, S>` laundering it — a guard author writing\n * `toState.transition.reload` compiles and throws. `DEFAULT_TRANSITION`\n * is not a claim that anything succeeded: it is the \"no transition information\"\n * value `matchPath` has always published through {@link materialize}, and\n * `completeTransition` overwrites it with the real meta at the commit.\n *\n * ⚑ One shape, not two, is also why the split below is free: both producers now\n * build the same hidden class, so the commit's `toState.transition = …` is an\n * overwrite rather than the shape transition it was when the field was added\n * after the fact (the cost #1684 paid for and #1694 had to undo).\n */\nfunction buildState<P extends Params, S extends SearchParams>(\n canonical: Canonical,\n path: string,\n): State<P, S> {\n // `Canonical` is deliberately NOT generic: it is one opaque intent shape, and\n // parameterising it would push the caller's `P` through the port and the merge\n // helpers for no gain. The parameter belongs to the FUNCTION, exactly as on\n // `makeState<P, S>` — a public entry point (`matchPath<P>`) instantiates it and\n // hands the result straight back to the consumer, so the chain\n // `matchPath<P>` → `materialize<P>` → `State<P>` has to carry the caller's type\n // (measured: without it `materialize` collapses the chain to `State<Params>`\n // and a consumer's `State<MyParams>` assignment fails TS2322).\n const state: State<P, S> = {\n name: canonical.name,\n params: canonical.path as P,\n search: canonical.query as S,\n path,\n context: {},\n transition: DEFAULT_TRANSITION,\n };\n\n // The path channel is frozen HERE, at the publication boundary — this is the\n // one place a `Canonical` becomes something user code can hold (#1598).\n //\n // `params` ONLY, and that asymmetry is measured rather than stylistic:\n // `canonical.query` is already frozen on every path — the fast path hands over\n // the `EMPTY_SEARCH` singleton, the slow one gets it back frozen from\n // `admittedSearch` — and re-freezing a frozen object is not free (~8 ns), so\n // freezing both regressed `isActiveRoute-exact` by 9.8 % while freezing one\n // wins 5-12 % on every producer that never publishes.\n freeze(state.params);\n\n return state;\n}\n\n/**\n * Stage ⑤b — the State of a canonical intent, ready to publish.\n * Accepts nothing but a {@link Canonical}.\n *\n * Deliberately does NOT call `makeState`: that would re-run stage ③ (idempotent\n * but a wasted pass) and rebuild the path itself, defeating ⑤a. Since Phase 4 it\n * could not, anyway — `makeState` is `canonicalize`'s literal form and would\n * recurse.\n *\n * ⚠ `path` is positional and REQUIRED. The entry points that could want a lazily\n * built path (`canNavigateTo`, `isActiveRoute`) settled the question in Phase 2\n * by calling `buildURL` themselves, so this primitive never grew the port\n * argument the milestone-1 docs left open — and with the deferral flag gone\n * (#1976) an options bag holding one required field bought nothing.\n */\nexport function materialize<\n P extends Params = Params,\n S extends SearchParams = SearchParams,\n>(canonical: Canonical, path: string): State<P, S> {\n return freezeStateShell(buildState<P, S>(canonical, path));\n}\n\n/**\n * Stage ⑤b for a state that is not published yet — same shape, writable shell.\n *\n * ⚑ The deferral is a SECOND ENTRY POINT rather than a `skipFreeze` boolean on\n * {@link materialize}, because the call table says it is two functions: six\n * production sites, three asking for the deferral and three not, none passing an\n * expression, and nobody passing both polarities (#1976). ⚠ Count the sites by\n * NAME, not by `materialize(` — two are spelled `materialize<P>(` and\n * `materialize<P, S>(`, and both sit on the non-deferring side. One flag would\n * govern TWO guarantees — the freeze its name describes, and the presence of\n * `transition`, which it does not — so asking for a writable shell would also\n * ask for an incomplete object. Two names separate them; `transition` is\n * unconditional\n * and only the freeze is deferred, which is what the name always claimed.\n *\n * Three reasons converge on this door, and they are NOT the same reason:\n * `buildNavigateState` NEEDS the writable shell (`completeTransition` attaches\n * the real meta and freezes in one step); `Router.canNavigateTo` wants FIDELITY\n * with the navigate path, so that a capability predicate consults guards with\n * the object shape a real navigation would hand them; and\n * `RoutesNamespace.#matchesActiveStateUnsafe` wants the SPEED — its state exists\n * for the length of one `areStatesEqual` call that reads three fields, and the\n * freeze it skips is ~5 % of that benchmark.\n */\nexport function materializePending<\n P extends Params = Params,\n S extends SearchParams = SearchParams,\n>(canonical: Canonical, path: string): State<P, S> {\n return buildState<P, S>(canonical, path);\n}\n","// packages/core/src/namespaces/StateNamespace/StateNamespace.ts\n\nimport { assertShippedChannelCorrect } from \"../../channels\";\nimport { EMPTY_PARAMS } from \"../../constants\";\nimport { recordsShallowEqual, slotsShallowEqual } from \"../../helpers\";\nimport { buildURL, canonicalize, materialize } from \"../../pipeline\";\n\nimport type { StateNamespaceDependencies } from \"./types\";\nimport type { RouterFSMContext } from \"../../routerFSM\";\nimport type { Params, SearchParams, State } from \"../../types\";\n\n/**\n * State SERVICE — no longer the owner of the state.\n *\n * The two cells live in the FSM context (plan §11.A2); this class keeps a\n * PRIVATE reference to it and reads through that rather than calling\n * `fsm.getContext()` per read. The form was chosen by measurement, not taste:\n * own field 569 ps, private ref 1.98 ns, `fsm.getContext()` 2.39 ns, port hop\n * 2.58 ns — and these reads sit on the render path, where `isActiveRoute` for\n * an inactive link costs ~33 ns in total.\n *\n * What stays here is the SERVICE half — `makeState` and `areStatesEqual` — which\n * never touched the cells: the two halves shared a class name and nothing else\n * (measured: the members' intersection is empty).\n */\nexport class StateNamespace {\n /**\n * The machine's context — the actual home of `current` / `previous`.\n * Assigned once, right after the FSM exists; nothing reads state before that.\n */\n #ctx!: RouterFSMContext;\n\n /**\n * Dependencies injected from Router.\n */\n #deps!: StateNamespaceDependencies;\n\n // =========================================================================\n // Instance methods (trust input - already validated by facade)\n // =========================================================================\n\n /**\n * Returns the current router state.\n *\n * The returned state is deeply frozen (immutable) for safety.\n * Returns `undefined` if the router has not been started or has been stopped.\n */\n get<P extends Params = Params>(): State<P> | undefined {\n return this.#ctx.current as State<P> | undefined; // NOSONAR -- generic narrowing needed for public API\n }\n\n /**\n * Returns the previous router state (before the last navigation).\n */\n getPrevious(): State | undefined {\n return this.#ctx.previous;\n }\n\n /**\n * Hand the service its context. Separate from construction because the\n * namespace is built before the FSM exists — same shape as `setDependencies`.\n */\n setContext(ctx: RouterFSMContext): void {\n this.#ctx = ctx;\n }\n\n // =========================================================================\n // Dependency Injection\n // =========================================================================\n\n /**\n * Sets dependencies for state creation methods.\n * Must be called before using makeState, areStatesEqual, etc.\n */\n setDependencies(deps: StateNamespaceDependencies): void {\n this.#deps = deps;\n }\n\n // =========================================================================\n // State Creation Methods\n // =========================================================================\n\n /**\n * Creates a state object for a route.\n *\n * `params` is frozen at creation so it is always immutable, including on the\n * pending form that defers the outer `Object.freeze(state)` call. This keeps\n * params-freezing invariants independent of transition-pipeline mutation\n * (e.g. `completeTransition` overwriting `state.transition`).\n *\n * **The LITERAL form of the pipeline** (nav-pipeline Phase 4). ONE terminal\n * for one rule, and that is load-bearing rather than tidy: a sweep of\n * `canonicalize`'s PORT consumers cannot see a method that reads its own\n * dependency bag, so a second canonicalisation here would be invisible to\n * exactly the audits that maintain the first (#1584). It IS\n * `canonicalize(…, { resolveForward: false })` — the same form\n * `buildPath` and `isActiveRoute`'s literal arm take, which is exactly this\n * method's documented contract: `forwardTo` is not resolved (`makeState(\"src\")`\n * stays on `\"src\"`) but the NAMED route's defaults are applied (forwardState\n * invariants #7/#8).\n *\n * ⚠ The literal form also applies `withholdFilledSlots` (a query default is\n * declined for a `?`-declared slot the caller filled in the PARAMS bag). That\n * is unreachable here rather than new behaviour: the only door to this method\n * is `PluginApi.makeState`, which runs the P1 channel guard first\n * (`getPluginApi.ts` → `throwOnMisChanneledKey`) on the SAME predicate — own\n * key, defined value, `?`-declared — so the bag that would trigger the\n * withholding is refused before it arrives. Verified by a 71-cell before/after\n * snapshot, not by reading.\n *\n * `context` is initialized as a fresh empty object — intentionally NOT frozen\n * so plugins can publish data via `claim.write(state, value)` after creation.\n */\n makeState<P extends Params = Params, S extends SearchParams = SearchParams>(\n name: string,\n params?: P,\n search?: S,\n path?: string,\n ): State<P, S> {\n // Stages ③ + the mode gate come from the ONE implementation\n // (`canonicalize`); there is no second copy here. `resolveForward: false` is the\n // whole difference from `navigate`'s form, and it is this method's contract:\n // the route NAMED is the route answered about.\n const port = this.#deps.port();\n const canonical = canonicalize(port, name, params ?? EMPTY_PARAMS, search, {\n resolveForward: false,\n });\n\n // ⑤a only when the caller did not supply the URL, and it is built from the\n // SAME canonical intent the state is materialised from — which is what keeps\n // `state.path` in step with `state.search` for a caller that passes no path\n // (`canNavigateTo`, `isActiveRoute`).\n //\n // ⚠ This door reaches NO seam, on either arm (#1938). It is `canonicalize`'s\n // literal form, and its one production caller restores a state from popstate\n // rather than resolving an intent — `seam-coverage-authority-1938` owns the\n // two rows.\n //\n // ⚠ No pending arm, and this method needs none: the two doors that want an\n // unfrozen state (`canNavigateTo`, `isActiveRoute`) reach the pipeline\n // primitive `materializePending` directly (#1976), not through here.\n // The public `PluginApi.makeState` type has four parameters, none optional,\n // so the type makes a fourth argument mandatory rather than customary at\n // whatever call sites exist; unfreezing a state is the pipeline's business,\n // reached through `materialize`, not through this primitive. Coverage is\n // what surfaced it — the same way it caught `deps.makeState` and\n // `paramsMatchExcluding` when Phase 2 migrated their last consumers.\n assertShippedChannelCorrect(\n \"makeState\",\n canonical.name,\n canonical.path,\n port.queryNames(canonical.name),\n );\n\n return materialize<P, S>(canonical, path ?? buildURL(canonical, port));\n }\n\n // =========================================================================\n // State Comparison Methods\n // =========================================================================\n\n /**\n * Compares two states for equality.\n * By default, ignores query params (only compares URL params).\n */\n areStatesEqual(\n state1: State | undefined,\n state2: State | undefined,\n ignoreQueryParams = true,\n ): boolean {\n if (!state1 || !state2) {\n return !!state1 === !!state2;\n }\n\n // ⚑ ONE read of the caller's name (#2085). Both operands are the\n // caller's, and this slot decides TWO things: whether the states name the\n // same route, and — below — which slot set to compare them on. Asked\n // twice, the second answer picks the slots of a route neither state\n // carries, leaving nothing to differ on.\n const name1 = state1.name;\n\n if (name1 !== state2.name) {\n return false;\n }\n\n if (ignoreQueryParams) {\n // URL (path) param names are cached at the routes layer and invalidated\n // on every tree mutation, so this stays correct after replace() (#723).\n return slotsShallowEqual(\n state1.params,\n state2.params,\n this.#deps.getUrlParams(name1),\n );\n }\n\n // Compare BOTH channels — path params and query (search). Query moved out\n // of `params` into `search` in M2 (#1548), so a full comparison must check\n // both. `search` is always present (makeState fills EMPTY_SEARCH).\n return (\n recordsShallowEqual(state1.params, state2.params) &&\n recordsShallowEqual(state1.search, state2.search)\n );\n }\n}\n","// packages/core/src/namespaces/PluginsNamespace/constants.ts\n\nimport {\n events as EVENTS_CONST,\n plugins as PLUGINS_CONST,\n} from \"../../constants\";\n\nimport type { EventName } from \"../../types\";\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/**\n * Maps plugin method names to router event names.\n */\nexport const EVENTS_MAP = {\n [PLUGINS_CONST.ROUTER_START]: EVENTS_CONST.ROUTER_START,\n [PLUGINS_CONST.ROUTER_STOP]: EVENTS_CONST.ROUTER_STOP,\n [PLUGINS_CONST.TRANSITION_SUCCESS]: EVENTS_CONST.TRANSITION_SUCCESS,\n [PLUGINS_CONST.TRANSITION_START]: EVENTS_CONST.TRANSITION_START,\n [PLUGINS_CONST.TRANSITION_LEAVE_APPROVE]:\n EVENTS_CONST.TRANSITION_LEAVE_APPROVE,\n [PLUGINS_CONST.TRANSITION_ERROR]: EVENTS_CONST.TRANSITION_ERROR,\n [PLUGINS_CONST.TRANSITION_CANCEL]: EVENTS_CONST.TRANSITION_CANCEL,\n} as const satisfies Record<\n (typeof PLUGINS_CONST)[keyof typeof PLUGINS_CONST],\n EventName\n>;\n\n/**\n * Plugin method names that correspond to router events.\n */\nexport const EVENT_METHOD_NAMES = objectKeys(\n EVENTS_MAP,\n) as (keyof typeof EVENTS_MAP)[];\n\nexport const LOGGER_CONTEXT = \"router.usePlugin\";\n","// packages/core/src/namespaces/PluginsNamespace/validators.ts\n\n/**\n * Static validation functions for PluginsNamespace.\n * Called by Router facade before instance methods.\n */\n\nimport type { Plugin } from \"../../types\";\n\n/**\n * Validates that a plugin factory returned a valid plugin object.\n */\nexport function validatePlugin(plugin: Plugin): void {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!(plugin && typeof plugin === \"object\") || Array.isArray(plugin)) {\n throw new TypeError(\n `[router.usePlugin] Plugin factory must return an object, got ${typeof plugin}`,\n );\n }\n\n // Detect async factory (returns Promise)\n if (typeof (plugin as unknown as { then?: unknown }).then === \"function\") {\n throw new TypeError(\n `[router.usePlugin] Async plugin factories are not supported. ` +\n `Factory returned a Promise instead of a plugin object.`,\n );\n }\n}\n","// packages/core/src/namespaces/PluginsNamespace/PluginsNamespace.ts\n\nimport { EVENTS_MAP, EVENT_METHOD_NAMES, LOGGER_CONTEXT } from \"./constants\";\nimport { validatePlugin } from \"./validators\";\n\nimport type { PluginsDependencies } from \"./types\";\nimport type {\n DefaultDependencies,\n Plugin,\n Unsubscribe,\n PluginFactory,\n} from \"../../types\";\n\n/** Captured like the deciding seven, but this one BUILDS the guarantee (#2073). */\nconst freeze = Object.freeze;\n\n/**\n * Independent namespace for managing plugins.\n *\n * Static methods handle validation (called by facade).\n * Instance methods handle storage and business logic.\n */\nexport class PluginsNamespace<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> {\n readonly #plugins = new Set<PluginFactory<Dependencies>>();\n readonly #unsubscribes = new Set<Unsubscribe>();\n\n #deps!: PluginsDependencies<Dependencies>;\n\n // =========================================================================\n // Static validation methods (called by facade before instance methods)\n // Proxy to functions in validators.ts for separation of concerns\n // =========================================================================\n\n static validatePlugin(plugin: Plugin): void {\n validatePlugin(plugin);\n }\n\n // =========================================================================\n // Dependency injection\n // =========================================================================\n\n setDependencies(deps: PluginsDependencies<Dependencies>): void {\n this.#deps = deps;\n }\n\n // =========================================================================\n // Instance methods (trust input - already validated by facade)\n // =========================================================================\n\n /**\n * Returns the number of registered plugins.\n * Used by facade for limit validation.\n */\n /* v8 ignore next 3 -- @preserve: only called via validator interface (ctx.validator?.plugins.validatePluginLimit), not reachable without validation plugin */\n count(): number {\n return this.#plugins.size;\n }\n\n /**\n * Registers one or more plugin factories.\n * Returns unsubscribe function to remove all added plugins.\n * Input already validated by facade (limit, duplicates).\n *\n * @param factories - Already validated by facade\n */\n use(...factories: PluginFactory<Dependencies>[]): Unsubscribe {\n // Emit warnings for count thresholds (not validation, just warnings)\n this.#deps\n .getValidator()\n ?.plugins.validateCountThresholds(this.#plugins.size + factories.length);\n\n // Fast path for single plugin (common case)\n if (factories.length === 1) {\n const factory = factories[0];\n const cleanup = this.#startPlugin(factory);\n\n this.#plugins.add(factory);\n\n let unsubscribed = false;\n\n const unsubscribe: Unsubscribe = () => {\n if (unsubscribed) {\n return;\n }\n\n unsubscribed = true;\n this.#plugins.delete(factory);\n this.#unsubscribes.delete(unsubscribe);\n try {\n cleanup();\n } catch (error) {\n this.#deps.logger.error(\n LOGGER_CONTEXT,\n \"Error during cleanup:\",\n error,\n );\n }\n };\n\n this.#unsubscribes.add(unsubscribe);\n\n return unsubscribe;\n }\n\n // Deduplicate batch with warning (validation already done by facade)\n const seenInBatch = this.#deduplicateBatch(factories);\n\n // Track successfully initialized plugins for cleanup\n const initializedPlugins: {\n factory: PluginFactory<Dependencies>;\n cleanup: Unsubscribe;\n }[] = [];\n\n // Initialize deduplicated plugins sequentially\n try {\n for (const plugin of seenInBatch) {\n const cleanup = this.#startPlugin(plugin);\n\n initializedPlugins.push({ factory: plugin, cleanup });\n }\n } catch (error) {\n // Rollback on failure - cleanup all initialized plugins\n for (const { cleanup } of initializedPlugins) {\n try {\n cleanup();\n } catch (cleanupError) {\n this.#deps.logger.error(\n LOGGER_CONTEXT,\n \"Cleanup error:\",\n cleanupError,\n );\n }\n }\n\n throw error;\n }\n\n // Commit phase - add to registry\n for (const { factory } of initializedPlugins) {\n this.#plugins.add(factory);\n }\n\n // Return unsubscribe function\n let unsubscribed = false;\n\n const unsubscribe: Unsubscribe = () => {\n if (unsubscribed) {\n return;\n }\n\n unsubscribed = true;\n this.#unsubscribes.delete(unsubscribe);\n\n for (const { factory } of initializedPlugins) {\n this.#plugins.delete(factory);\n }\n\n for (const { cleanup } of initializedPlugins) {\n try {\n cleanup();\n } catch (error) {\n this.#deps.logger.error(\n LOGGER_CONTEXT,\n \"Error during cleanup:\",\n error,\n );\n }\n }\n };\n\n this.#unsubscribes.add(unsubscribe);\n\n return unsubscribe;\n }\n\n /**\n * Returns registered plugin factories.\n */\n getAll(): PluginFactory<Dependencies>[] {\n return [...this.#plugins];\n }\n\n /**\n * Disposes all registered plugins by running their teardown callbacks\n * and removing event listener subscriptions.\n * Active disposal is required because plugins have an active lifecycle\n * (event subscriptions, teardown hooks).\n * Named \"dispose\" (not \"clear\") because there is active cleanup to perform.\n */\n disposeAll(): void {\n for (const unsubscribe of this.#unsubscribes) {\n unsubscribe();\n }\n\n this.#plugins.clear();\n this.#unsubscribes.clear();\n }\n\n // =========================================================================\n // Private methods\n // =========================================================================\n\n /**\n * Deduplicates batch with warning for duplicates within batch.\n * Validation (existing duplicates) is done by facade.\n */\n #deduplicateBatch(\n plugins: PluginFactory<Dependencies>[],\n ): Set<PluginFactory<Dependencies>> {\n const seenInBatch = new Set<PluginFactory<Dependencies>>();\n\n for (const plugin of plugins) {\n if (seenInBatch.has(plugin)) {\n this.#deps.getValidator()?.plugins.warnBatchDuplicates(plugins);\n } else {\n seenInBatch.add(plugin);\n }\n }\n\n return seenInBatch;\n }\n\n #startPlugin(pluginFactory: PluginFactory<Dependencies>): Unsubscribe {\n const appliedPlugin = this.#deps.compileFactory(pluginFactory);\n\n PluginsNamespace.validatePlugin(appliedPlugin);\n this.#deps.getValidator()?.plugins.validatePluginKeys(appliedPlugin);\n\n freeze(appliedPlugin);\n\n // Collect all unsubscribe functions\n const removeEventListeners: Unsubscribe[] = [];\n\n // Subscribe plugin methods to corresponding router events\n for (const methodName of EVENT_METHOD_NAMES) {\n if (methodName in appliedPlugin) {\n if (typeof appliedPlugin[methodName] === \"function\") {\n removeEventListeners.push(\n this.#deps.addEventListener(\n EVENTS_MAP[methodName],\n appliedPlugin[methodName],\n ),\n );\n\n if (methodName === \"onStart\" && this.#deps.canNavigate()) {\n this.#deps.getValidator()?.plugins.warnPluginAfterStart(methodName);\n }\n } else {\n this.#deps.getValidator()?.plugins.warnPluginMethodType(methodName);\n }\n }\n }\n\n // Return composite cleanup function\n return () => {\n for (const removeListener of removeEventListeners) {\n removeListener();\n }\n\n if (typeof appliedPlugin.teardown === \"function\") {\n appliedPlugin.teardown();\n }\n };\n }\n}\n","// packages/core/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts\n\nimport type { RouteLifecycleDependencies } from \"./types\";\nimport type {\n DefaultDependencies,\n GuardFn,\n State,\n GuardFnFactory,\n} from \"../../types\";\n\n/** Captured like the deciding seven, but this one BUILDS the guarantee (#2072). */\nconst objectCreate = Object.create;\n\n// Boolean shorthand has only two possible values, so the guard and its factory\n// are module-level singletons — registering `true`/`false` reuses one cached\n// factory instead of allocating a fresh closure per call (#962).\nconst TRUE_GUARD: GuardFn = () => true;\nconst FALSE_GUARD: GuardFn = () => false;\nconst TRUE_FACTORY: GuardFnFactory = () => TRUE_GUARD;\nconst FALSE_FACTORY: GuardFnFactory = () => FALSE_GUARD;\n\n/**\n * Converts a boolean value to a guard function factory.\n * Used for the shorthand syntax where true/false is passed instead of a function.\n * Returns one of two cached factories — no per-call allocation (#962).\n */\nfunction booleanToFactory<Dependencies extends DefaultDependencies>(\n value: boolean,\n): GuardFnFactory<Dependencies> {\n return value ? TRUE_FACTORY : FALSE_FACTORY;\n}\n\n/**\n * Puts a slot back the way it was: re-seat the previous entry, or delete the\n * slot when there was none. Used for the factory Map and its compiled twin\n * alike — since #1649 the two are one unit and roll back together, so the\n * symmetry is the point rather than a coincidence (#963).\n */\nfunction restoreSlot<T>(\n map: Map<string, T>,\n name: string,\n previous: T | undefined,\n): void {\n if (previous === undefined) {\n map.delete(name);\n } else {\n map.set(name, previous);\n }\n}\n\n/**\n * Origin lane for a guard clear. Every `clearCanActivate` / `clearCanDeactivate`\n * caller names its lane — there is no origin-blind default — so a new call site\n * cannot silently wipe both the route-config and the external guard (#1171):\n *\n * - `\"definition\"` — clear only the route-config guard (`update(name, {…: null})`, #952).\n * - `\"external\"` — clear only the external, component-managed guard\n * (`removeXGuard()` and post-leave auto-cleanup — the inverse of `addXGuard()`).\n * - `\"both\"` — clear both (route removal / router teardown; the route is gone).\n */\nexport type GuardClearScope = \"definition\" | \"external\" | \"both\";\n\n/**\n * Source of truth for `canActivate` / `canDeactivate` guards.\n *\n * Storage is split by origin into four factory Maps (definition vs external,\n * each ×activate/deactivate); a single compiled-function Map per kind backs\n * navigation (\"external wins\" — when a route holds both a definition and an\n * external guard, the compiled guard is the external one, regardless of\n * registration order; #1174). `getFunctions()` returns a cached\n * `[deactivate, activate]` tuple for the hot path (stable reference, no\n * per-navigate allocation).\n *\n * All input validation is handled upstream by `getLifecycleApi` and\n * `getRoutesApi` — this class has no static methods.\n *\n * **Ordering convention.** Every paired surface lists **deactivate before\n * activate**: `getFunctions()` / `getFactories()` return `[deactivate,\n * activate]`, `getFactoriesByOrigin()` returns that tuple per origin, and\n * `canNavigateTo(toDeactivate, toActivate, …)` takes deactivate first. Keep\n * any new paired surface consistent with this order.\n */\nexport class RouteLifecycleNamespace<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> {\n // Storage split by origin: definition vs external. Resolution is\n // EXTERNAL-WINS regardless of registration order (#1174): the compiled slot\n // reflects the external factory whenever one exists, else the definition. One\n // policy across every path — `#registerHandler` (keeps external over a later\n // definition), `#recompileSlot`, and `clearDefinitionGuards` (#1192) — so a\n // clone's fixed definition→external replay yields the source's effective guard\n // with no extra tracking. Both semantics are expressed over these primary Maps.\n readonly #definitionActivateFactories = new Map<\n string,\n GuardFnFactory<Dependencies>\n >();\n readonly #externalActivateFactories = new Map<\n string,\n GuardFnFactory<Dependencies>\n >();\n readonly #definitionDeactivateFactories = new Map<\n string,\n GuardFnFactory<Dependencies>\n >();\n readonly #externalDeactivateFactories = new Map<\n string,\n GuardFnFactory<Dependencies>\n >();\n // Compiled-function view. Single Map per kind because navigation does not\n // distinguish origin — it just runs the effective guard. Set on add\n // (external-wins — a definition does not overwrite a live external, #1174)\n // and re-derived on clear from whichever origin Map still holds the slot.\n readonly #canDeactivateFunctions = new Map<string, GuardFn>();\n readonly #canActivateFunctions = new Map<string, GuardFn>();\n // The compiled form kept BESIDE its factory, split by origin exactly like the\n // factory Maps above (#1649). This is the whole reason `#recompileSlot` is a\n // Map READ and not a factory INVOCATION: clearing a guard no longer runs\n // application code, so neither the destructive post-leave cleanup in\n // `completeTransition` nor `replace()`'s `clearDefinitionGuards` can be torn\n // down from inside itself. Nothing extra is compiled to fill them —\n // `#registerHandler` already compiled the definition factory and DISCARDED\n // the result whenever external won; these Maps keep it instead.\n // Contract consequence (named in the changeset): a guard factory runs exactly\n // ONCE per registration per router. Re-registration, `cloneRouter` and\n // route-CRUD still re-run it; a slot re-derivation no longer does, so a\n // factory that reads a dependency at compile time no longer silently\n // re-reads it at an unpredictable moment.\n readonly #compiledDefinitionDeactivate = new Map<string, GuardFn>();\n readonly #compiledExternalDeactivate = new Map<string, GuardFn>();\n readonly #compiledDefinitionActivate = new Map<string, GuardFn>();\n readonly #compiledExternalActivate = new Map<string, GuardFn>();\n // Cached tuple — Maps never change reference, so this is stable\n readonly #functionsTuple: [Map<string, GuardFn>, Map<string, GuardFn>] = [\n this.#canDeactivateFunctions,\n this.#canActivateFunctions,\n ];\n\n #deps!: RouteLifecycleDependencies<Dependencies>;\n\n setDependencies(deps: RouteLifecycleDependencies<Dependencies>): void {\n this.#deps = deps;\n }\n\n getHandlerCount(type: \"activate\" | \"deactivate\"): number {\n const definitionMap =\n type === \"activate\"\n ? this.#definitionActivateFactories\n : this.#definitionDeactivateFactories;\n const externalMap =\n type === \"activate\"\n ? this.#externalActivateFactories\n : this.#externalDeactivateFactories;\n\n if (definitionMap.size === 0) {\n return externalMap.size;\n }\n\n if (externalMap.size === 0) {\n return definitionMap.size;\n }\n\n const names = new Set(definitionMap.keys());\n\n for (const name of externalMap.keys()) {\n names.add(name);\n }\n\n return names.size;\n }\n\n /**\n * Pre-flights the #961 handler-limit `RangeError` into the route-CRUD PREPARE\n * phase (#1046). `#registerHandler`'s per-slot limit check throws AFTER the\n * tree/config swap, so `add`/`replace`/`update` tore post-commit when the\n * validator was installed and the per-type count was at `maxLifecycleHandlers`.\n * Running the same check here — before any store mutation — restores atomicity\n * (#951/#956/#698): a batch that would exceed the limit aborts before a single\n * write.\n *\n * Only NEW slots count (an overwrite leaves the union count unchanged, mirroring\n * `#registerHandler`). For `replace` (`clearsDefinition = true`) the definition\n * guards are about to be cleared, so the projection runs against the surviving\n * EXTERNAL guards only — exactly the post-clear state the install loop sees.\n * Plugin-gated: a no-op without the validator (the limit is opt-in).\n *\n * @param activateNames - route names a `canActivate` would be registered for\n * @param deactivateNames - route names a `canDeactivate` would be registered for\n * @param clearsDefinition - true for `replace` (definition guards cleared first)\n */\n preflightHandlerLimit(\n activateNames: Iterable<string>,\n deactivateNames: Iterable<string>,\n clearsDefinition: boolean,\n ): void {\n const validator = this.#deps.getValidator();\n\n if (!validator) {\n return;\n }\n\n const check = (\n type: \"activate\" | \"deactivate\",\n names: Iterable<string>,\n methodName: string,\n ): void => {\n const { definition, external } = this.#getFactoryMaps(type);\n\n // A name already holding a guard of this type is an overwrite (no new\n // slot, mirroring `#registerHandler`). After a definition-clear (replace)\n // only EXTERNAL guards survive, so the existing-name check — and the base\n // count below — run against `external` alone, matching the post-clear\n // install state the loop sees.\n let newSlots = 0;\n\n for (const name of names) {\n const isExisting = clearsDefinition\n ? external.has(name)\n : definition.has(name) || external.has(name);\n\n if (!isExisting) {\n newSlots++;\n }\n }\n\n if (newSlots === 0) {\n return;\n }\n\n // The install loop throws when a new-slot registration observes\n // `count >= max`. Starting from `base` and adding `newSlots` new names,\n // the highest pre-register count it reaches is `base + newSlots - 1` —\n // replicate that worst case so the pre-flight throws iff the loop would.\n const base = clearsDefinition\n ? external.size\n : this.getHandlerCount(type);\n\n validator.lifecycle.validateHandlerLimit(base + newSlots - 1, methodName);\n };\n\n check(\"activate\", activateNames, \"canActivate\");\n check(\"deactivate\", deactivateNames, \"canDeactivate\");\n }\n\n // =========================================================================\n // Instance methods\n // =========================================================================\n\n /**\n * Adds a canActivate guard for a route.\n *\n * @param name - Route name (input-validated by facade)\n * @param handler - Guard function or boolean (input-validated by facade)\n * @param isFromDefinition - True when guard comes from route definition\n * (lands in the definition Map; subject to `clearDefinitionGuards()`).\n * False when added via `getLifecycleApi().addActivateGuard(...)` (lands in\n * the external Map; survives `replace()`).\n *\n * ⚑ REQUIRED, with no default (#1977). The clear side states the rule for\n * itself — \"there is no origin-blind default, so every caller commits to a\n * lane and a new call site cannot silently clear both\" — and a default here\n * contradicts it, landing on the MINORITY polarity: three of the four\n * in-repo callers are the definition lane and would have to remember `true`.\n * A forgotten argument files a definition guard as EXTERNAL, where\n * `clearDefinitionGuards()` does not reach it, so `replace()` keeps a guard\n * belonging to a tree that no longer exists (measured: 1 surviving guard\n * against 0 for the same call with the argument). An optional parameter\n * gives the type nothing to catch.\n *\n * External wins at runtime (#1174): when a route holds both a definition and\n * an external guard, the compiled function is the external one, regardless of\n * registration order. Within one origin the most recent add overwrites. Origin\n * determines which Map the factory is filed under (relevant for\n * `clearDefinitionGuards()` and `cloneRouter` re-registration).\n */\n addCanActivate(\n name: string,\n handler: GuardFnFactory<Dependencies> | boolean,\n isFromDefinition: boolean,\n precompiledFn?: GuardFn,\n ): void {\n this.#registerHandler(\n \"activate\",\n name,\n handler,\n isFromDefinition,\n \"canActivate\",\n precompiledFn,\n );\n }\n\n /**\n * Adds a canDeactivate guard for a route.\n *\n * Symmetric counterpart to {@link addCanActivate}.\n */\n addCanDeactivate(\n name: string,\n handler: GuardFnFactory<Dependencies> | boolean,\n isFromDefinition: boolean,\n precompiledFn?: GuardFn,\n ): void {\n this.#registerHandler(\n \"deactivate\",\n name,\n handler,\n isFromDefinition,\n \"canDeactivate\",\n precompiledFn,\n );\n }\n\n /**\n * Removes a canActivate guard for a route. `scope` names the origin lane\n * (see {@link GuardClearScope}) — there is no origin-blind default, so every\n * caller commits to a lane and a new call site cannot silently clear both.\n * Delegates to {@link #clearGuard} (mirrors the add side's `#registerHandler`).\n *\n * @param name - Route name (already validated by facade)\n * @param scope - Which origin(s) to clear: `\"definition\"` / `\"external\"` / `\"both\"`\n */\n clearCanActivate(name: string, scope: GuardClearScope): void {\n this.#clearGuard(\"activate\", name, scope);\n }\n\n /**\n * Removes a canDeactivate guard for a route. Symmetric counterpart to\n * {@link clearCanActivate}.\n *\n * The `\"external\"` lane is what makes a route-config `canDeactivate` durable:\n * post-leave auto-cleanup (`completeTransition`) and `removeDeactivateGuard()`\n * unregister only the external, component-managed guard (router5 mount/unmount\n * heritage), while a definition guard survives for re-entry — symmetric with\n * definition `canActivate`, which lives as long as the route is in the tree\n * (#1171). Clearing both by default made a config guard one-shot: the first\n * permitted leave erased it, so re-entry was unguarded, `getRoutesApi().get()`\n * lost the field, and a clone taken after the leave never received it\n * (clone invariant #6).\n *\n * @param name - Route name (already validated by facade)\n * @param scope - Which origin(s) to clear: `\"definition\"` / `\"external\"` / `\"both\"`\n */\n clearCanDeactivate(name: string, scope: GuardClearScope): void {\n this.#clearGuard(\"deactivate\", name, scope);\n }\n\n /**\n * Clears all lifecycle handlers (canActivate and canDeactivate).\n * Used by clearRoutes to reset all lifecycle state.\n */\n clearAll(): void {\n this.#definitionActivateFactories.clear();\n this.#externalActivateFactories.clear();\n this.#definitionDeactivateFactories.clear();\n this.#externalDeactivateFactories.clear();\n this.#canActivateFunctions.clear();\n this.#canDeactivateFunctions.clear();\n this.#compiledDefinitionDeactivate.clear();\n this.#compiledExternalDeactivate.clear();\n this.#compiledDefinitionActivate.clear();\n this.#compiledExternalActivate.clear();\n }\n\n /**\n * Clears only lifecycle handlers that were registered from route definitions.\n * Used by HMR `replace()` to remove definition-sourced guards without\n * touching externally-added guards.\n *\n * For a slot where BOTH a definition and an external guard exist, the external\n * factory survives — and the compiled function is RE-DERIVED from it (#1192).\n * Under external-wins (#1174) the compiled slot is already the external guard,\n * so this re-derivation is idempotent — it goes through the same choke point\n * that keeps clearing correct (and stays robust if the compiled slot were ever\n * out of sync). For a definition-only slot, the compiled function is dropped.\n *\n * ⚑ Since #1649 the re-derivation READS the surviving external guard's stored\n * compiled form instead of invoking its factory, which is what stopped this\n * method from running application code mid-`replace()` (the #1627 site).\n */\n clearDefinitionGuards(): void {\n for (const name of this.#definitionActivateFactories.keys()) {\n this.#compiledDefinitionActivate.delete(name);\n\n if (this.#externalActivateFactories.has(name)) {\n this.#recompileSlot(\"activate\", name);\n } else {\n this.#canActivateFunctions.delete(name);\n }\n }\n\n for (const name of this.#definitionDeactivateFactories.keys()) {\n this.#compiledDefinitionDeactivate.delete(name);\n\n if (this.#externalDeactivateFactories.has(name)) {\n this.#recompileSlot(\"deactivate\", name);\n } else {\n this.#canDeactivateFunctions.delete(name);\n }\n }\n\n this.#definitionActivateFactories.clear();\n this.#definitionDeactivateFactories.clear();\n }\n\n /**\n * Returns lifecycle factories as a flat `[deactivate, activate]` tuple of\n * `Record<name, factory>` — the effective view where external wins over\n * definition for the same slot. Used by `getRoutesApi` to enrich route\n * objects with their current canActivate / canDeactivate factories and by\n * the route-removal cleanup path.\n *\n * For cloneRouter (which needs to preserve origin on re-registration), use\n * {@link getFactoriesByOrigin} instead.\n */\n getFactories(): [\n Record<string, GuardFnFactory<Dependencies>>,\n Record<string, GuardFnFactory<Dependencies>>,\n ] {\n // Null-prototype dictionaries: these are keyed by a ROUTE NAME, and core\n // accepts a route named after any `Object.prototype` member. A plain `{}`\n // breaks the consumers of these records on both axes — `getRoutesApi` asks\n // `name in record` and finds an inherited member (a `canDeactivate`\n // nobody registered), while the clone path enumerates them (reaching them\n // through {@link getFactoriesByOrigin}, not this method), and the\n // `\"__proto__\"` write dispatches into the inherited setter so the record\n // has no own key to enumerate — the clone silently loses the guard\n // (#1801). The rest of this layer is already null-prototype for exactly\n // this reason: all six `RouteConfig` maps, `routeCustomFields`, and the\n // matcher's `staticChildren`.\n const deactivateRecord = objectCreate(null) as Record<\n string,\n GuardFnFactory<Dependencies>\n >;\n const activateRecord = objectCreate(null) as Record<\n string,\n GuardFnFactory<Dependencies>\n >;\n\n for (const [name, factory] of this.#definitionDeactivateFactories) {\n deactivateRecord[name] = factory;\n }\n for (const [name, factory] of this.#externalDeactivateFactories) {\n deactivateRecord[name] = factory;\n }\n\n for (const [name, factory] of this.#definitionActivateFactories) {\n activateRecord[name] = factory;\n }\n for (const [name, factory] of this.#externalActivateFactories) {\n activateRecord[name] = factory;\n }\n\n return [deactivateRecord, activateRecord];\n }\n\n /**\n * Returns factories tagged by origin — definition and external as separate\n * `[deactivate, activate]` tuples. Used by `cloneRouter` to re-register\n * guards on the clone with their original origin flag preserved.\n */\n getFactoriesByOrigin(): {\n definition: [\n Record<string, GuardFnFactory<Dependencies>>,\n Record<string, GuardFnFactory<Dependencies>>,\n ];\n external: [\n Record<string, GuardFnFactory<Dependencies>>,\n Record<string, GuardFnFactory<Dependencies>>,\n ];\n } {\n // Null-prototype, for the reason spelled out in `getFactories` above\n // (#1801) — and these four are the ones `cloneRouter` ENUMERATES, i.e.\n // the half where a swapped prototype loses the guard outright.\n const defDeact = objectCreate(null) as Record<\n string,\n GuardFnFactory<Dependencies>\n >;\n const defAct = objectCreate(null) as Record<\n string,\n GuardFnFactory<Dependencies>\n >;\n const extensionDeact = objectCreate(null) as Record<\n string,\n GuardFnFactory<Dependencies>\n >;\n const extensionAct = objectCreate(null) as Record<\n string,\n GuardFnFactory<Dependencies>\n >;\n\n for (const [name, factory] of this.#definitionDeactivateFactories) {\n defDeact[name] = factory;\n }\n for (const [name, factory] of this.#definitionActivateFactories) {\n defAct[name] = factory;\n }\n for (const [name, factory] of this.#externalDeactivateFactories) {\n extensionDeact[name] = factory;\n }\n for (const [name, factory] of this.#externalActivateFactories) {\n extensionAct[name] = factory;\n }\n\n return {\n definition: [defDeact, defAct],\n external: [extensionDeact, extensionAct],\n };\n }\n\n /**\n * Returns compiled lifecycle functions for transition execution.\n *\n * @returns Tuple of [canDeactivateFunctions, canActivateFunctions] as Maps\n */\n getFunctions(): [Map<string, GuardFn>, Map<string, GuardFn>] {\n return this.#functionsTuple;\n }\n\n canNavigateTo(\n toDeactivate: string[],\n toActivate: string[],\n toState: State,\n fromState: State | undefined,\n ): boolean {\n for (const segment of toDeactivate) {\n if (\n !this.#checkGuardSync(\n this.#canDeactivateFunctions,\n segment,\n toState,\n fromState,\n \"canNavigateTo\",\n )\n ) {\n return false;\n }\n }\n\n for (const segment of toActivate) {\n if (\n !this.#checkGuardSync(\n this.#canActivateFunctions,\n segment,\n toState,\n fromState,\n \"canNavigateTo\",\n )\n ) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * Compiles a guard factory to its `GuardFn` WITHOUT registering it — surfaces\n * a throwing / non-function factory eagerly. The prepare-then-commit\n * add/replace path (`adoptRouteArtifacts`) calls this for every pending guard\n * BEFORE the store swap (#956), so a malformed factory aborts the mutation\n * with the store untouched. The returned function is then installed via the\n * `precompiledFn` argument of {@link addCanActivate} / {@link addCanDeactivate}\n * — no re-compile, so a factory with compile-time side effects runs exactly\n * once. Same boolean-shorthand handling + compile + non-function check as the\n * inline `#registerHandler` path, so a route-config `canActivate: true`\n * (boolean shorthand, runtime-reachable via the public route type) compiles to\n * the cached `TRUE_GUARD`/`FALSE_GUARD` instead of throwing on a non-callable.\n */\n compileGuardFactory(\n handler: GuardFnFactory<Dependencies> | boolean,\n methodName: string,\n ): GuardFn {\n const factory =\n typeof handler === \"boolean\"\n ? booleanToFactory<Dependencies>(handler)\n : handler;\n const fn = this.#deps.compileFactory(factory);\n\n if (typeof fn !== \"function\") {\n throw new TypeError(\n `[router.${methodName}] Factory must return a function, got ${typeof fn}`,\n );\n }\n\n return fn;\n }\n\n // =========================================================================\n // Private methods (business logic)\n // =========================================================================\n\n /**\n * Routes a registration into the origin-specific factory Map and updates the\n * compiled function under EXTERNAL-WINS (#1174): the just-added factory becomes\n * the compiled guard unless it is a definition registered while an external\n * guard is already live (then external stays effective; the definition is still\n * stored for a later `clearDefinitionGuards()`). Within one origin the most\n * recent add overwrites.\n * Emits overwrite / threshold warnings symmetric with the pre-refactor\n * single-Map behaviour: any prior entry for the slot — same origin or\n * cross-origin — counts as an overwrite for the warning surface; only a\n * brand-new slot (no entry in either Map) increments the threshold check.\n */\n #registerHandler(\n type: \"activate\" | \"deactivate\",\n name: string,\n handler: GuardFnFactory<Dependencies> | boolean,\n isFromDefinition: boolean,\n methodName: string,\n precompiledFn?: GuardFn,\n ): void {\n const factoryMaps = this.#getFactoryMaps(type);\n const functions =\n type === \"activate\"\n ? this.#canActivateFunctions\n : this.#canDeactivateFunctions;\n const targetMap = isFromDefinition\n ? factoryMaps.definition\n : factoryMaps.external;\n const otherMap = isFromDefinition\n ? factoryMaps.external\n : factoryMaps.definition;\n\n const isOverwrite = targetMap.has(name) || otherMap.has(name);\n\n if (isOverwrite) {\n this.#deps\n .getValidator()\n ?.lifecycle.warnOverwrite(name, type, methodName);\n } else {\n // Single enforcement choke point for EVERY registration path: programmatic\n // (getLifecycleApi) and route-config (getRoutesApi.add/update, where\n // isFromDefinition=true). The hard limit throws here so route-config guards\n // are bounded exactly like programmatic ones (#961); the approaching-limit\n // warning follows. Only new slots count toward the limit — an overwrite\n // leaves the count unchanged. `getHandlerCount` is read once and only when\n // the validator is installed (opt-in), so the no-plugin path stays free.\n const validator = this.#deps.getValidator();\n\n if (validator) {\n const count = this.getHandlerCount(type);\n\n validator.lifecycle.validateHandlerLimit(count, methodName);\n validator.lifecycle.validateCountThresholds(count + 1, methodName);\n }\n }\n\n const factory =\n typeof handler === \"boolean\"\n ? booleanToFactory<Dependencies>(handler)\n : handler;\n\n // Capture the slot's prior factory (if any) BEFORE the overwrite, so a\n // compile-throw can be rolled back to the previously-valid guard rather\n // than dropping it (#963).\n const previousFactory = targetMap.get(name);\n // The compiled twin of the slot being overwritten, captured for the same\n // rollback (#1649): factory and compiled form are one unit now, so they\n // must move together or `#recompileSlot` would re-derive from a factory\n // whose compiled form belongs to a different registration.\n const compiledMaps = this.#getCompiledMaps(type);\n const compiledTarget = isFromDefinition\n ? compiledMaps.definition\n : compiledMaps.external;\n const previousCompiled = compiledTarget.get(name);\n\n targetMap.set(name, factory);\n\n // External-wins (#1174): the compiled slot reflects the external guard\n // whenever one exists, regardless of registration order. A definition\n // registered while an external guard is live is still stored (so a later\n // replace()-clear can re-derive from it via `#recompileSlot`) but does NOT\n // overwrite the compiled function — external stays effective. This makes\n // `#registerHandler` consistent with `#recompileSlot` / `clearDefinitionGuards`\n // (both external-wins, #1192), so the whole namespace has ONE policy, and\n // cloneRouter's fixed definition→external replay yields the same effective\n // guard as the source with no extra origin tracking. (The factory is still\n // compiled below to validate it and to keep the rollback path symmetric.)\n const externalWins = isFromDefinition && otherMap.has(name);\n\n try {\n // A pre-validated function (from the #956 add/replace pre-compile) is\n // installed directly — no re-compile; otherwise compile + non-function\n // check here (`compileGuardFactory` throws on a bad factory).\n const fn = precompiledFn ?? this.compileGuardFactory(factory, methodName);\n\n // Stored whatever the origin — INCLUDING the definition compile that\n // external-wins discards below. That discarded result is exactly what a\n // later `clearCanDeactivate`/`clearDefinitionGuards` needs, and storing it\n // is what spares them a second run of the factory (#1649).\n compiledTarget.set(name, fn);\n\n if (!externalWins) {\n functions.set(name, fn);\n }\n } catch (error) {\n // Roll the slot back to its pre-call state: restore the previous factory\n // on an overwrite (#963) — and its compiled twin with it (#1649) — else\n // clear both. `#recompileSlot` then resets the compiled function from\n // whichever origin still holds the slot: the restored same-origin entry,\n // a surviving cross-origin one, or (empty slot) deletes it.\n restoreSlot(targetMap, name, previousFactory);\n restoreSlot(compiledTarget, name, previousCompiled);\n\n this.#recompileSlot(type, name);\n\n throw error;\n }\n }\n\n /**\n * Shared implementation for {@link clearCanActivate} / {@link clearCanDeactivate}\n * — the clear-side counterpart to {@link #registerHandler}. `scope` selects the\n * origin lane (no origin-blind default, #1171); when one origin is cleared and\n * the other survives, `#recompileSlot` re-derives the compiled function from\n * the survivor's stored compiled form (external wins, #1174) — a READ, never a\n * re-compile, which is what keeps this clear free of application code (#1649).\n */\n #clearGuard(\n type: \"activate\" | \"deactivate\",\n name: string,\n scope: GuardClearScope,\n ): void {\n const { definition, external } = this.#getFactoryMaps(type);\n const compiled = this.#getCompiledMaps(type);\n const clearedDefinition =\n scope === \"external\" ? false : definition.delete(name);\n const clearedExternal =\n scope === \"definition\" ? false : external.delete(name);\n\n // The compiled form is part of the slot, so it is cleared with it (#1649).\n if (clearedDefinition) {\n compiled.definition.delete(name);\n }\n\n if (clearedExternal) {\n compiled.external.delete(name);\n }\n\n if (clearedDefinition || clearedExternal) {\n this.#recompileSlot(type, name);\n }\n }\n\n /**\n * Re-derives the compiled-function slot from whichever origin still has an\n * entry for `name` after a clear. External wins over definition; if neither\n * has an entry, the compiled function is deleted.\n *\n * ⚑ **A READ, not a re-compile (#1649).** Calling the surviving factory here\n * runs application code inside two DESTRUCTIVE operations\n * (`completeTransition`'s post-leave cleanup and `replace()`'s\n * `clearDefinitionGuards`), which is how a guard factory reaches `dispose()` /\n * `stop()` / starts a navigation from inside a teardown it is not supposed to\n * observe (#1611 / #1626 / #1627). Each factory's compiled form is stored\n * beside it at registration, so the survivor's is already in hand and no user\n * code runs here at all — and with no second call there is nothing for a\n * `v8 ignore` block to defend against.\n */\n #recompileSlot(type: \"activate\" | \"deactivate\", name: string): void {\n const compiled = this.#getCompiledMaps(type);\n const functions =\n type === \"activate\"\n ? this.#canActivateFunctions\n : this.#canDeactivateFunctions;\n\n const effective =\n compiled.external.get(name) ?? compiled.definition.get(name);\n\n if (!effective) {\n functions.delete(name);\n\n return;\n }\n\n functions.set(name, effective);\n }\n\n /**\n * The compiled-form counterpart of {@link #getFactoryMaps} — same origin\n * split, same `deactivate`-before-`activate` reading order.\n */\n #getCompiledMaps(type: \"activate\" | \"deactivate\"): {\n definition: Map<string, GuardFn>;\n external: Map<string, GuardFn>;\n } {\n return type === \"activate\"\n ? {\n definition: this.#compiledDefinitionActivate,\n external: this.#compiledExternalActivate,\n }\n : {\n definition: this.#compiledDefinitionDeactivate,\n external: this.#compiledExternalDeactivate,\n };\n }\n\n #getFactoryMaps(type: \"activate\" | \"deactivate\"): {\n definition: Map<string, GuardFnFactory<Dependencies>>;\n external: Map<string, GuardFnFactory<Dependencies>>;\n } {\n return type === \"activate\"\n ? {\n definition: this.#definitionActivateFactories,\n external: this.#externalActivateFactories,\n }\n : {\n definition: this.#definitionDeactivateFactories,\n external: this.#externalDeactivateFactories,\n };\n }\n\n /**\n * Shared implementation for synchronous guard checks.\n * Warns if a guard returns a Promise (async guards are not supported in sync mode).\n * Catches exceptions and treats them as navigation-blocking (`false`).\n *\n * @param functions - Map of compiled guard functions to look up\n * @param name - Route name to check the guard for\n * @param toState - Target navigation state\n * @param fromState - Current state (`undefined` on initial navigation)\n * @param methodName - Public API method name for warning messages\n */\n #checkGuardSync(\n functions: Map<string, GuardFn>,\n name: string,\n toState: State,\n fromState: State | undefined,\n methodName: string,\n ): boolean {\n const guardFn = functions.get(name);\n\n if (!guardFn) {\n return true;\n }\n\n try {\n const result = guardFn(toState, fromState);\n\n if (typeof result === \"boolean\") {\n return result;\n }\n\n this.#deps.getValidator()?.lifecycle.warnAsyncGuardSync(name, methodName);\n\n return false;\n } catch (error) {\n // #959: a throwing sync guard must not vanish silently. `navigate()`\n // surfaces the same throw via `handleGuardError` → TRANSITION_ERROR; the\n // sync predicate (`canNavigateTo`) has no error channel, so core logs it\n // directly. This is an OPERATIONAL signal (the guard crashed — distinct\n // from the opt-in validator DX warnings above for which the validator is\n // the right home): the navigation is still treated as blocked (`false`).\n this.#deps.logger.warn(\n `router.${methodName}`,\n `Guard for \"${name}\" threw — treated as navigation-blocking (returned false)`,\n error,\n );\n\n return false;\n }\n }\n}\n","// packages/core/src/namespaces/RoutesNamespace/constants.ts\n\n/**\n * Default route name for the root node.\n */\nexport const DEFAULT_ROUTE_NAME = \"\";\n\n/**\n * Keys that belong to a route's structural/config surface — everything NOT in\n * this set is a plugin-defined **custom field** (e.g. lifecycle hooks,\n * `preload`, `searchSchema`), stored in `RoutesStore.routeCustomFields`.\n *\n * Single source of truth for the custom-field split, shared by route\n * registration (`add`/`replace`) and `update` so both classify patch keys\n * identically. `name`/`path`/`children` define route identity and are not\n * patchable via `update`; the remaining seven are the structural/guard config\n * and are exactly the members of `RouteConfigUpdate`.\n *\n * ⚑ **This list mirrors a TYPE, and the mirror is now enforced (#1738):** it must\n * equal the members `Route` DECLARES in `types/router.ts` — lexically, so a\n * plugin's augmentation stays a custom field — and\n * `tests/functional/route-key-authority-1738.test.ts` fails in BOTH directions,\n * a declared field missing here and a phantom key here that no field declares.\n * Being a hand-written list is why it needs that: `defaultSearch` was added to\n * `Route` by RFC-4 M2 (#1548) and never reached this set, so across 33 core releases (0.82.0, where the field shipped, to 0.92.1)\n * a core config field was stored in the plugin bag AND — because\n * `prepareCustomFields` reads the value of every key this set does not know, one\n * read after `commitRouteUpdate`'s own destructuring — a `defaultSearch` getter\n * was invoked twice on `update`, landing two different values in the two homes.\n *\n * ⚠ Membership here decides CLASSIFICATION only, not carriage: every structural\n * field is written by its own explicit branch — `registerSingleRouteHandlers` for\n * registration, and on `update` the four scalars by `commitScalarConfig`,\n * `forwardTo` by `prepareForwardTo` + its commit, the two guards by\n * `commitGuardUpdate`. Those branches are not duplicates of this set and do not\n * become redundant by a key being added here.\n *\n * ⚠ **The double read is an `update`-path fact, not a general one.** On\n * registration the split uses `Object.entries(route)`, which materialises every\n * value BEFORE the filter, so a getter there is invoked three times (once\n * discarded by the filter, then the existence check and the assignment) — for\n * `defaultParams` exactly as for `defaultSearch`. Membership buys the classification\n * on that path and no read at all; the \"user getter called once\" invariant is\n * asserted for `update` only, and closing that asymmetry is out of this issue.\n */\nexport const STANDARD_ROUTE_KEYS: ReadonlySet<string> = new Set([\n \"name\",\n \"path\",\n \"children\",\n \"canActivate\",\n \"canDeactivate\",\n \"forwardTo\",\n \"encodeParams\",\n \"decodeParams\",\n \"defaultParams\",\n \"defaultSearch\",\n]);\n","// packages/core/src/namespaces/RoutesNamespace/helpers.ts\n\nimport { assertRouteDefaultChannels } from \"../../channels\";\nimport { areParamValuesEqual } from \"../../helpers\";\nimport { putField } from \"../../utils/ingest\";\n\nimport type { RoutesStore } from \"./routesStore\";\nimport type { RouteConfig } from \"./types\";\nimport type { Matcher, RouteDefinition, RouteTree } from \"../../engine\";\nimport type {\n DefaultDependencies,\n ForwardToCallback,\n Params,\n ParamsSearch,\n SearchParams,\n Route,\n} from \"../../types\";\n\n// ⚑ Captured at module load, like every deciding read in `helpers.ts` — but this\n// one BUILDS the guarantee rather than answering about it (#2073): the registries\n// below are published by reference, so a re-pointed `Object.freeze` would remove\n// the seal instead of changing a verdict.\nconst freeze = Object.freeze;\n\n/** Captured like the deciding seven, but this one BUILDS the guarantee (#2072). */\nconst objectCreate = Object.create;\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 hasOwn = Object.hasOwn;\nconst objectKeys = Object.keys;\n\n/**\n * Creates an empty RouteConfig.\n */\nexport function createEmptyConfig(): RouteConfig {\n return {\n decoders: objectCreate(null) as Record<\n string,\n (channels: ParamsSearch) => ParamsSearch\n >,\n encoders: objectCreate(null) as Record<\n string,\n (channels: ParamsSearch) => ParamsSearch\n >,\n defaultParams: objectCreate(null) as Record<string, Params>,\n defaultSearch: objectCreate(null) as Record<string, SearchParams>,\n forwardMap: objectCreate(null) as Record<string, string>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n forwardFnMap: objectCreate(null) as Record<string, ForwardToCallback<any>>,\n };\n}\n\n/**\n * Does ANY route in this config forward? The tree-wide answer behind\n * `RoutesStore.hasAnyForward` — see that field for why one boolean is worth\n * deriving (#1595).\n *\n * Deliberately NOT a field on {@link RouteConfig}: {@link assignConfigEntries}\n * enumerates that interface's values and assumes every one is a record object,\n * so a boolean there would be copied as if it had entries.\n */\nexport function anyForwardConfigured(config: RouteConfig): boolean {\n return (\n objectKeys(config.forwardMap).length > 0 ||\n objectKeys(config.forwardFnMap).length > 0\n );\n}\n\n/**\n * Copies every {@link RouteConfig} sub-map's entries from `source` into\n * `target` (shallow per map — entries are shared by reference). Driven by\n * `Object.keys(source)` instead of one `Object.assign` per field, so a newly\n * added config sub-field is carried over automatically with nothing to forget\n * at each copy site (#965). Both configs are produced by\n * {@link createEmptyConfig}, so every key in `source` also exists on `target`,\n * and every value is a record object — the invariant this enumeration relies on.\n */\nexport function assignConfigEntries(\n target: RouteConfig,\n source: RouteConfig,\n): void {\n for (const key of objectKeys(source) as (keyof RouteConfig)[]) {\n Object.assign(target[key], source[key]);\n }\n}\n\n// ============================================================================\n// Route Tree Helpers\n// ============================================================================\n\n/**\n * {@link paramsMatch} over the keys the COMMITTED STATE actually carries.\n *\n * That bag is the route's realised parameter surface — its path slots, plus\n * whatever any `defaultParams` on the chain put there — so a key it does not\n * carry is one the CALLER invented. Such a key reaches no URL and names nothing\n * the router knows, so it decides nothing (#1978), and both arms of\n * `isActiveRoute` say so because both ask this.\n *\n * ⚠ Asking the STATE rather than a registry is what keeps that true without a\n * chain walk: a default declared on an ANCESTOR of the committed route is\n * already merged into `state.params`, while no lookup keyed by one route name\n * can see it.\n *\n * ⚠ It does NOT make an omitted key inert — the exact arm's\n * `areStatesEqual(…, true)` sweeps the route's declared slots separately, so a\n * link that leaves one out is still refused there.\n */\nexport function locationParamsMatch(source: Params, target: Params): boolean {\n for (const key in source) {\n if (!hasOwn(target, key)) {\n continue;\n }\n\n if (!areParamValuesEqual(source[key], target[key])) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * A route name for a DIAGNOSTIC, obtained without reading the caller's value.\n *\n * `isActiveRoute`'s two handlers interpolate the name into the message they log\n * before answering `false`. `name` is typed `string`, but a caller may hand core\n * an object whose `toString` is application code, so interpolating it directly\n * puts a throw INSIDE the handler that exists to absorb one — out of the\n * predicate and into the render (#1946). `typeof` invokes nothing, and the name\n * still prints wherever it is worth printing.\n *\n * ⚠ Not a validation seam. Bare core degrades and\n * `@real-router/validation-plugin` diagnoses; this only decides what a message\n * may safely say.\n */\nexport function describeRouteName(name: string): string {\n return typeof name === \"string\" ? name : \"<non-string>\";\n}\n\n/**\n * Checks if all params from source exist with same values in target.\n * Small function body allows V8 inlining.\n */\nexport function paramsMatch(source: Params, target: Params): boolean {\n for (const key in source) {\n // Provenance-tolerant per value (#1554) — `isActiveRoute` compares a caller\n // bag against the COMMITTED state, whose values may have come from the URL\n // parser (`?tab=2` → `2`) while the caller wrote strings. Both of that\n // predicate's arms reach this function, and its exact arm reaches\n // `areParamValuesEqual` through `areStatesEqual` as well, so one location\n // gets one answer whichever arm asks (#1978).\n if (!areParamValuesEqual(source[key], target[key])) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Sanitizes a route by keeping only essential properties.\n */\nexport function sanitizeRoute<Dependencies extends DefaultDependencies>(\n route: Route<Dependencies>,\n): RouteDefinition {\n const sanitized: RouteDefinition = {\n name: route.name,\n path: route.path,\n };\n\n if (route.children) {\n // ⚑ `putField`, not `sanitized.children = …` (#1852 / #2139). `sanitized` is\n // an object literal with `name` and `path` on it and nothing else, so this\n // key has no own slot to overwrite and a plain assignment walks the\n // prototype. Measured under an ambient `children` accessor: with a setter\n // the children went into it and `sanitized` never got them; with a getter\n // alone the assignment threw in strict mode and a legal registration became\n // an error.\n putField(\n sanitized,\n \"children\",\n route.children.map((child) => sanitizeRoute(child)),\n );\n }\n\n return sanitized;\n}\n\n/**\n * Collects the FULL dotted names of `node` and every real descendant of it,\n * into `into`. `fullName` is the node's own full name — children extend it,\n * because a nested definition carries a BARE name.\n */\nfunction collectDefinitionNames(\n node: RouteDefinition,\n fullName: string,\n into: Set<string>,\n): void {\n into.add(fullName);\n\n if (node.children) {\n for (const child of node.children) {\n collectDefinitionNames(child, `${fullName}.${child.name}`, into);\n }\n }\n}\n\n/**\n * Removes `routeName` from `definitions` and reports the full dotted names it\n * ACTUALLY took with it — the spliced node plus its real `children`, and\n * nothing else. `undefined` when the name is not a definition at all.\n *\n * ⚑ The returned set is the AUTHORITY on \"what this removal removed\" (#1757),\n * and it exists because the alternative — testing a name for the string prefix\n * `${routeName}.` — answers a strictly WIDER question. Core accepts a dotted\n * LEAF name, so `{ name: \"x.y\" }` declared beside `{ name: \"x\" }` is a\n * standalone top-level node: the splice never touches it, yet the prefix test\n * claims it. Four sites asked the string and were wrong on exactly that shape —\n * the config/lifecycle purge (a fail-open: the survivor's blocking\n * `canActivate` was unregistered), the `forwardMap` value sweep, the\n * `TREE_CHANGED` payload, and the active-route refusal.\n *\n * Structural rather than lexical, so it is right for BOTH spellings by\n * construction: a real child is inside the spliced node and is collected; a\n * flat namesake is a sibling in the array and is not.\n */\nexport function spliceSubtree(\n definitions: RouteDefinition[],\n routeName: string,\n parentPrefix = \"\",\n): Set<string> | undefined {\n for (let i = 0; i < definitions.length; i++) {\n const route = definitions[i];\n const fullName = parentPrefix\n ? `${parentPrefix}.${route.name}`\n : route.name;\n\n if (fullName === routeName) {\n definitions.splice(i, 1);\n\n const removed = new Set<string>();\n\n collectDefinitionNames(route, fullName, removed);\n\n return removed;\n }\n\n if (route.children && routeName.startsWith(`${fullName}.`)) {\n const removed = spliceSubtree(route.children, routeName, fullName);\n\n if (removed) {\n return removed;\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Clears configuration entries that match the predicate.\n */\nexport function clearConfigEntries<T>(\n config: Record<string, T>,\n matcher: (key: string) => boolean,\n): void {\n for (const key of objectKeys(config)) {\n if (matcher(key)) {\n delete config[key];\n }\n }\n}\n\n/**\n * Used by matchPath() when trailingSlash is \"preserve\": the matcher's\n * buildPath() with an unset trailingSlash mode strips trailing slashes,\n * but \"preserve\" means the source path's trailing-slash choice wins.\n * If the source had a trailing slash, re-attach it to the rewritten path.\n * The reverse case (rewritten has trailing, source does not) is not\n * reachable with the current matcher — it never adds a trailing slash\n * with undefined mode.\n */\nexport function matchSourceTrailingSlash(\n sourcePath: string,\n rewrittenPath: string,\n): string {\n const queryIndex = rewrittenPath.search(/[?#]/);\n const pathPart =\n queryIndex === -1 ? rewrittenPath : rewrittenPath.slice(0, queryIndex);\n\n // Stryker disable next-line LogicalOperator: equivalent — buildPath strips trailing slashes, so the rewritten path never ends with \"/\" unless it IS \"/\" (already caught by the `=== \"/\"` operand). `endsWith(\"/\")` is unreachable-true, so `||` ≡ `&&`.\n if (pathPart === \"/\" || pathPart.endsWith(\"/\")) {\n return rewrittenPath;\n }\n\n const sourceQueryIndex = sourcePath.search(/[?#]/);\n const sourcePathPart =\n sourceQueryIndex === -1\n ? sourcePath\n : sourcePath.slice(0, sourceQueryIndex);\n\n if (!(sourcePathPart.length > 1 && sourcePathPart.endsWith(\"/\"))) {\n return rewrittenPath;\n }\n\n const querySuffix = queryIndex === -1 ? \"\" : rewrittenPath.slice(queryIndex);\n\n return `${pathPart}/${querySuffix}`;\n}\n\n// =============================================================================\n// The query-channel registry — ONE derivation, shared by every reader (#1556)\n// =============================================================================\n\n/** Flattens the path-slot names declared across a route's matched segments. */\nexport function collectUrlParamsArray(\n segments: readonly RouteTree[],\n): string[] {\n const params: string[] = [];\n\n for (const segment of segments) {\n for (const param of segment.paramMeta.urlParams) {\n params.push(param);\n }\n }\n\n return params;\n}\n\n/**\n * The route's PATH slot names, cached per route name.\n *\n * Store-level rather than a namespace method so the config-time channel check\n * reads the SAME registry the URL build prints from. A second derivation is\n * exactly the drift #1556 removed.\n */\nexport function urlParamsFor(\n matcher: Matcher,\n name: string,\n cache: Map<string, readonly string[]>,\n): readonly string[] {\n const cached = cache.get(name);\n\n // Stryker disable next-line BlockStatement: equivalent — cache short-circuit; emptying the early-return recomputes the identical value (deterministic per route name) and re-caches it. (ConditionalExpression stays live: `→true` returns undefined on a cache miss = killed.)\n if (cached !== undefined) {\n return cached;\n }\n\n const segments = matcher.getSegmentsByName(name);\n const result = freeze(\n segments ? collectUrlParamsArray(segments as readonly RouteTree[]) : [],\n );\n\n cache.set(name, result);\n\n return result;\n}\n\n/** Store-bound {@link urlParamsFor}. */\nexport function urlParamsOf<Dependencies extends DefaultDependencies>(\n store: RoutesStore<Dependencies>,\n name: string,\n): readonly string[] {\n return urlParamsFor(store.matcher, name, store.urlParamsCache);\n}\n\n/**\n * The route's declared `?query` names minus its path slots — the registry that\n * both classifies and PRINTS (#1556), with the `/items/:id?id` carve-out\n * (#843 / #1549) falling out of the subtraction rather than being re-decided.\n */\nexport function queryParamsFor(\n matcher: Matcher,\n name: string,\n urlCache: Map<string, readonly string[]>,\n queryCache: Map<string, readonly string[]>,\n): readonly string[] {\n const cached = queryCache.get(name);\n\n // Stryker disable next-line BlockStatement: equivalent — cache short-circuit; emptying the early-return recomputes the identical value (deterministic per route name) and re-caches it. (ConditionalExpression stays live: `→true` returns undefined on a cache miss = killed.)\n if (cached !== undefined) {\n return cached;\n }\n\n const declared = matcher.getDeclaredQueryParams(name);\n let result: readonly string[] = [];\n\n if (declared) {\n const urlParams = urlParamsFor(matcher, name, urlCache);\n\n // ⚑ `filter` performs ArraySpeciesCreate on its RECEIVER, so\n // `declared.constructor` decides the class of the array cached here and\n // re-read on every navigation — measured, with a subclass planted on that\n // slot `getQueryParams` answered with an instance of it. What closes that is\n // the freeze on `declaredQueryParams` at its SOURCE, not the build form —\n // a species-free loop here earns nothing while the source is sealed, which\n // is why this stays the plain `filter` (#2137).\n result = declared.filter((param) => !urlParams.includes(param));\n }\n\n const frozen = freeze(result);\n\n queryCache.set(name, frozen);\n\n return frozen;\n}\n\n/** Store-bound {@link queryParamsFor}. */\nexport function queryParamsOf<Dependencies extends DefaultDependencies>(\n store: RoutesStore<Dependencies>,\n name: string,\n): readonly string[] {\n return queryParamsFor(\n store.matcher,\n name,\n store.urlParamsCache,\n store.queryParamsCache,\n );\n}\n\n/**\n * Store-layer adapter for {@link assertRouteDefaultChannels}: supplies the\n * declared-query accessor the pure rule takes as data.\n *\n * The caches are LOCAL to the attempt, not the store's, and that is the whole\n * reason this adapter exists rather than the four entry points each building the\n * closure. Every caller runs on PREPARED artifacts, before any swap: validating\n * against the store's caches would answer about a tree the rejected batch has\n * not installed — and the guard would then be checking the wrong config while\n * claiming to protect the right one.\n */\nexport function assertRouteDefaultChannelsFor(\n matcher: Matcher,\n config: RouteConfig,\n method: string,\n): void {\n const urlCache = new Map<string, readonly string[]>();\n const queryCache = new Map<string, readonly string[]>();\n\n assertRouteDefaultChannels(\n config.defaultParams,\n (name) => queryParamsFor(matcher, name, urlCache, queryCache),\n method,\n );\n}\n","// packages/core/src/namespaces/RoutesNamespace/forwardChain.ts\n\n/**\n * The `forwardTo` chain a name resolves to, or the name itself.\n *\n * ⚑ The entry name is coerced ONCE, and that single `String()` is the fix for\n * #1882 rather than a formality. This is a root export taking a name it uses as\n * a PROPERTY KEY, and the walk asked the same question twice — `while\n * (forwardMap[current])` tested one coercion and `const next =\n * forwardMap[current]` indexed another. For a name that answers differently\n * between the two (an accessor-backed or `toString`-backed value, which is\n * supported input), the second read indexed a route the first never named:\n * measured on a map `{ alias: \"users\", other: \"home\" }`, a name answering\n * `\"alias\"` then `\"other\"` resolved to **`home`** — the forward target of a\n * route the caller never asked about.\n *\n * ⚠ NOT a type gate, and the distinction is load-bearing. #1881 gated three\n * neighbouring doors and #1891 reverted them; `ARCHITECTURE.md` \"Route-Name Type\n * Gates\" admits a gate only where a STABLE non-string already does damage, and a\n * stable one here answers exactly what its `toString` names — which is what this\n * coercion preserves. It is also the one door of the family with NO validator\n * seam: a free function has nothing for `@real-router/validation-plugin` to hook,\n * and that plugin is itself a consumer, so \"bare core degrades, the opt-in\n * validator diagnoses\" has nowhere to live here.\n *\n * ⚑ It also makes the declared `: string` return true. Uncoerced, a walk with no\n * entry in the map hands the caller's own OBJECT straight back.\n */\nexport function resolveForwardChain(\n startRoute: string,\n forwardMap: Record<string, string>,\n maxDepth = 100,\n): string {\n // ⚠ Both the `unknown` hop and the disable below are load-bearing, and neither\n // is style. `startRoute` is DECLARED `string`, and on the strength of that\n // declaration TWO autofixable rules offer to delete this coercion:\n // `@typescript-eslint/no-unnecessary-type-conversion` (which the `unknown`\n // silences) and `unicorn/no-useless-coercion` (which it does not). Running\n // `lint --fix` deleted it twice while this fix was being written — the second\n // time into `const start = raw`, which does not even type-check — silently\n // restoring #1882 both times. The declared type is the CONTRACT; it is not a\n // runtime guarantee, and trusting it is precisely what this function did wrong.\n const raw: unknown = startRoute;\n // eslint-disable-next-line unicorn/no-useless-coercion -- the declared `string` is a contract, not a runtime guarantee (#1882)\n const start = String(raw);\n const visited = new Set<string>();\n const chain: string[] = [start];\n let current = start;\n\n for (;;) {\n // ⚑ ONE map read and ONE coercion per hop, and both halves are the same\n // defect the entry read had (#1882). `while (forwardMap[current])` tested one\n // value and `const next = forwardMap[current]` took another — two questions\n // where the caller sees one — and `current = next` then handed a raw VALUE\n // back to the top, where it was read as a key twice more. The MAP's declared\n // `Record<string, string>` has exactly the status `startRoute: string` has:\n // a contract, not a runtime guarantee. Measured on the export before this,\n // entry name a plain `\"a\"`, map `{ a: bag→\"b\" then \"c\", b: \"usersB\",\n // c: \"usersC\" }` → **`usersC`**, the forward target of a route no read of\n // the map ever named on its first answer.\n //\n // ⚠ Core itself cannot produce that map: registration branches on\n // `typeof route.forwardTo === \"string\"` and sends everything else to\n // `forwardFnMap`, so `config.forwardMap`'s values are strings by\n // construction, and core's one caller (`refreshForwardMap`) walks the map's\n // own keys. This closes the EXPORT's contract, which is the whole reason\n // #1882 was a fix rather than a formality.\n const rawHop: unknown = forwardMap[current];\n\n if (!rawHop) {\n break;\n }\n\n // ⚠ The disable is the SAME one the entry read carries twenty lines up, for\n // the same reason and against the same rule. `lint --fix` deleted this\n // coercion the moment it was written — a THIRD time in this family — turning\n // it into `const next = rawHop`, which reds the terminal-hop cell in\n // `read-count-authority.test.ts` and restores the object-return arm.\n // The second rule is `no-base-to-string`, which fires because the falsy\n // check narrows `unknown` to `{}` — i.e. it warns that a plain object\n // stringifies to `\"[object Object]\"`. It does, and that is precisely what\n // the implicit `ToPropertyKey` behind `forwardMap[current]` produced before\n // this line existed; making it explicit changes nothing except that it\n // happens ONCE. The falsy check must stay above the coercion: `String()` of\n // an absent entry is `\"undefined\"`, which is truthy and would walk forever.\n // eslint-disable-next-line unicorn/no-useless-coercion, @typescript-eslint/no-base-to-string -- the map's declared `Record<string, string>` is a contract, not a runtime guarantee (#1882)\n const next = String(rawHop);\n\n if (visited.has(next)) {\n const cycleStart = chain.indexOf(next);\n const cycle = [...chain.slice(cycleStart), next];\n\n throw new Error(`Circular forwardTo: ${cycle.join(\" → \")}`);\n }\n\n visited.add(current);\n chain.push(next);\n current = next;\n\n if (chain.length > maxDepth) {\n throw new Error(\n `forwardTo chain exceeds maximum depth (${maxDepth}): ${chain.join(\" → \")}`,\n );\n }\n }\n\n return current;\n}\n","// packages/core/src/namespaces/RoutesNamespace/routesStore.ts\n\nimport { DEFAULT_ROUTE_NAME, STANDARD_ROUTE_KEYS } from \"./constants\";\nimport { resolveForwardChain } from \"./forwardChain\";\nimport {\n anyForwardConfigured,\n assertRouteDefaultChannelsFor,\n assignConfigEntries,\n createEmptyConfig,\n queryParamsOf,\n sanitizeRoute,\n} from \"./helpers\";\nimport { assertChannelCorrect } from \"../../channels\";\nimport {\n assertNoDottedRouteName,\n assertRouteNameNotEmpty,\n createMatcher,\n createRouteTree,\n routeTreeToDefinitions,\n} from \"../../engine\";\nimport { assertRouteNameIsString } from \"../../guards\";\nimport { copyOwnData } from \"../../helpers\";\nimport { putField } from \"../../utils/ingest\";\n\nimport type { RouteConfig, RoutesDependencies } from \"./types\";\nimport type {\n CreateMatcherOptions,\n Matcher,\n RouteDefinition,\n RouteTree,\n} from \"../../engine\";\nimport type {\n DefaultDependencies,\n ForwardToCallback,\n GuardFn,\n Params,\n ParamsSearch,\n SearchParams,\n RouteConfigUpdate,\n RouterLogger,\n GuardFnFactory,\n Route,\n} from \"../../types\";\nimport type { RouteLifecycleNamespace } from \"../RouteLifecycleNamespace\";\n\n/** Captured like the deciding seven, but this one BUILDS the guarantee (#2072). */\n/**\n * Captured at module load: `freeze`.\n *\n * ⚑ A guard is only as strong as the intrinsic it reads WHEN IT RUNS, and an\n * application can re-point `Object.freeze` after boot — a freeze that reads the\n * re-pointed one silently does nothing. Same capture, and the same reason, as\n * `OptionsNamespace`.\n *\n * ⚠ It does NOT close a shim evaluated BEFORE this module, the ordinary\n * polyfill order.\n */\nconst freeze = Object.freeze;\n\nconst objectCreate = Object.create;\n\nconst objectEntries = Object.entries;\nconst objectKeys = Object.keys;\n\n/**\n * Intrinsics captured at module load (#1971).\n *\n * ⚑ Two of the three `__proto__` write primitives in core were captured a\n * commit earlier, and `defineProperty` — whose own comment names both of them\n * as the mirrors it follows — was left reading the global. Measured with a naive\n * polyfill after boot: the record's prototype is replaced, the field vanishes\n * from own keys, and a key nobody set reads back through `getRouteConfig`,\n * which plugins index by key. #1788 verbatim, while both captured mirrors held\n * under the identical tamper.\n *\n * ⚠ It does NOT close a shim evaluated BEFORE this module — see the sibling\n * headers in `src/engine`.\n */\nconst defineProperty = Object.defineProperty;\nconst fromEntries = Object.fromEntries;\n// =============================================================================\n// Interfaces\n// =============================================================================\n\nexport interface RoutesStore<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> {\n /**\n * DERIVED VIEW, not stored state: reconstructed from `tree` on every access\n * via `routeTreeToDefinitions` (the lossless inverse cloneRouter already\n * relies on — the `~` absolute marker is restored, child order is the\n * definition order). The tree is the single source of truth, so a third\n * retained copy of the route table (~30 B/route) is not kept. Every reader\n * is a cold CRUD/plugin-registration path; the derive is O(N).\n *\n * The returned array is a FRESH snapshot each time — mutating it never\n * affects the store (pass an explicitly-mutated snapshot to\n * `commitTreeChanges` instead, as `remove` does).\n */\n readonly definitions: RouteDefinition[];\n readonly config: RouteConfig;\n tree: RouteTree;\n matcher: Matcher;\n /**\n * Per-route-name cache of URL (path) param names, derived from `matcher` and\n * read by `RoutesNamespace.getUrlParams` (powers `areStatesEqual` /\n * `isActiveRoute`). Cleared on every `matcher` rebuild so comparisons never\n * stay frozen to a route's pre-mutation param shape (#723).\n */\n readonly urlParamsCache: Map<string, readonly string[]>;\n /**\n * Per-route-name cache of declared query param names (`?a&b` across the\n * route's segments), read by `RoutesNamespace.getQueryParams` — THE registry\n * every channel mechanism classifies through and the URL build prints from\n * (#1556). Same lifecycle as `urlParamsCache`: cleared on every `matcher`\n * rebuild.\n */\n readonly queryParamsCache: Map<string, readonly string[]>;\n resolvedForwardMap: Record<string, string>;\n\n /**\n * Is `replace()`'s revalidation window open (#1758 / #1759)?\n *\n * ⚑ Application code runs inside it — the route's `decodeParams`, invoked by\n * the revalidating `matchPath`, and the new route's activation guards — while\n * the router holds a state it has not yet revalidated. Reached from there,\n * route-CRUD committed a bag the route could no longer build and a navigation\n * left the committed state on a route the batch had dropped. Both doors\n * consult this; `revalidation-window-ban-1758-1759.test.ts` owns the cells.\n */\n revalidating: boolean;\n\n /**\n * Does ANY route in the tree forward? Read by `isActiveRoute` before its\n * `forwardTo` arm's per-route gate, and worth its own field for a reason that\n * is measurable rather than aesthetic (#1595): the two maps behind that gate\n * are `Object.create(null)` dictionaries, which V8 keeps in dictionary mode,\n * and two lookups on them cost ~14 ns — paid by every route in the tree for a\n * feature only forwarding routes use, on every `<Link>` render across six\n * adapters. A tree with no `forwardTo` at all — the common case — answers with\n * one boolean load instead.\n *\n * ⚠ Maintained ONLY through {@link adoptForwardState}, together with\n * `resolvedForwardMap`. The two are views of the same config and a stale\n * `false` here silently switches the arm OFF, which is a correctness bug\n * wearing a performance change's clothes: a `<Link>` to a forwarding route\n * would render inactive again (the defect #1573 shipped the arm to fix).\n * Pinned in `tests/functional/routes/isActiveRoute.test.ts`, describe\n * `\"forwardTo arm survives route-CRUD (#1595)\"` — and the generative pin in\n * `tests/property/cloneRouter.properties.ts`. ⚠ That describe name is\n * narrower than its contents: `cloneRouter` writes forward config without\n * being route-CRUD (#1800), so a clone case lives there too.\n */\n hasAnyForward: boolean;\n routeCustomFields: Record<string, Record<string, unknown>>;\n rootPath: string;\n readonly matcherOptions: CreateMatcherOptions | undefined;\n depsStore: RoutesDependencies<Dependencies> | undefined;\n lifecycleNamespace: RouteLifecycleNamespace<Dependencies> | undefined;\n readonly pendingCanActivate: Map<string, GuardFnFactory<Dependencies>>;\n readonly pendingCanDeactivate: Map<string, GuardFnFactory<Dependencies>>;\n}\n\n// =============================================================================\n// Tree operations\n// =============================================================================\n\nfunction rebuildTree(\n definitions: readonly RouteDefinition[],\n rootPath: string,\n matcherOptions: CreateMatcherOptions | undefined,\n): { tree: RouteTree; matcher: Matcher } {\n const tree = createRouteTree(DEFAULT_ROUTE_NAME, rootPath, definitions);\n const matcher = createMatcher(matcherOptions);\n\n matcher.registerTree(tree);\n\n return { tree, matcher };\n}\n\n/**\n * Rebuilds tree+matcher in place from `definitions` (defaults to the current\n * tree's own derived definitions — the same-table case, e.g. a rootPath\n * change).\n */\nexport function rebuildTreeInPlace<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n store: RoutesStore<Dependencies>,\n definitions: readonly RouteDefinition[] = store.definitions,\n): void {\n const result = rebuildTree(definitions, store.rootPath, store.matcherOptions);\n\n store.tree = result.tree;\n store.matcher = result.matcher;\n store.urlParamsCache.clear();\n store.queryParamsCache.clear();\n}\n\n/**\n * Prepare-then-commit root-path change.\n *\n * A root `?`-declaration declares the name on EVERY route at once, so a\n * `defaultParams` that was legal a moment ago can stop being legal without any\n * route changing — the one mutation where re-checking the WHOLE config is not\n * redundant. Built into locals first so a rejected root path leaves the store\n * exactly as it was, matching the atomicity `add` / `replace` promise.\n */\nexport function applyRootPath<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(store: RoutesStore<Dependencies>, newRootPath: string): void {\n const prepared = rebuildTree(\n store.definitions,\n newRootPath,\n store.matcherOptions,\n );\n\n assertRouteDefaultChannelsFor(prepared.matcher, store.config, \"setRootPath\");\n\n store.rootPath = newRootPath;\n store.tree = prepared.tree;\n store.matcher = prepared.matcher;\n store.urlParamsCache.clear();\n store.queryParamsCache.clear();\n}\n\nexport function commitTreeChanges<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n store: RoutesStore<Dependencies>,\n definitions: readonly RouteDefinition[],\n): void {\n rebuildTreeInPlace(store, definitions);\n adoptForwardState(store, refreshForwardMap(store.config));\n}\n\n// =============================================================================\n// Store reset\n// =============================================================================\n\n/**\n * Clears all routes and resets config.\n * Does NOT clear lifecycle handlers or state — caller handles that.\n */\nexport function resetStore<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(store: RoutesStore<Dependencies>): void {\n clearRouteData(store);\n rebuildTreeInPlace(store, []);\n}\n\n/**\n * Clears route data without rebuilding the tree.\n * Used by replace() to avoid double rebuild (clearRouteData + commitTreeChanges).\n * `definitions` needs no clearing — it is derived from the tree, which the\n * caller rebuilds (resetStore → empty, replace → the new artifacts).\n */\nexport function clearRouteData<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(store: RoutesStore<Dependencies>): void {\n Object.assign(store.config, createEmptyConfig());\n\n adoptForwardState(store, objectCreate(null) as Record<string, string>);\n store.routeCustomFields = objectCreate(null) as Record<\n string,\n Record<string, unknown>\n >;\n}\n\n// =============================================================================\n// Forward map\n// =============================================================================\n\n/**\n * The ONE way `resolvedForwardMap` and `hasAnyForward` move (#1595). They are two\n * views of the same forward config, so every site that re-derives one derives the\n * other here — a site that assigned only the map would leave a stale `false`\n * behind, and `isActiveRoute` would stop consulting its `forwardTo` arm.\n *\n * ⚠ EXPORTED for `cloneRouter` (#1800), which is the one writer of forward\n * config outside this module. It assigned only the map, and the stale `false`\n * this docstring predicts is exactly what shipped: every SSR clone answered\n * `isActiveRoute` = `false` for every forwarding route. Keep new writers going\n * through here rather than deriving the flag themselves — the field's own\n * comment says \"maintained ONLY through\" this function, and a third writer is\n * how the next one gets missed.\n *\n * ⚠ `resolved` is ASSIGNED, not merged. A caller that must keep its store's own\n * map object (`cloneRouter` does — sharing the source's would alias the two\n * stores) passes `Object.assign(store.resolvedForwardMap, incoming)`, which\n * returns that same object.\n */\nexport function adoptForwardState<Dependencies extends DefaultDependencies>(\n store: RoutesStore<Dependencies>,\n resolved: Record<string, string>,\n): void {\n store.resolvedForwardMap = resolved;\n store.hasAnyForward = anyForwardConfigured(store.config);\n}\n\nexport function refreshForwardMap(config: RouteConfig): Record<string, string> {\n const map = objectCreate(null) as Record<string, string>;\n\n for (const fromRoute of objectKeys(config.forwardMap)) {\n map[fromRoute] = resolveForwardChain(fromRoute, config.forwardMap);\n }\n\n return map;\n}\n\n// =============================================================================\n// Route handler registration\n// =============================================================================\n\n/**\n * Throws if `forwardTo` is an async function (native or transpiled). Async\n * forwardTo callbacks break the synchronous matchPath/buildPath contract.\n * Runs inside `registerForwardTo` (the add/replace build path, before any store\n * mutation) AND inside `getRoutesApi`'s `updateForwardTo` (the update path), so\n * `update(name, { forwardTo: async })` is rejected at registration with the same\n * actionable error instead of deferring a generic TypeError to navigation (#967).\n */\nexport function assertForwardToNotAsync(\n forwardTo: unknown,\n fullName: string,\n): void {\n if (typeof forwardTo !== \"function\") {\n return;\n }\n\n const isNativeAsync =\n (forwardTo as { constructor: { name: string } }).constructor.name ===\n \"AsyncFunction\";\n const isTranspiledAsync = (forwardTo as { toString: () => string })\n .toString()\n .includes(\"__awaiter\");\n\n if (isNativeAsync || isTranspiledAsync) {\n throw new TypeError(\n `forwardTo callback cannot be async for route \"${fullName}\". ` +\n `Async functions break matchPath/buildPath.`,\n );\n }\n}\n\nfunction registerForwardTo<Dependencies extends DefaultDependencies>(\n route: Route<Dependencies>,\n fullName: string,\n config: RouteConfig,\n logger: RouterLogger,\n): void {\n if (route.canActivate) {\n /* v8 ignore next -- @preserve: edge case, both string and function tested separately */\n const forwardTarget =\n typeof route.forwardTo === \"string\" ? route.forwardTo : \"[dynamic]\";\n\n logger.warn(\n \"real-router\",\n `Route \"${fullName}\" has both forwardTo and canActivate. ` +\n `canActivate will be ignored because forwardTo creates a redirect (industry standard). ` +\n `Move canActivate to the target route \"${forwardTarget}\".`,\n );\n }\n\n if (route.canDeactivate) {\n /* v8 ignore next -- @preserve: edge case, both string and function tested separately */\n const forwardTarget =\n typeof route.forwardTo === \"string\" ? route.forwardTo : \"[dynamic]\";\n\n logger.warn(\n \"real-router\",\n `Route \"${fullName}\" has both forwardTo and canDeactivate. ` +\n `canDeactivate will be ignored because forwardTo creates a redirect (industry standard). ` +\n `Move canDeactivate to the target route \"${forwardTarget}\".`,\n );\n }\n\n assertForwardToNotAsync(route.forwardTo, fullName);\n\n // forwardTo is guaranteed to exist at this point\n if (typeof route.forwardTo === \"string\") {\n config.forwardMap[fullName] = route.forwardTo;\n } else {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n config.forwardFnMap[fullName] = route.forwardTo!;\n }\n}\n\nfunction registerSingleRouteHandlers<Dependencies extends DefaultDependencies>(\n route: Route<Dependencies>,\n fullName: string,\n config: RouteConfig,\n routeCustomFields: Record<string, Record<string, unknown>>,\n pendingCanActivate: Map<string, GuardFnFactory<Dependencies>>,\n pendingCanDeactivate: Map<string, GuardFnFactory<Dependencies>>,\n logger: RouterLogger,\n): void {\n const customFields = fromEntries(\n objectEntries(route).filter(([key]) => !STANDARD_ROUTE_KEYS.has(key)),\n );\n\n if (objectKeys(customFields).length > 0) {\n // ⚑ FROZEN, not copied (#2172). `getRouteConfig` hands this record straight\n // out to plugins, so an unfrozen one is core's route store with a public\n // write door: a plugin memoising onto the record it just read\n // (`cfg.__compiled ??= compile(...)`) lands in the store, and under SSR in\n // every per-request clone.\n //\n // ⚠ Frozen rather than copied because the record is CORE's — `fromEntries`\n // above minted it — so there is no caller object to avoid freezing, and the\n // door is hot: `preload-plugin` calls it per navigation and\n // `search-schema-plugin` per route resolution. A copy would allocate on\n // every one of those to fix a write nobody legitimately makes.\n //\n // ⚠ ONE level, and the boundary is deliberate. The VALUES are the caller's\n // arbitrary application data — schemas, factories, class instances — and\n // plugins key caches on their identity, so copying them would break\n // `searchSchema` memoisation to close a hole nothing reported.\n routeCustomFields[fullName] = freeze(customFields);\n }\n\n // Guards are collected here and registered into the lifecycle later — by\n // `adoptRouteArtifacts` (add/replace) or `RoutesNamespace.flushPendingGuards`\n // (initial routes, the final step of the Router constructor — #1331) — so\n // the build stays a pure, side-effect-free preparation step.\n if (route.canActivate) {\n pendingCanActivate.set(fullName, route.canActivate);\n }\n\n if (route.canDeactivate) {\n pendingCanDeactivate.set(fullName, route.canDeactivate);\n }\n\n if (route.forwardTo) {\n registerForwardTo(route, fullName, config, logger);\n }\n\n if (route.decodeParams) {\n const decode = route.decodeParams;\n\n config.decoders[fullName] = (channels: ParamsSearch): ParamsSearch =>\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime fallback if a user-provided decoder violates its `{ params, search }` return type\n decode(channels) ?? channels;\n }\n\n if (route.encodeParams) {\n const encode = route.encodeParams;\n\n config.encoders[fullName] = (channels: ParamsSearch): ParamsSearch =>\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime fallback if a user-provided encoder violates its `{ params, search }` return type\n encode(channels) ?? channels;\n }\n\n // ⚑ ADOPTED, not aliased (#2172). Three doors hand this value straight back\n // out — `getRoutesApi(r).get(name)`, the `subscribeChanges` payload, and the\n // route record — so an aliased slot puts the caller's own literal behind all\n // three, and a write there is a write to live routing. The ordinary path that\n // reaches it is read-modify-write: `route.defaultParams.locale = …;\n // routes.update(...)`, where the router moves before `update` validates.\n //\n // ⚠ Registration time, not read time. Copying at the handout would allocate\n // per `get()` call and leave the STORE aliased, so a second reader — the\n // payload — still reaches what the first one guarded.\n if (route.defaultParams) {\n config.defaultParams[fullName] = copyOwnData(\n \"defaultParams\",\n route.defaultParams,\n );\n }\n\n if (route.defaultSearch) {\n config.defaultSearch[fullName] = copyOwnData(\n \"defaultSearch\",\n route.defaultSearch,\n );\n }\n}\n\nfunction registerAllRouteHandlers<Dependencies extends DefaultDependencies>(\n routes: readonly Route<Dependencies>[],\n config: RouteConfig,\n routeCustomFields: Record<string, Record<string, unknown>>,\n pendingCanActivate: Map<string, GuardFnFactory<Dependencies>>,\n pendingCanDeactivate: Map<string, GuardFnFactory<Dependencies>>,\n logger: RouterLogger,\n parentName = \"\",\n): void {\n for (const route of routes) {\n const fullName = parentName ? `${parentName}.${route.name}` : route.name;\n\n registerSingleRouteHandlers(\n route,\n fullName,\n config,\n routeCustomFields,\n pendingCanActivate,\n pendingCanDeactivate,\n logger,\n );\n\n if (route.children) {\n registerAllRouteHandlers(\n route.children,\n config,\n routeCustomFields,\n pendingCanActivate,\n pendingCanDeactivate,\n logger,\n fullName,\n );\n }\n }\n}\n\n// =============================================================================\n// Prepare-then-commit (issue #698)\n//\n// add()/replace() build the complete new store state into LOCAL structures, and\n// only swap it into the store once every core-level error has surfaced from the\n// build itself (async/circular forwardTo throw in registerAllRouteHandlers /\n// refreshForwardMap; invalid path constraint throws in rebuildTree). The store\n// is mutated only by `adoptRouteArtifacts`, which compiles every prepared guard\n// factory BEFORE the swap (#956): a factory that throws on compile (or returns a\n// non-function) aborts there, with the store still untouched. So all error\n// classes — core-level build errors AND malformed guard factories — surface\n// before any mutation, leaving the existing routes intact (full atomicity). The\n// silent-corruption cases route-tree never throws on (duplicate name vs an\n// existing route, a name duplicated within the batch, missing parent) are caught\n// up front by `assertAddable`.\n// =============================================================================\n\n/**\n * The fully-built, ready-to-swap result of preparing a route mutation. Holds\n * everything `adoptRouteArtifacts` assigns into the store.\n */\ninterface RouteArtifacts<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> {\n readonly config: RouteConfig;\n readonly routeCustomFields: Record<string, Record<string, unknown>>;\n readonly pendingCanActivate: Map<string, GuardFnFactory<Dependencies>>;\n readonly pendingCanDeactivate: Map<string, GuardFnFactory<Dependencies>>;\n readonly tree: RouteTree;\n readonly matcher: Matcher;\n readonly resolvedForwardMap: Record<string, string>;\n}\n\n/** Null-proto shallow clone of a RouteConfig (preserves every sub-map's contents). */\nfunction cloneConfig(config: RouteConfig): RouteConfig {\n const clone = createEmptyConfig();\n\n assignConfigEntries(clone, config);\n\n return clone;\n}\n\n/**\n * Returns a new definitions array with `added` inserted, without mutating the\n * input. For a top-level add the existing definitions are shallow-copied and\n * `added` appended. For a parented add the spine down to the parent is cloned\n * (siblings/other branches are shared by reference) and `added` appended to the\n * parent's children. Caller guarantees the parent path exists (see assertAddable).\n */\nfunction insertAddedDefinitions(\n definitions: readonly RouteDefinition[],\n added: RouteDefinition[],\n parentSegments: readonly string[],\n): RouteDefinition[] {\n if (parentSegments.length === 0) {\n return [...definitions, ...added];\n }\n\n const [head, ...rest] = parentSegments;\n\n return definitions.map((def) => {\n if (def.name !== head) {\n return def;\n }\n\n const children = def.children ?? [];\n\n return {\n ...def,\n children:\n rest.length === 0\n ? [...children, ...added]\n : insertAddedDefinitions(children, added, rest),\n };\n });\n}\n\n/** Depth-first walk yielding each route's full dotted name (no side effects). */\nfunction walkRouteNames<Dependencies extends DefaultDependencies>(\n routes: readonly Route<Dependencies>[],\n parentName: string,\n onName: (fullName: string) => void,\n): void {\n for (const route of routes) {\n const fullName = parentName ? `${parentName}.${route.name}` : route.name;\n\n onName(fullName);\n\n if (route.children) {\n walkRouteNames(route.children, fullName, onName);\n }\n }\n}\n\n/**\n * Rejects a route name duplicated WITHIN a single batch — the silent-overwrite\n * case route-tree stays last-wins on (#953 for `add`, #968 for `replace`). Walks\n * the same depth-first dotted names, but tracks them in a local Set: a name seen\n * twice in one array means the caller's second route would silently shadow the\n * first (`matchPath` for the first route's path becomes unreachable). Mirrors\n * validation-plugin's batch-dup message (route-tree `checkBatchNameDuplicate`)\n * so the no-plugin error matches the with-plugin one. `methodName` is \"addRoute\"\n * for both add and replace — the plugin reports \"addRoute\" for replace batches\n * too, so this keeps with/without-plugin parity.\n */\nexport function assertNoDuplicateNamesInBatch<\n Dependencies extends DefaultDependencies,\n>(\n routes: readonly Route<Dependencies>[],\n parentName: string,\n methodName: string,\n): void {\n const seen = new Set<string>();\n\n walkRouteNames(routes, parentName, (fullName) => {\n if (seen.has(fullName)) {\n throw new Error(\n `[router.${methodName}] Duplicate route \"${fullName}\" in batch`,\n );\n }\n\n seen.add(fullName);\n });\n}\n\nconst INTERNAL_ROUTE_PREFIX = \"@@\";\n\n/**\n * Rejects a single (bare) route name that uses the reserved \"@@\" prefix —\n * internal / system names such as UNKNOWN_ROUTE (`\"@@router/UNKNOWN_ROUTE\"`).\n * Mutating such a name would let a real URL `matchPath` to a state with\n * `name === UNKNOWN_ROUTE`, silently conflating a genuine route with \"not\n * found\". This always-on guard protected all four mutators (#238) until the\n * the validation extraction (#359) demoted it to the opt-in\n * validation-plugin; only `add` was restored (#954), so `remove`/`update`\n * regained it via this helper (#1047). Mirrors validation-plugin's\n * `throwIfInternalRoute` message so the no-plugin error matches the with-plugin\n * one.\n */\nexport function assertNoInternalRouteName(\n name: unknown,\n methodName: string,\n): asserts name is string {\n // ⚑ The type check belongs HERE and not at each door because this is the\n // first always-on name check every one of the five runs — `remove` / `update`\n // call it directly, `add` / `replace` / the constructor through\n // {@link assertNoInternalNamesInBatch} — and {@link assertNoDottedRouteName}\n // already documents that ordering as load-bearing for its own reasoning. So\n // one check covers every caller-supplied route name in core (#1896).\n //\n // ⚠ NOT a gate in the sense of `ARCHITECTURE.md` \"Route-Name Type Gates\":\n // every door here refuses a non-string with or without it, so nothing that\n // ANSWERS starts refusing. What it changes is the SHAPE of the refusal.\n // Without it the `startsWith` below is a string method on a value nothing\n // type-checked, so bare core answers `TypeError: name.startsWith is not a\n // function`, and `null` / `undefined` leak `Cannot read properties of null\n // (reading 'startsWith')` — both naming a private local rather than the\n // door.\n //\n // The wording is validation-plugin's `validateRouteName`, byte for byte,\n // including its `typeof` quirks (`typeof null === \"object\"`) — the same\n // mirroring #1047 and #1763 used, so the no-plugin error matches the\n // with-plugin one. The constructor is the door that gains the most: the\n // plugin installs through `usePlugin`, i.e. after construction, so it never\n // had a message from either layer.\n assertRouteNameIsString(name, methodName);\n\n if (name.startsWith(INTERNAL_ROUTE_PREFIX)) {\n throw new Error(\n `[router.${methodName}] Route name \"${name}\" uses the reserved \"${INTERNAL_ROUTE_PREFIX}\" prefix. Routes with this prefix are internal and cannot be modified through the public API.`,\n );\n }\n}\n\n/**\n * Batch counterpart to {@link assertNoInternalRouteName}: rejects any route in\n * the batch (recursing children) whose BARE leaf name uses the reserved \"@@\"\n * prefix (the prefix is on the leaf, not the dotted fullName). Used by `add`\n * (#954) and `replace` (#1047).\n */\nexport function assertNoInternalNamesInBatch<\n Dependencies extends DefaultDependencies,\n>(routes: readonly Route<Dependencies>[], methodName: string): void {\n for (const route of routes) {\n assertNoInternalRouteName(route.name, methodName);\n\n if (route.children) {\n assertNoInternalNamesInBatch(route.children, methodName);\n }\n }\n}\n\n/**\n * Applies {@link assertRouteNameNotEmpty} across a batch, recursing children.\n *\n * ⚑ Refusing the whole batch is what keeps the TREE right, not merely the\n * name: accepted, `{ name: \"\", children: [...] }` loses its parent and\n * re-parents the children to the root, where they answer to a name the author\n * never wrote (#1804).\n */\nexport function assertNonEmptyNamesInBatch<\n Dependencies extends DefaultDependencies,\n>(routes: readonly Route<Dependencies>[], methodName: string): void {\n for (const route of routes) {\n assertRouteNameNotEmpty(route.name, methodName);\n\n if (route.children) {\n assertNonEmptyNamesInBatch(route.children, methodName);\n }\n }\n}\n\n/**\n * Applies {@link assertNoDottedRouteName} across a batch, recursing children —\n * the check is on the BARE leaf name, so nested routes (whose names are simple\n * by construction) pass and only the dotted spelling is refused.\n *\n * ⚑ Core applies the rule on every registration door; `validateRoute` applies\n * the same predicate only for `@real-router/validation-plugin`. A dotted LEAF\n * is a standalone node whose name merely LOOKS like a path through the tree,\n * and predicates across several packages read that resemblance as ancestry —\n * `isActiveRoute` reporting a `<Link to=\"users\">` active while the address bar\n * shows another route (#1763), `remove()` purging a surviving route's config\n * and guards (#1757), and the `add` / `buildPath` halves of #1194. Each has a\n * local fix; none of them can be complete, because the resemblance is readable\n * from any name string and the places that read one are not enumerable.\n * Refusing to CREATE the shape makes every one of those predicates correct by\n * construction — including the two (`route-utils`'s exported\n * `areRoutesRelated`, `solid`'s `isRouteActive`) that take names only and have\n * no tree to consult.\n */\nexport function assertNoDottedNamesInBatch<\n Dependencies extends DefaultDependencies,\n>(routes: readonly Route<Dependencies>[], methodName: string): void {\n for (const route of routes) {\n assertNoDottedRouteName(route.name, methodName);\n\n if (route.children) {\n assertNoDottedNamesInBatch(route.children, methodName);\n }\n }\n}\n\n/**\n * Rejects two routes that share the same `path` at the same parent level WITHIN\n * a single `add` batch (#955). The matcher resolves a path collision last-wins,\n * so the earlier route stays addressable by name (`has` / `buildPath`) but is\n * unreachable by URL (`matchPath` returns the later route) — a silent shadow.\n * Paths only collide among siblings, so seen paths are tracked per parent\n * fullName. Mirrors validation-plugin's message (route-tree\n * `checkBatchPathDuplicate`) so the no-plugin error matches the with-plugin one.\n * Scoped to the batch (not the existing tree) per #955 — the in-batch case the\n * issue describes.\n */\nexport function assertNoDuplicatePathsInBatch<\n Dependencies extends DefaultDependencies,\n>(\n routes: readonly Route<Dependencies>[],\n parentName: string,\n methodName: string,\n): void {\n const seenByParent = new Map<string, Set<string>>();\n\n const walk = (\n siblings: readonly Route<Dependencies>[],\n parent: string,\n ): void => {\n for (const route of siblings) {\n const paths = seenByParent.get(parent);\n\n if (paths?.has(route.path)) {\n throw new Error(\n `[router.${methodName}] Path \"${route.path}\" is already defined`,\n );\n }\n\n if (paths) {\n paths.add(route.path);\n } else {\n seenByParent.set(parent, new Set([route.path]));\n }\n\n if (route.children) {\n walk(route.children, parent ? `${parent}.${route.name}` : route.name);\n }\n }\n };\n\n walk(routes, parentName);\n}\n\n/**\n * Up-front guard for `add` against the corruptions route-tree stays silent on: a\n * missing `parent`, a name that collides with an EXISTING route, a name\n * duplicated WITHIN the batch, a reserved \"@@\"-prefixed name (which would shadow\n * an internal/system route name), and a path duplicated among siblings WITHIN\n * the batch (any of which would otherwise be silently overwritten/shadowed).\n * Throws before any build.\n */\nexport function assertAddable<Dependencies extends DefaultDependencies>(\n store: RoutesStore<Dependencies>,\n routes: readonly Route<Dependencies>[],\n parentName: string | undefined,\n): void {\n assertNoInternalNamesInBatch(routes, \"addRoute\");\n assertNonEmptyNamesInBatch(routes, \"addRoute\");\n assertNoDottedNamesInBatch(routes, \"addRoute\");\n\n if (parentName !== undefined && !store.matcher.hasRoute(parentName)) {\n throw new Error(\n `[router.addRoute] Parent route \"${parentName}\" does not exist`,\n );\n }\n\n walkRouteNames(routes, parentName ?? \"\", (fullName) => {\n if (store.matcher.hasRoute(fullName)) {\n throw new Error(`[router.addRoute] Route \"${fullName}\" already exists`);\n }\n });\n\n assertNoDuplicateNamesInBatch(routes, parentName ?? \"\", \"addRoute\");\n assertNoDuplicatePathsInBatch(routes, parentName ?? \"\", \"addRoute\");\n}\n\n/**\n * Builds RouteArtifacts from a final definitions array and the routes whose\n * handlers (config + guards) populate `config`/`routeCustomFields`. Guards are\n * collected into the returned pending maps (depsStore is intentionally omitted\n * so nothing compiles or touches the lifecycle here). THROWS on async/circular\n * forwardTo and invalid path constraint — before the caller mutates the store.\n *\n * Takes a single args object: the positional list hit 8 parameters (S107) when\n * the per-router `logger` (#724) joined it, and named fields read better at the\n * two call sites anyway.\n */\nfunction buildArtifacts<Dependencies extends DefaultDependencies>({\n definitions,\n routesForHandlers,\n config,\n routeCustomFields,\n handlerParentName,\n rootPath,\n matcherOptions,\n logger,\n}: {\n definitions: readonly RouteDefinition[];\n routesForHandlers: readonly Route<Dependencies>[];\n config: RouteConfig;\n routeCustomFields: Record<string, Record<string, unknown>>;\n handlerParentName: string;\n rootPath: string;\n matcherOptions: CreateMatcherOptions | undefined;\n logger: RouterLogger;\n}): RouteArtifacts<Dependencies> {\n const pendingCanActivate = new Map<string, GuardFnFactory<Dependencies>>();\n const pendingCanDeactivate = new Map<string, GuardFnFactory<Dependencies>>();\n\n registerAllRouteHandlers(\n routesForHandlers,\n config,\n routeCustomFields,\n pendingCanActivate,\n pendingCanDeactivate,\n logger,\n handlerParentName,\n );\n\n const resolvedForwardMap = refreshForwardMap(config);\n const { tree, matcher } = rebuildTree(definitions, rootPath, matcherOptions);\n\n return {\n config,\n routeCustomFields,\n pendingCanActivate,\n pendingCanDeactivate,\n tree,\n matcher,\n resolvedForwardMap,\n };\n}\n\n/** Builds the merged artifacts for an incremental `add` (existing ∪ new). */\nexport function buildAddArtifacts<Dependencies extends DefaultDependencies>(\n store: RoutesStore<Dependencies>,\n routes: readonly Route<Dependencies>[],\n parentName: string | undefined,\n logger: RouterLogger,\n): RouteArtifacts<Dependencies> {\n const definitions = insertAddedDefinitions(\n store.definitions,\n routes.map((route) => sanitizeRoute(route)),\n parentName === undefined ? [] : parentName.split(\".\"),\n );\n\n return buildArtifacts({\n definitions,\n routesForHandlers: routes,\n config: cloneConfig(store.config),\n routeCustomFields: Object.assign(\n objectCreate(null) as Record<string, Record<string, unknown>>,\n store.routeCustomFields,\n ),\n handlerParentName: parentName ?? \"\",\n rootPath: store.rootPath,\n matcherOptions: store.matcherOptions,\n logger,\n });\n}\n\n/** Builds the fresh artifacts for a full `replace` (standalone new set). */\nexport function buildReplaceArtifacts<Dependencies extends DefaultDependencies>(\n routes: readonly Route<Dependencies>[],\n rootPath: string,\n matcherOptions: CreateMatcherOptions | undefined,\n logger: RouterLogger,\n): RouteArtifacts<Dependencies> {\n return buildArtifacts({\n definitions: routes.map((route) => sanitizeRoute(route)),\n routesForHandlers: routes,\n config: createEmptyConfig(),\n routeCustomFields: objectCreate(null) as Record<\n string,\n Record<string, unknown>\n >,\n handlerParentName: \"\",\n rootPath,\n matcherOptions,\n logger,\n });\n}\n\n/**\n * Compiles every pending guard factory up front, returning\n * `[name, factory, compiledFn]` triples for installation. THROWS from `compile`\n * on the first factory that throws on compile or returns a non-function — the\n * pre-swap validation that makes `adoptRouteArtifacts` atomic for malformed\n * guards (#956). Compiling here (not at install) means a factory with\n * compile-time side effects runs exactly once.\n */\nfunction compilePendingGuards<Dependencies extends DefaultDependencies>(\n pending: Map<string, GuardFnFactory<Dependencies>>,\n compile: (\n handler: GuardFnFactory<Dependencies>,\n methodName: string,\n ) => GuardFn,\n methodName: string,\n): [string, GuardFnFactory<Dependencies>, GuardFn][] {\n const compiled: [string, GuardFnFactory<Dependencies>, GuardFn][] = [];\n\n for (const [name, factory] of pending) {\n compiled.push([name, factory, compile(factory, methodName)]);\n }\n\n return compiled;\n}\n\n/** Pre-compiled guard triples for {@link adoptRouteArtifacts} install. */\nexport interface CompiledArtifactGuards<\n Dependencies extends DefaultDependencies,\n> {\n activate: [string, GuardFnFactory<Dependencies>, GuardFn][];\n deactivate: [string, GuardFnFactory<Dependencies>, GuardFn][];\n}\n\n/**\n * Compiles an artifacts' pending guard factories up front (#956), THROWING on\n * the first factory that throws on compile or returns a non-function.\n *\n * `replaceRoutes` calls this in its PREPARE phase — **before**\n * `clearDefinitionGuards()` — and hands the result to `adoptRouteArtifacts`, so\n * a compile-throw aborts with BOTH the tree AND the old definition guards intact\n * (#1193, mirroring #1046's handler-limit hoist). `add` has no clear step, so\n * `adoptRouteArtifacts` compiles inline for it.\n */\nexport function compileArtifactGuards<Dependencies extends DefaultDependencies>(\n artifacts: RouteArtifacts<Dependencies>,\n deps: RoutesDependencies<Dependencies>,\n): CompiledArtifactGuards<Dependencies> {\n return {\n activate: compilePendingGuards(\n artifacts.pendingCanActivate,\n deps.compileGuard,\n \"canActivate\",\n ),\n deactivate: compilePendingGuards(\n artifacts.pendingCanDeactivate,\n deps.compileGuard,\n \"canDeactivate\",\n ),\n };\n}\n\n/**\n * Commits prepared artifacts into the store in place. Every pending guard\n * factory is compiled BEFORE the tree/config swap (#956): a factory that throws\n * on compile (or returns a non-function) aborts here with the store untouched,\n * so `add`/`replace` are atomic for malformed guards too — not just core build\n * errors. The tree/config assignments are pure and cannot throw; the\n * pre-compiled guards are then installed without re-compiling (the factory ran\n * once, at the pre-compile above). `depsStore` is always set on a wired router,\n * which is the only path that reaches `add`/`replace`.\n *\n * ⚠ **The config-time channel check (`assertRouteDefaultChannels`) is the\n * CALLER's PREPARE step, not this function's.** Run HERE, one line before the\n * swap, it is early enough for `add` and too late for `replace`: `replace`\n * erases the old definition guards BEFORE calling this, so a batch the check\n * refuses leaves the tree intact and the guards gone — a previously guarded\n * route freely activatable. That is the #1193 fail-open shape verbatim, which is\n * why the guard COMPILE sits in the callers; the channel check sits beside it,\n * for the same reason. Keeping this function\n * throw-free is what makes its \"atomic swap\" contract true rather than nearly\n * true.\n */\nexport function adoptRouteArtifacts<Dependencies extends DefaultDependencies>(\n store: RoutesStore<Dependencies>,\n artifacts: RouteArtifacts<Dependencies>,\n precompiled?: CompiledArtifactGuards<Dependencies>,\n): void {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- depsStore is set once the router is wired; add/replace only run on a wired router (constructor-time registration uses createRoutesStore)\n const deps = store.depsStore!;\n\n // Pre-swap compile: surfaces a malformed guard factory before any mutation.\n // `replace()` pre-compiles in its PREPARE phase (BEFORE clearDefinitionGuards)\n // and passes the result here, so a compile-throw never erases the old\n // definition guards (#1193); `add` has no clear step and compiles inline.\n const { activate: compiledActivate, deactivate: compiledDeactivate } =\n precompiled ?? compileArtifactGuards(artifacts, deps);\n\n // Atomic swap — pure assignments, cannot throw. (`definitions` is derived\n // from `tree`, so swapping the tree IS the definitions swap.)\n Object.assign(store.config, artifacts.config);\n store.routeCustomFields = artifacts.routeCustomFields;\n store.tree = artifacts.tree;\n store.matcher = artifacts.matcher;\n store.urlParamsCache.clear();\n store.queryParamsCache.clear();\n adoptForwardState(store, artifacts.resolvedForwardMap);\n\n // Install pre-compiled guards — no re-compile, no throw.\n for (const [name, factory, fn] of compiledActivate) {\n deps.addActivateGuard(name, factory, fn);\n }\n\n for (const [name, factory, fn] of compiledDeactivate) {\n deps.addDeactivateGuard(name, factory, fn);\n }\n}\n\n/**\n * COMMIT core for `update()` — the field-patch counterpart to\n * {@link adoptRouteArtifacts} (add/replace) / {@link commitTreeChanges} (remove)\n * / {@link resetStore} (clear), co-located here so all four route-CRUD commit\n * cores live in one file. Stays **NO_TREE_REBUILD**: an O(1) per-field patch\n * that never rebuilds the tree/matcher (so it must NOT funnel through\n * `adoptRouteArtifacts`).\n *\n * Prepare-then-commit (#951): every throwing step runs in PREPARE, before any\n * store write, so a rejected update leaves the route's prior config fully intact\n * — an async/cyclic `forwardTo` (#967), a guard factory that throws on compile\n * (#956 seam), a throwing custom-field getter, and the #961 handler-limit\n * pre-flight (#1046). Returns the structural fields for the caller's conditional\n * TREE_CHANGED emit, computed from the single destructure here so core invokes\n * each user getter once (#797 / #952 `null`-clears-definition-only preserved).\n */\n/**\n * `update` adopts a structural field on the same terms registration does\n * (#1797). `registerSingleRouteHandlers` gates all seven — the two default\n * bags, the two codecs, the two guard factories and `forwardTo` — on\n * TRUTHINESS, and no VALID value of any of them is falsy: a forward target is a\n * non-empty route name, the default bags are objects, the codecs and factories\n * are functions. So a type-invalid falsy is ABSENCE at both doors, instead of\n * registration dropping it while `update` stored it.\n *\n * ⚠ `null` is answered FIRST and passes through untouched — it is falsy, and it\n * is the documented removal marker. `undefined` keeps meaning \"said nothing\"\n * (#1550 / #1551), which is what a non-adopted value collapses onto.\n *\n * ⚠ What this refuses to store is measured, not stylistic: `decodeParams: 0`\n * reached the decoder slot and turned `matchPath` into a thrower\n * (`decoder is not a function`), and `match()` may not throw on input — its\n * callers in the browser, hash, navigation and SSR packages do not catch.\n */\nfunction adoptable<T>(value: T): T | undefined {\n if (value === null) {\n return value;\n }\n\n return value || undefined;\n}\n\n/**\n * `copyOwnData` for a route-config slot that may also be absent or a deletion.\n *\n * ⚠ `null` and `undefined` pass through UNTOUCHED, and they are not the same\n * thing here: `undefined` means the patch did not mention the field, `null` means\n * delete the entry. `commitScalarField` one screen down distinguishes them, so\n * flattening either into a copy would turn a deletion into a write of `{}`.\n */\nfunction adoptRouteBag<T extends object>(\n field: string,\n value: T | null | undefined,\n): T | null | undefined {\n return value == null ? value : copyOwnData(field, value);\n}\n\nexport function commitRouteUpdate<Dependencies extends DefaultDependencies>(\n store: RoutesStore<Dependencies>,\n lifecycle: RouteLifecycleNamespace<Dependencies>,\n name: string,\n updates: RouteConfigUpdate<Dependencies>,\n): {\n forwardTo?: string | ForwardToCallback<Dependencies> | null | undefined;\n defaultParams?: Params | null | undefined;\n defaultSearch?: SearchParams | null | undefined;\n decodeParams?: ((channels: ParamsSearch) => ParamsSearch) | null | undefined;\n encodeParams?: ((channels: ParamsSearch) => ParamsSearch) | null | undefined;\n} {\n const {\n forwardTo: rawForwardTo,\n defaultParams: rawDefaultParams,\n defaultSearch: rawDefaultSearch,\n decodeParams: rawDecodeParams,\n encodeParams: rawEncodeParams,\n canActivate: rawCanActivate,\n canDeactivate: rawCanDeactivate,\n } = updates;\n\n // One rule for all seven, applied to the SINGLE destructure above so each\n // user getter is still invoked exactly once (#797 / #952).\n const forwardTo = adoptable(rawForwardTo);\n // ⚑ ADOPTED here, the only place that reaches both consumers (#2172).\n // `commitScalarField` writes this value into the store AND the return below\n // becomes the `patch` payload `subscribeChanges` hands out, so a copy at\n // either one alone leaves the other aliased.\n //\n // ⚑ And BEFORE the channel assertion twenty lines down, deliberately. That\n // check reads the bag to decide whether a key belongs to the path channel; on\n // the caller's object it judged one value and the store kept another — the\n // read-before-copy class (#2134) — and moving the copy above it closes that\n // window for this door at no extra read.\n const defaultParams = adoptRouteBag(\n \"defaultParams\",\n adoptable(rawDefaultParams),\n );\n const defaultSearch = adoptRouteBag(\n \"defaultSearch\",\n adoptable(rawDefaultSearch),\n );\n const decodeParams = adoptable(rawDecodeParams);\n const encodeParams = adoptable(rawEncodeParams);\n const canActivate = adoptable(rawCanActivate);\n const canDeactivate = adoptable(rawCanDeactivate);\n\n // ===== PREPARE — compute every change into LOCALS. Any throw here aborts\n // before a single store write, so the whole field set is applied\n // all-or-nothing (#951).\n\n // Channel check on the INCOMING value, in PREPARE: `update` does not rebuild\n // the tree (NO_TREE_REBUILD), so the route's declarations are the ones the\n // matcher already holds. Checked before any write, so a mis-channelled\n // `defaultParams` aborts the whole update rather than landing half-applied.\n if (defaultParams !== undefined && defaultParams !== null) {\n assertChannelCorrect(\n \"updateRoute\",\n name,\n defaultParams,\n queryParamsOf(store, name),\n \"this route's `defaultParams`\",\n \"Move it to `defaultSearch`\",\n );\n }\n\n const forwardToPlan =\n forwardTo === undefined\n ? undefined\n : prepareForwardTo(name, forwardTo, store.config);\n\n const nextCustomFields = prepareCustomFields(store, name, updates);\n\n // Guard factories are compiled NOW (a throwing factory surfaces in PREPARE);\n // the precompiled function is installed in COMMIT without re-invoking the\n // factory, so a factory side effect runs exactly once (reuses the #956\n // compile-then-install seam). Compiled after the other prepares so a throw\n // upstream skips invoking the factory at all.\n const activateFn =\n canActivate === undefined || canActivate === null\n ? undefined\n : lifecycle.compileGuardFactory(canActivate, \"canActivate\");\n const deactivateFn =\n canDeactivate === undefined || canDeactivate === null\n ? undefined\n : lifecycle.compileGuardFactory(canDeactivate, \"canDeactivate\");\n\n // Pre-flight the #961 handler-limit before the COMMIT writes, so an at-limit\n // update that adds a NEW guard slot aborts before forwardTo / scalar config\n // land (#1046, #951). A slot is new only when `name` does not already hold a\n // guard of that type — an overwrite does not count.\n lifecycle.preflightHandlerLimit(\n activateFn === undefined ? [] : [name],\n deactivateFn === undefined ? [] : [name],\n false,\n );\n\n // ===== COMMIT — pure writes from here; nothing below throws.\n // Custom (plugin-defined) fields. Consumers read these lazily via\n // getRouteConfig (lifecycle hooks, preload, searchSchema), so no TREE_CHANGED\n // is needed — the next read sees the new value; the caller's emit stays\n // structural-only by design (O-7).\n if (nextCustomFields !== undefined) {\n if (objectKeys(nextCustomFields).length > 0) {\n store.routeCustomFields[name] = freeze(nextCustomFields);\n } else {\n delete store.routeCustomFields[name];\n }\n }\n\n if (forwardToPlan !== undefined) {\n store.config.forwardMap = forwardToPlan.forwardMap;\n store.config.forwardFnMap = forwardToPlan.forwardFnMap;\n adoptForwardState(store, forwardToPlan.resolved);\n }\n\n commitScalarConfig(store, name, {\n defaultParams,\n defaultSearch,\n decodeParams,\n encodeParams,\n });\n\n // Install the guards from their PREPARE-phase precompiled functions; a `null`\n // clears the definition-origin guard only (#952). See commitGuardUpdate.\n commitGuardUpdate(lifecycle, \"activate\", name, canActivate, activateFn);\n commitGuardUpdate(lifecycle, \"deactivate\", name, canDeactivate, deactivateFn);\n\n return {\n forwardTo,\n defaultParams,\n defaultSearch,\n decodeParams,\n encodeParams,\n };\n}\n\n/**\n * PREPARE step for a `forwardTo` update (#951 atomicity): computes the new\n * forward maps and the resolved forward chain into LOCALS and returns them\n * WITHOUT touching the store. A throw here — an async `forwardTo` (#967) or a\n * cycle surfaced by `refreshForwardMap` — aborts `update()` before any field is\n * committed. The caller writes the returned bundle into the store in its COMMIT\n * phase. (Mirrors the build-then-swap shape of #698, but the swap is deferred to\n * the caller so it can be sequenced with the other prepared fields.)\n */\nfunction prepareForwardTo<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n name: string,\n forwardTo: string | ForwardToCallback<Dependencies> | null,\n config: RouteConfig,\n): {\n forwardMap: RouteConfig[\"forwardMap\"];\n forwardFnMap: RouteConfig[\"forwardFnMap\"];\n resolved: Record<string, string>;\n} {\n // #967: reject an async forwardTo at update time — parity with add/replace\n // (registerForwardTo runs the same check on the build path). A no-op for\n // string/null. Without this the async callback is stored silently and\n // surfaces later as a generic \"must return a string, got object\" TypeError\n // from #resolveDynamicForward at navigation. Runs first, before any clone.\n assertForwardToNotAsync(forwardTo, name);\n\n const forwardMap = Object.assign(\n objectCreate(null) as RouteConfig[\"forwardMap\"],\n config.forwardMap,\n );\n const forwardFnMap = Object.assign(\n objectCreate(null) as RouteConfig[\"forwardFnMap\"],\n config.forwardFnMap,\n );\n\n if (forwardTo === null) {\n delete forwardMap[name];\n delete forwardFnMap[name];\n } else if (typeof forwardTo === \"string\") {\n delete forwardFnMap[name];\n forwardMap[name] = forwardTo;\n } else {\n delete forwardMap[name];\n forwardFnMap[name] = forwardTo;\n }\n\n const resolved = refreshForwardMap({ ...config, forwardMap });\n\n return { forwardMap, forwardFnMap, resolved };\n}\n\n/**\n * PREPARE step for a route's plugin-defined **custom fields** (#951) — the\n * `update` counterpart to how `add`/`replace` register them\n * (`registerSingleRouteHandlers`). A custom field is any patch key not in\n * {@link STANDARD_ROUTE_KEYS}.\n *\n * Computes the merged record and RETURNS it for the caller to commit;\n * `undefined` means no custom-field key was present, so the caller leaves the\n * store untouched. Semantics mirror the scalar fields in\n * {@link commitScalarConfig}: shallow-merge by patch key, `null` removes a\n * single field, `undefined` is a no-op (leaves the field untouched). When the\n * merge empties the record, the caller drops the whole entry so `getRouteConfig`\n * returns `undefined` — symmetric with `add`, which only stores a record when at\n * least one custom field exists.\n *\n * Reading the custom-field getters HERE (in PREPARE, not at commit) is what lets\n * a throwing getter abort the whole update before any field is written. The\n * merged record is a **fresh object**, never mutated in place: `cloneRouter`\n * shares per-route custom-field records by reference (`Object.assign`), so\n * replacing the reference keeps a clone isolated from post-clone updates on the\n * source.\n */\nfunction prepareCustomFields<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n store: RoutesStore<Dependencies>,\n name: string,\n updates: RouteConfigUpdate<Dependencies>,\n): Record<string, unknown> | undefined {\n let next: Record<string, unknown> | undefined;\n\n // `Object.keys` (not `Object.entries`): a value is read only AFTER the\n // standard-key guard, so structural-field getters — already read once by\n // `update`'s destructuring — are not re-invoked. `Object.entries` would read\n // every value eagerly, double-invoking a `defaultParams`/`forwardTo` getter\n // and breaking the \"user getter called once\" invariant.\n\n for (const key of objectKeys(updates)) {\n if (STANDARD_ROUTE_KEYS.has(key)) {\n continue;\n }\n\n const value = (updates as Record<string, unknown>)[key];\n\n // `undefined` mirrors the structural path: leave the field untouched.\n if (value === undefined) {\n continue;\n }\n\n // Clone-on-first-write — keeps clones (which alias this record) isolated.\n next ??= { ...store.routeCustomFields[name] };\n\n if (value === null) {\n delete next[key];\n } else {\n // ⚑ A plain `next[key] = value` dispatched into whatever the chain\n // carries under this name. For the literal `\"__proto__\"` that is the\n // inherited SETTER, so an object value swapped the prototype of the record\n // about to be STORED — and `getRouteConfig` hands that record to plugins,\n // which read it by key (`config?.[hookName]`, `config?.preload`). An\n // injected function was therefore compiled and invoked as a lifecycle hook\n // or a preload factory; a non-object value was silently dropped, because\n // the setter ignores it (#1788).\n //\n // ⚠ That was fixed by special-casing the literal alone, on the\n // reasoning that `constructor` / `toString` and friends are plain data\n // properties which land correctly through assignment. True of\n // `Object.prototype`'s OWN twelve members, and not the hazard (#1852):\n // the key here is a CUSTOM FIELD NAME from the caller's patch, and an\n // application that defines an accessor under that name turns the\n // assignment into a call into its own code. Measured through\n // `update(\"home\", { zzHaz: 42 })` with such an accessor, the getter+setter\n // shape was the bad one — no throw at all, `update()` reported success,\n // the value went to the foreign setter, and the field vanished; on a route\n // with no other custom field the record emptied and `getRouteConfig`\n // answered `undefined`.\n //\n // `putField` covers every name, and it also restores the agreement with\n // REGISTRATION this comment already cared about: `fromEntries` there\n // DEFINES for every key, so `add` was immune on this axis while `update`\n // was not.\n putField(next, key, value);\n }\n }\n\n return next;\n}\n\n/**\n * Applies one nullable scalar-config update in place: `undefined` is a no-op\n * (field not in the patch), `null` deletes the entry, any other value sets it.\n */\nfunction commitScalarField<T>(\n map: Record<string, T>,\n name: string,\n value: T | null | undefined,\n): void {\n if (value === undefined) {\n return;\n }\n\n if (value === null) {\n delete map[name];\n } else {\n map[name] = value;\n }\n}\n\n/**\n * COMMIT step for the scalar config fields of an update (#951): writes\n * `defaultParams` / `defaultSearch` / `decodeParams` / `encodeParams` in place.\n * These assignments\n * are pure and never throw, so they run in the COMMIT phase after every throwing\n * field has been validated in PREPARE. `forwardTo` is handled separately — it\n * has its own throwing prepare step ({@link prepareForwardTo}).\n */\nfunction commitScalarConfig<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n store: RoutesStore<Dependencies>,\n name: string,\n updates: {\n defaultParams?: Params | null | undefined;\n defaultSearch?: SearchParams | null | undefined;\n decodeParams?:\n ((channels: ParamsSearch) => ParamsSearch) | null | undefined;\n encodeParams?:\n ((channels: ParamsSearch) => ParamsSearch) | null | undefined;\n },\n): void {\n commitScalarField(store.config.defaultParams, name, updates.defaultParams);\n commitScalarField(store.config.defaultSearch, name, updates.defaultSearch);\n\n if (updates.decodeParams !== undefined) {\n if (updates.decodeParams === null) {\n delete store.config.decoders[name];\n } else {\n const decoder = updates.decodeParams;\n\n store.config.decoders[name] = (channels: ParamsSearch): ParamsSearch =>\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime fallback if user-provided decoder violates its `{ params, search }` return type\n decoder(channels) ?? channels;\n }\n }\n\n if (updates.encodeParams !== undefined) {\n if (updates.encodeParams === null) {\n delete store.config.encoders[name];\n } else {\n const encoder = updates.encodeParams;\n\n store.config.encoders[name] = (channels: ParamsSearch): ParamsSearch =>\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime fallback if user-provided encoder violates its `{ params, search }` return type\n encoder(channels) ?? channels;\n }\n }\n}\n\n/**\n * COMMIT step for one guard field of an update (#951). `undefined` is a no-op;\n * `null` clears the DEFINITION-origin guard only, preserving an external guard\n * (#952); a factory installs together with its PREPARE-phase `precompiledFn`\n * (no re-compile — #956 seam). Extracted from `update()` so its prepare/commit\n * orchestration stays within the cognitive-complexity budget.\n */\nfunction commitGuardUpdate<Dependencies extends DefaultDependencies>(\n lifecycle: RouteLifecycleNamespace<Dependencies>,\n kind: \"activate\" | \"deactivate\",\n name: string,\n value: GuardFnFactory<Dependencies> | null | undefined,\n precompiledFn: GuardFn | undefined,\n): void {\n if (value === undefined) {\n return;\n }\n\n if (kind === \"activate\") {\n if (value === null) {\n lifecycle.clearCanActivate(name, \"definition\");\n } else {\n lifecycle.addCanActivate(name, value, true, precompiledFn);\n }\n } else if (value === null) {\n lifecycle.clearCanDeactivate(name, \"definition\");\n } else {\n lifecycle.addCanDeactivate(name, value, true, precompiledFn);\n }\n}\n\n// =============================================================================\n// Factory\n// =============================================================================\n\nexport function createRoutesStore<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n routes: Route<Dependencies>[],\n matcherOptions: CreateMatcherOptions | undefined,\n logger: RouterLogger,\n): RoutesStore<Dependencies> {\n // Initial routes are a standalone set at rootPath \"\" — same build the\n // prepare-then-commit `replace` path uses. Guards land in the pending maps,\n // flushed by `flushPendingGuards()` at the end of the Router constructor\n // (#1331); `setDependencies` itself is a pure assignment.\n //\n // Reject the silent-corruption cases BEFORE building, giving the constructor\n // parity with `add()` / `replace()` — the third and last route-population\n // entry point (#1351): within-batch duplicate names (#953/#968) and reserved\n // \"@@\" names (#954). Without these the constructor silently last-wins a\n // duplicate-name sibling (the first route is dropped → its deep-link 404s)\n // while add/replace throw. `methodName` is \"addRoute\" so all three entry\n // points surface the identical bare-core error. (Duplicate PATHS are already\n // rejected downstream by the path-matcher backstop #1153, so they are not\n // re-checked here.)\n // ⚑ `routes` IS core's snapshot: the constructor door guards and copies in one\n // walk (`guardRouteStructure`, #2139) and hands the result down, so there is\n // no second walk of the caller's array here and nothing below reads a\n // definition the guard did not judge. Taking another snapshot at this line\n // would re-open exactly the window that walk closed.\n const batch = routes;\n\n assertNoInternalNamesInBatch(batch, \"addRoute\");\n assertNonEmptyNamesInBatch(batch, \"constructor\");\n assertNoDottedNamesInBatch(batch, \"constructor\");\n assertNoDuplicateNamesInBatch(batch, \"\", \"addRoute\");\n\n const artifacts = buildReplaceArtifacts(batch, \"\", matcherOptions, logger);\n\n const store: RoutesStore<Dependencies> = {\n // Deferred access: the getter runs only after `store` is initialized.\n get definitions() {\n return routeTreeToDefinitions(store.tree);\n },\n config: artifacts.config,\n tree: artifacts.tree,\n matcher: artifacts.matcher,\n urlParamsCache: new Map(),\n queryParamsCache: new Map(),\n resolvedForwardMap: artifacts.resolvedForwardMap,\n hasAnyForward: anyForwardConfigured(artifacts.config),\n routeCustomFields: artifacts.routeCustomFields,\n rootPath: \"\",\n matcherOptions,\n depsStore: undefined,\n lifecycleNamespace: undefined,\n revalidating: false,\n pendingCanActivate: artifacts.pendingCanActivate,\n pendingCanDeactivate: artifacts.pendingCanDeactivate,\n };\n\n // Same config-time channel check the add/replace path runs, so the\n // constructor is not the one population entry point that accepts a config\n // whose own state the router would then reject on `start()`.\n assertRouteDefaultChannelsFor(store.matcher, store.config, \"addRoute\");\n\n // ⚑ The SLOT, not only what it holds. `matcherOptions` is a frozen snapshot\n // precisely because it is reachable from outside core — `routeGetStore()` is\n // on the `RouterInternals` contract, published at\n // `@real-router/core/validation` — but freezing the object left the property\n // that HOLDS it plain writable. Measured: replacing it wholesale was accepted\n // and `dispose()` then threw the named config error, i.e. the #1796 defect\n // reproduced verbatim through the very surface the freeze cites as its reason.\n // Nothing assigns this slot at runtime; the matcher is rebuilt around it.\n //\n // ⚠ State the level you closed, and only that one. This shape repeated three\n // times on the way here — snapshot, container, slot — and it does NOT stop\n // here: `routeGetStore()` hands out fifteen slots, of which EIGHT are\n // destructive when replaced (`matcher`, `tree`, `config`, both caches,\n // `rootPath`, `depsStore`, `lifecycleNamespace`). Only this one is sealed,\n // because only this one was made load-bearing by the snapshot work; the others\n // corrupt loudly or silently on their own terms. Whether the store should be\n // handed to plugins writable at all is the larger question, tracked separately.\n //\n // ⚠ `writable: false` only THROWS in strict mode. A sloppy-mode consumer's\n // write is silently ignored instead — the value is still protected, the\n // signal is not.\n defineProperty(store, \"matcherOptions\", {\n writable: false,\n configurable: false,\n });\n\n return store;\n}\n","// packages/core/src/transitionPath.ts\n\nimport type { State } from \"./types\";\n\n/** Captured like the deciding seven, but this one BUILDS the guarantee (#2073). */\nconst freeze = Object.freeze;\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/**\n * Per-segment param-source map for a route name — `{ segment: { param: \"url\" |\n * \"query\" } }`. Resolved from the live matcher via `RoutesNamespace.getMetaForState`\n * (RFC-4 M2 / #1548: this replaced the removed `stateMetaStore` WeakMap — ownership\n * is now read by `state.name`, not carried per-State object). `undefined` when the\n * name is not in the tree (a CRUD-removed route → FAST PATH 3 fallback).\n */\nexport type RouteMetaLookup = (\n name: string,\n) => Record<string, Record<string, \"url\" | \"query\">> | undefined;\n\n/**\n * Parameters extracted from a route segment.\n * Maps parameter names to their string values.\n */\ntype PrimitiveParam = string | number | boolean;\n\n/**\n * Represents a transition path between two router states.\n * Contains information about which route segments need to be activated/deactivated.\n */\nexport interface TransitionPath {\n /** The common ancestor route segment where paths diverge */\n intersection: string;\n /** Route segments that need to be deactivated (in reverse order) */\n toDeactivate: string[];\n /** Route segments that need to be activated (in order) */\n toActivate: string[];\n}\n\n// Constants for better maintainability\nconst ROUTE_SEGMENT_SEPARATOR = \".\";\nconst EMPTY_INTERSECTION = \"\";\nconst DEFAULT_ROUTE_NAME = \"\";\nconst FROZEN_EMPTY_ARRAY: string[] = [];\n\nObject.freeze(FROZEN_EMPTY_ARRAY);\n\n/**\n * Handles conversion of route names with many segments (5+).\n * Internal helper for nameToIDs function.\n *\n * Uses optimized hybrid approach: split to get segments, then slice original\n * string to build cumulative paths. This approach is 65-81% faster than\n * string concatenation for typical cases (5-10 segments).\n *\n * @param name - Route name with 5 or more segments\n * @returns Array of cumulative segment IDs\n * @throws {Error} If route depth exceeds maximum allowed\n * @internal\n */\nfunction nameToIDsGeneral(name: string): string[] {\n // We know there are at least 5 segments at this point (after fast paths)\n const segments = name.split(ROUTE_SEGMENT_SEPARATOR);\n const segmentCount = segments.length;\n\n // First segment is always just itself\n const ids: string[] = [segments[0]];\n\n // Calculate cumulative lengths and slice from original string\n // This avoids repeated string concatenation (O(k²) → O(k))\n let cumulativeLength = segments[0].length;\n\n for (let i = 1; i < segmentCount - 1; i++) {\n cumulativeLength += 1 + segments[i].length; // +1 for dot separator\n ids.push(name.slice(0, cumulativeLength));\n }\n\n // Last segment is always the full route name\n ids.push(name);\n\n return ids;\n}\n\nconst PRIMITIVE_TYPES: ReadonlySet<string> = new Set([\n \"string\",\n \"number\",\n \"boolean\",\n]);\n\nfunction isPrimitive(value: unknown): value is PrimitiveParam {\n return PRIMITIVE_TYPES.has(typeof value);\n}\n\n/**\n * Compares segment parameters between two states without creating intermediate objects.\n * Returns true if all primitive params for the given segment are equal in both states.\n */\nfunction segmentParamsEqual(\n name: string,\n toMetaParams: Record<string, unknown>,\n toState: State,\n fromState: State,\n): boolean {\n const keys = toMetaParams[name];\n\n if (!keys || typeof keys !== \"object\") {\n return true;\n }\n\n for (const key of objectKeys(keys)) {\n const toVal = toState.params[key];\n const fromVal = fromState.params[key];\n\n if (\n isPrimitive(toVal) &&\n isPrimitive(fromVal) &&\n String(toVal) !== String(fromVal)\n ) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Finds the point where two state paths diverge based on segments and parameters.\n * Compares both segment names and their parameters to find the first difference.\n *\n * @param toMetaParams - Cached meta.params from toState (avoids per-segment WeakMap lookup)\n * @param toState - Target state\n * @param fromState - Source state\n * @param toStateIds - Segment IDs for target state\n * @param fromStateIds - Segment IDs for source state\n * @param maxI - Maximum index to check (minimum of both arrays)\n * @returns Index of first difference, or maxI if all checked segments match\n */\nfunction pointOfDifference(\n toMetaParams: Record<string, unknown>,\n toState: State,\n fromState: State,\n toStateIds: string[],\n fromStateIds: string[],\n maxI: number,\n): number {\n for (let i = 0; i < maxI; i++) {\n const toSegment = toStateIds[i];\n const fromSegment = fromStateIds[i];\n\n // Different segment names - immediate difference\n if (toSegment !== fromSegment) {\n return i;\n }\n\n if (!segmentParamsEqual(toSegment, toMetaParams, toState, fromState)) {\n return i;\n }\n }\n\n return maxI;\n}\n\n/**\n * Converts a route name to an array of hierarchical segment identifiers.\n * Each segment ID includes all parent segments in the path.\n *\n * @param name - Route name in dot notation (e.g., 'users.profile.edit')\n * @returns Array of cumulative segment IDs\n * @throws {Error} If route depth exceeds maximum allowed depth\n *\n * @example\n * // Simple route\n * nameToIDs('users');\n * // Returns: ['users']\n *\n * @example\n * // Nested route\n * nameToIDs('users.profile.edit');\n * // Returns: ['users', 'users.profile', 'users.profile.edit']\n *\n * @example\n * // Empty string (root route)\n * nameToIDs('');\n * // Returns: ['']\n *\n * @remarks\n * Input parameter is NOT validated in this function for performance reasons.\n * Validation significantly slows down nameToIDs execution.\n * The input should be validated by the function/method that calls nameToIDs.\n */\n// Module-global cache (shared across all router instances): bounded in practice by\n// the app's route-name vocabulary, which is stable across cloneRouter() requests, so\n// it does not grow per request. Intentionally NOT cleared on dispose() — it is not\n// per-router, so one router's teardown must not evict entries other routers rely on.\nconst nameToIDsCache = new Map<string, string[]>();\n\nexport function nameToIDs(name: string): string[] {\n const cached = nameToIDsCache.get(name);\n\n // Stryker disable next-line BlockStatement: equivalent — dropping the cache-hit early return recomputes the identical frozen id chain (the cache is a perf optimization, not a correctness gate).\n if (cached) {\n return cached;\n }\n\n const result = computeNameToIDs(name);\n\n freeze(result);\n nameToIDsCache.set(name, result);\n\n return result;\n}\n\nfunction computeNameToIDs(name: string): string[] {\n if (!name) {\n return [DEFAULT_ROUTE_NAME];\n }\n\n const firstDot = name.indexOf(ROUTE_SEGMENT_SEPARATOR);\n\n if (firstDot === -1) {\n return [name];\n }\n\n const secondDot = name.indexOf(ROUTE_SEGMENT_SEPARATOR, firstDot + 1);\n\n if (secondDot === -1) {\n return [name.slice(0, firstDot), name];\n }\n\n const thirdDot = name.indexOf(ROUTE_SEGMENT_SEPARATOR, secondDot + 1);\n\n // Stryker disable next-line UnaryOperator,BlockStatement: equivalent — inverting/emptying the 3-segment fast path routes the name through nameToIDsGeneral (below), which yields the identical id chain (same rationale as the L242 ArithmeticOperator disable). The ConditionalExpression/EqualityOperator siblings stay live (→true and !== are killed).\n if (thirdDot === -1) {\n return [name.slice(0, firstDot), name.slice(0, secondDot), name];\n }\n\n // Stryker disable next-line ArithmeticOperator: equivalent — `thirdDot - 1` makes fourthDot non-(-1), routing 5+ segment names through nameToIDsGeneral, which yields the identical id chain.\n const fourthDot = name.indexOf(ROUTE_SEGMENT_SEPARATOR, thirdDot + 1);\n\n // Stryker disable next-line UnaryOperator,BlockStatement: equivalent — inverting/emptying the 4-segment fast path routes the name through nameToIDsGeneral (below), which yields the identical id chain (same rationale as the L242 ArithmeticOperator disable). The ConditionalExpression/EqualityOperator siblings stay live (→true and !== are killed).\n if (fourthDot === -1) {\n return [\n name.slice(0, firstDot),\n name.slice(0, secondDot),\n name.slice(0, thirdDot),\n name,\n ];\n }\n\n return nameToIDsGeneral(name);\n}\n\n/**\n * Calculates the transition path between two router states.\n * Determines which route segments need to be deactivated and activated\n * to transition from one state to another.\n *\n * @param toState - Target state to transition to\n * @param fromState - Current state to transition from (optional)\n * @returns Transition path with intersection and segments to activate/deactivate\n *\n * @throws {TypeError} When toState is null or undefined\n * @throws {TypeError} When toState is not an object\n * @throws {TypeError} When toState.name is missing or not a string\n * @throws {TypeError} When toState.params is missing or not an object\n * @throws {TypeError} When toState.path is missing or not a string\n * @throws {TypeError} When toState.name contains invalid route format:\n * - Contains only whitespace (e.g., \" \")\n * - Has consecutive dots (e.g., \"users..profile\")\n * - Has leading/trailing dots (e.g., \".users\" or \"users.\")\n * - Segments don't match pattern [a-zA-Z_][a-zA-Z0-9_-]* (e.g., \"users.123\")\n * - Contains spaces or special characters (e.g., \"users profile\")\n * - Exceeds maximum length (8192 characters)\n * @throws {TypeError} When fromState is provided and has any of the validation errors listed above for toState\n *\n * @example\n * // ✅ Valid calls\n * getTransitionPath({ name: 'users.profile', params: {}, path: '/users/profile' });\n * getTransitionPath(toState, fromState);\n * getTransitionPath({ name: '', params: {}, path: '/' }); // root route\n *\n * @example\n * // ❌ Invalid calls that throw TypeError\n * getTransitionPath(null); // toState is null\n * getTransitionPath(undefined); // toState is undefined\n * getTransitionPath({}); // missing required fields\n * getTransitionPath({ name: 123, params: {}, path: '/' }); // name not a string\n * getTransitionPath({ name: 'home', path: '/' }); // missing params\n * getTransitionPath({ name: 'users..profile', params: {}, path: '/' }); // consecutive dots\n * getTransitionPath({ name: '.users', params: {}, path: '/' }); // leading dot\n * getTransitionPath({ name: 'users.', params: {}, path: '/' }); // trailing dot\n * getTransitionPath({ name: 'users profile', params: {}, path: '/' }); // contains space\n * getTransitionPath({ name: 'users.123', params: {}, path: '/' }); // segment starts with number\n * getTransitionPath(validToState, { name: 'invalid..route', params: {}, path: '/' }); // fromState invalid\n *\n * @example\n * // Full activation (no fromState)\n * getTransitionPath(makeState('users.profile'));\n * // Returns: {\n * // intersection: '',\n * // toActivate: ['users', 'users.profile'],\n * // toDeactivate: []\n * // }\n *\n * @example\n * // Partial transition with common ancestor\n * getTransitionPath(\n * makeState('users.profile'),\n * makeState('users.list')\n * );\n * // Returns: {\n * // intersection: 'users',\n * // toActivate: ['users.profile'],\n * // toDeactivate: ['users.list']\n * // }\n *\n * @example\n * // Complete route change\n * getTransitionPath(\n * makeState('admin.dashboard'),\n * makeState('users.profile')\n * );\n * // Returns: {\n * // intersection: '',\n * // toActivate: ['admin', 'admin.dashboard'],\n * // toDeactivate: ['users.profile', 'users']\n * // }\n */\n// Single-entry cache: shouldUpdateNode calls getTransitionPath N times per\n// navigation with the same state objects (once per subscribed node).\n// Cache by reference eliminates N-1 redundant computations.\n// Module-global (≤2 State refs); not cleared on dispose — negligible, not per-router.\nlet cached1To: State | undefined;\nlet cached1From: State | undefined;\nlet cached1Result: TransitionPath | null = null;\n\nlet cached2To: State | undefined;\nlet cached2From: State | undefined;\nlet cached2Result: TransitionPath | null = null;\n\nfunction computeTransitionPath(\n toState: State,\n fromState: State | undefined,\n getMeta: RouteMetaLookup,\n): TransitionPath {\n // ===== FAST PATH 1: Initial navigation (no fromState) =====\n // This is the best performing case in benchmarks (5M ops/sec)\n if (!fromState) {\n return {\n intersection: EMPTY_INTERSECTION,\n toActivate: nameToIDs(toState.name),\n toDeactivate: FROZEN_EMPTY_ARRAY,\n };\n }\n\n // ===== FAST PATH 3: Route name not in tree → full reload =====\n // Ownership is read from the live matcher by `state.name` (RFC-4 M2 / #1548 —\n // the per-State `stateMetaStore` WeakMap was removed). `getMeta` returns\n // `undefined` only when the name is not in the tree — a CRUD-removed route.\n const toMetaParams = getMeta(toState.name);\n const fromMetaParams = getMeta(fromState.name);\n\n if (!toMetaParams && !fromMetaParams) {\n // FAST PATH 3 (both names gone from the tree — e.g. `replace()`d away, or a\n // survivor state whose route was removed). Consumers read the result\n // order-INSENSITIVELY, so the from-chain is returned as-is (root→leaf, no\n // reverse needed): `shouldUpdateNode` reads `toDeactivate` by MEMBERSHIP\n // (`.includes`). Any state whose name IS still in the tree — every navigate\n // pipeline state, popstate/start, and `canNavigateTo`'s built toState — takes\n // the STANDARD PATH below, which trims the shared ancestor and reverses\n // correctly. (Pre-#1548 this branch keyed on a missing per-State WeakMap\n // entry; it now keys on tree membership by name.)\n return {\n intersection: EMPTY_INTERSECTION,\n toActivate: nameToIDs(toState.name),\n toDeactivate: nameToIDs(fromState.name),\n };\n }\n\n // ===== STANDARD PATH: Routes with parameters =====\n const toStateIds = nameToIDs(toState.name);\n const fromStateIds = nameToIDs(fromState.name);\n // Stryker disable next-line MethodExpression: equivalent — Math.max reads one index past the shorter id array; that slot is undefined, so the `toSegment !== fromSegment` check in pointOfDifference returns the same divergence index Math.min would stop at.\n const maxI = Math.min(fromStateIds.length, toStateIds.length);\n\n const i = pointOfDifference(\n (toMetaParams ?? fromMetaParams) as Record<string, unknown>,\n toState,\n fromState,\n toStateIds,\n fromStateIds,\n maxI,\n );\n\n // Optimization: Build deactivation list in reverse order directly\n // instead of slice(i).toReversed() which creates 2 arrays\n let toDeactivate: string[];\n\n if (i >= fromStateIds.length) {\n toDeactivate = FROZEN_EMPTY_ARRAY;\n } else if (i === 0 && fromStateIds.length === 1) {\n // Single-segment route: reversed = original, reuse cached frozen array\n toDeactivate = fromStateIds;\n } else {\n toDeactivate = [];\n\n for (let j = fromStateIds.length - 1; j >= i; j--) {\n toDeactivate.push(fromStateIds[j]);\n }\n }\n\n // Build activation list — reuse cached frozen array when using full list\n const toActivate = i === 0 ? toStateIds : toStateIds.slice(i);\n\n // Determine intersection point (common ancestor)\n const intersection = i > 0 ? fromStateIds[i - 1] : EMPTY_INTERSECTION;\n\n return {\n intersection,\n toDeactivate,\n toActivate,\n };\n}\n\nexport function getTransitionPath(\n toState: State,\n fromState: State | undefined,\n getMeta: RouteMetaLookup,\n): TransitionPath {\n // Cache keys on (toState, fromState) identity only — `getMeta` is deterministic\n // per route name and per-router (states are unique per router), so the same\n // state pair always resolves the same meta, and the cache stays correct\n // without keying on the callback (RFC-4 M2 / #1548).\n // Stryker disable BlockStatement: equivalent — both cache short-circuits below; emptying either early-return recomputes the identical TransitionPath (computeTransitionPath is deterministic for the same to/from states) and re-caches it. Restored right after.\n if (\n cached1Result !== null &&\n toState === cached1To &&\n fromState === cached1From\n ) {\n return cached1Result;\n }\n\n if (\n cached2Result !== null &&\n toState === cached2To &&\n fromState === cached2From\n ) {\n return cached2Result;\n }\n // Stryker restore BlockStatement\n\n const result = computeTransitionPath(toState, fromState, getMeta);\n\n cached2To = cached1To;\n cached2From = cached1From;\n cached2Result = cached1Result;\n\n cached1To = toState;\n cached1From = fromState;\n cached1Result = result;\n\n return result;\n}\n","// packages/core/src/namespaces/RoutesNamespace/RoutesNamespace.ts\n\nimport { DEFAULT_ROUTE_NAME } from \"./constants\";\nimport {\n describeRouteName,\n locationParamsMatch,\n matchSourceTrailingSlash,\n paramsMatch,\n queryParamsOf,\n urlParamsOf,\n} from \"./helpers\";\nimport { createRoutesStore, applyRootPath, resetStore } from \"./routesStore\";\nimport {\n assertChannelCorrect,\n assertShippedChannelCorrect,\n} from \"../../channels\";\nimport { constants, EMPTY_PARAMS, EMPTY_SEARCH } from \"../../constants\";\nimport {\n adoptChannel,\n normalizeChannel,\n mergeDefined,\n recordsShallowEqual,\n withoutUnsafeKey,\n} from \"../../helpers\";\nimport {\n buildURL,\n canonicalize,\n materialize,\n materializePending,\n} from \"../../pipeline\";\nimport { getTransitionPath } from \"../../transitionPath\";\n\nimport type { RoutesStore } from \"./routesStore\";\nimport type { RoutesDependencies } from \"./types\";\nimport type {\n CreateMatcherOptions,\n RouteParams,\n RouteTreeState,\n} from \"../../engine\";\nimport type { RouteResolver } from \"../../pipeline\";\nimport type { RouteMetaLookup } from \"../../transitionPath\";\nimport type {\n DefaultDependencies,\n ForwardToCallback,\n AnyOptions,\n Params,\n RouterLogger,\n SearchParams,\n State,\n Route,\n} from \"../../types\";\nimport type { RouteLifecycleNamespace } from \"../RouteLifecycleNamespace\";\n\n/** Captured like the deciding seven, but this one BUILDS the guarantee (#2073). */\nconst freeze = Object.freeze;\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 hasOwn = Object.hasOwn;\n\nfunction createRouteState<P extends RouteParams = RouteParams>(\n matchResult: {\n readonly segments: readonly { fullName: string }[];\n readonly params: Readonly<Record<string, unknown>>;\n readonly search: Readonly<Record<string, unknown>>;\n readonly meta: Readonly<Record<string, Record<string, \"url\" | \"query\">>>;\n },\n name?: string,\n): RouteTreeState<P> {\n // The matcher yields ≥1 segment for every successful match, each carrying the\n // cumulative route name as `fullName`, so the last element is always present.\n // (Formerly `buildNameFromSegments` with a `?? \"\"` fallback — that branch was\n // unreachable defensive cruft propped up by a white-box test; inlined here.)\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- matcher invariant: a successful match is never empty\n const resolvedName = name ?? matchResult.segments.at(-1)!.fullName;\n\n return {\n name: resolvedName,\n params: matchResult.params as P,\n search: matchResult.search,\n meta: matchResult.meta,\n };\n}\n\ninterface CachedBuildPathOpts {\n readonly trailingSlash?: \"always\" | \"never\" | \"strict\" | undefined;\n readonly queryParamsMode?: \"default\" | \"strict\" | \"loose\" | undefined;\n}\n\n/**\n * The router's four `trailingSlash` values, narrowed to the three the matcher's\n * PER-CALL options take. `\"preserve\"` is the only one that becomes `undefined`:\n * `matchPath` re-reads the raw option and calls `matchSourceTrailingSlash`, so\n * the matcher never sees it.\n *\n * `\"strict\"` passes through and resolves PER ROUTE on the build side, from the\n * compiled `hasTrailingSlash` (#2017). It has to reach the build, because the\n * matcher already carries it as the construction flag `strictTrailingSlash`\n * (`Router.ts` projects it there) and therefore DEMANDS exact\n * trailing-slash-ness when matching — with nothing normalising the built path,\n * a route declared `/b/` committed `state.path` `\"/b\"`, which that same\n * router's `matchPath` refused.\n *\n * ⚠ The predicate is the COMPILED path, not the declaration: registration and\n * the matcher both compute `length > 1 && endsWith(\"/\")`, so a bare `/` can\n * never disagree and the root route is clean — while a splat whose VALUE ends\n * in `/` disagreed with no trailing slash declared anywhere.\n *\n * ⚠ One home for the rule, two call sites for the BAG, and that split is\n * deliberate (#1980): `#getBuildPathOptions` caches its first input for the life\n * of the router, while `matchPath`'s options come from the caller of the\n * published `getInternals().matchPath`. Sharing the cache between them lets one\n * doctored bag rewrite every later `buildPath`.\n */\nfunction narrowTrailingSlash(\n ts: AnyOptions[\"trailingSlash\"] | undefined,\n): \"never\" | \"always\" | \"strict\" | undefined {\n return ts === \"preserve\" ? undefined : ts;\n}\n\n/**\n * Independent namespace for managing routes.\n *\n * Static methods handle validation (called by facade).\n * Instance methods handle storage and business logic.\n */\nexport class RoutesNamespace<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> {\n readonly #store: RoutesStore<Dependencies>;\n\n #cachedBuildPathOpts: CachedBuildPathOpts | undefined;\n // Source `options` reference captured on the first #getBuildPathOptions call;\n // used only by the dev-build immutability assertion below (#957).\n #cachedOptionsSource: AnyOptions | undefined;\n\n get #deps(): RoutesDependencies<Dependencies> {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n return this.#store.depsStore!;\n }\n\n constructor(\n // No `= []` default: the sole caller (Router's ctor) always passes its own\n // already-defaulted `routes` — a namespace-level default would be dead code\n // and a default-before-required-params smell (S1788).\n routes: Route<Dependencies>[],\n matcherOptions: CreateMatcherOptions | undefined,\n logger: RouterLogger,\n ) {\n this.#store = createRoutesStore(routes, matcherOptions, logger);\n }\n\n /**\n * Creates a predicate function to check if a route node should be updated.\n * Note: Argument validation is done by facade (Router.ts) via validateShouldUpdateNodeArgs.\n */\n\n static shouldUpdateNode(\n nodeName: string,\n getMeta: RouteMetaLookup,\n ): (toState: State, fromState?: State) => boolean {\n return (toState: State, fromState?: State): boolean => {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!(toState && typeof toState === \"object\" && \"name\" in toState)) {\n throw new TypeError(\n \"[router.shouldUpdateNode] toState must be valid State object\",\n );\n }\n\n // ⚑ `?.` on a field the type declares REQUIRED, and the honest form here\n // (#1976): `getInternals` is published and the commit door preserves a\n // foreign State's ABSENCE rather than fabricating meta (#1792), so\n // `getState()` can legally return one without it — measured, this\n // predicate threw on exactly that state.\n //\n // Tolerate rather than throw, because absent and `DEFAULT_TRANSITION`\n // give the SAME answer: `reload` is `undefined` in both, so the node\n // falls through to the ordinary comparison below.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- see above: the required field is genuinely absent on a foreign committed State\n if (toState.transition?.reload) {\n return true;\n }\n\n // Root node (DEFAULT_ROUTE_NAME === \"\") has no route-level identity — it\n // represents \"any route\". It must update on every transition so that\n // consumers subscribed via useRouteNode(\"\") (including RouteView at\n // the top of the tree) see every change. This matches the documented\n // contract in adapter docs: `useRouteNode(\"\")` — Root — ALL route\n // changes. See #519 for the missed transitions it was suffering from\n // (users → users.user had intersection=\"users\", leaving the root node\n // un-updated under a flat <Match segment=\"users.user\" exact> pattern).\n if (nodeName === DEFAULT_ROUTE_NAME) {\n return true;\n }\n\n const { intersection, toActivate, toDeactivate } = getTransitionPath(\n toState,\n fromState,\n getMeta,\n );\n\n if (nodeName === intersection) {\n return true;\n }\n\n if (toActivate.includes(nodeName)) {\n return true;\n }\n\n return toDeactivate.includes(nodeName);\n };\n }\n\n // =========================================================================\n // Dependency injection\n // =========================================================================\n\n /**\n * Is `replace()`'s revalidation window open (#1758 / #1759)?\n *\n * Read by `Router.#assertNotReentrant` beside `EventBus.isProcessing()` and\n * `Navigation.isPreparing()`, and by the route-CRUD ban. The window's own\n * writer is `replaceRoutes`, which raises it in a `try` and lowers it in the\n * matching `finally` — left raised, it would deadlock the router against its\n * own next call, the same failure the preparing flag names.\n */\n isRevalidating(): boolean {\n return this.#store.revalidating;\n }\n\n /**\n * Sets dependencies. Pure assignment — no side effects (#1331).\n *\n * The pending canActivate/canDeactivate factories from initial routes are\n * flushed separately by {@link flushPendingGuards}, called once wiring is\n * complete, so the order of the wire-* calls is unconstrained.\n */\n setDependencies(deps: RoutesDependencies<Dependencies>): void {\n this.#store.depsStore = deps;\n }\n\n /**\n * Registers the pending guard factories collected from initial route\n * definitions. Deferred out of {@link setDependencies} (#1331) so it runs on\n * a fully-built, fully-bound router: a guard factory that calls any\n * `router.*` method sees a ready instance instead of a half-assembled one.\n *\n * Invoked as the last step of the Router constructor. Idempotent after the\n * first call (the pending maps are cleared). Runtime `add()`/`replace()`\n * compile guards in their own PREPARE phase and never populate these maps.\n */\n flushPendingGuards(): void {\n const deps = this.#deps;\n\n for (const [routeName, handler] of this.#store.pendingCanActivate) {\n deps.addActivateGuard(routeName, handler);\n }\n\n this.#store.pendingCanActivate.clear();\n\n for (const [routeName, handler] of this.#store.pendingCanDeactivate) {\n deps.addDeactivateGuard(routeName, handler);\n }\n\n this.#store.pendingCanDeactivate.clear();\n }\n\n /**\n * Sets the lifecycle namespace reference.\n */\n setLifecycleNamespace(\n namespace: RouteLifecycleNamespace<Dependencies> | undefined,\n ): void {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n this.#store.lifecycleNamespace = namespace!;\n }\n\n // =========================================================================\n // Route tree operations\n // =========================================================================\n\n setRootPath(newRootPath: string): void {\n applyRootPath(this.#store, newRootPath);\n }\n\n hasRoute(name: string): boolean {\n return this.#store.matcher.hasRoute(name);\n }\n\n clearRoutes(): void {\n resetStore(this.#store);\n }\n\n // =========================================================================\n // Path operations\n // =========================================================================\n\n /**\n * Builds a URL path for a route.\n * Note: Argument validation is done by facade (Router.ts) via validateBuildPathArgs.\n *\n * @param route - Route name\n * @param params - Route parameters\n * @param search - Query-channel params. REQUIRED: the port is the only\n * caller and hands over `canonical.query`, which `canonicalize` always\n * defines, so a `?? EMPTY_SEARCH` here would be a branch nothing takes\n * @param options - Router options\n */\n buildPath(\n route: string,\n params: Params,\n search: SearchParams,\n options?: AnyOptions,\n ): string {\n if (route === constants.UNKNOWN_ROUTE) {\n return typeof params.path === \"string\" ? params.path : \"\";\n }\n\n // The route codec sees BOTH channels — `encodeParams({ params, search })` →\n // `{ params, search }` (§4) — so an encoder can shape the query as well as\n // the path.\n // ⚑ BOUND ONCE, and the two lines are the whole of #1889. `route` is the\n // caller's own argument, used here as a PROPERTY KEY, so `typeof …[route]`\n // and `…[route](…)` were two `ToPropertyKey` calls into application code\n // with a gap between them. Three failure modes, all measured on bare core:\n //\n // • the route's `encodeParams` RAN before a refusal that was already\n // guaranteed — the caller's own function, invoked for work whose result\n // nobody can observe (5 coercions where a string caller has 0);\n // • a DRIFTING name type-checked one route's encoder and invoked\n // another's — reads `C,C,C,A` tested C and ran A;\n // • a drift landing on a route with NO encoder threw the raw\n // `this[#store].config.encoders[route] is not a function`, leaking a\n // mangled private-field expression to the caller. The single bind closes\n // that one too, without a line of its own.\n //\n // ⚠ One divergence of two survives and this does NOT claim otherwise: a\n // drift can still split the ENCODER read from the MATCHER read below, so\n // `C,C,C,A` runs C's encoder and builds for A. Closing that is the terminal\n // question #1883 owns.\n const encoder = this.#store.config.encoders[route];\n\n if (typeof encoder === \"function\") {\n const encoded = encoder({\n // BOTH channels spread, and the symmetry is the point. Only `search`\n // arrives frozen: `path` is frozen by `materialize` at the publication\n // boundary and is still live here (#1928, measured from inside the\n // interceptor). Without the copy, a codec that edits its argument in\n // place — legal before this entry point joined the pipeline — would be a\n // silent no-op (sloppy mode) or a `TypeError` (ESM) on ONE half of one\n // documented hook and work on the other. Copying both is what keeps the\n // two halves behaving alike.\n params: { ...params },\n search: { ...search },\n });\n\n return this.#store.matcher.buildPath(\n route,\n encoded.params,\n encoded.search,\n this.#getBuildPathOptions(options),\n );\n }\n\n return this.#store.matcher.buildPath(\n route,\n params,\n search,\n this.#getBuildPathOptions(options),\n );\n }\n\n /**\n * The INTENT form of ⑤a: canonicalise, then print.\n *\n * ⚠ The merge lives HERE and not inside `buildPath` above, and that is what\n * closes #1847. `buildPath` is the port's ⑤a printer, which builds the URL\n * from ALREADY-MERGED channels. Two callers reach it: this one, and\n * `buildURL` on the navigate path, which hands it a `Canonical`. A printer\n * that merged again would put\n * `canonicalize` on a navigation TWICE, each pass reading the route's own\n * `defaultSearch` / `defaultParams` independently — the object is held by\n * reference and read on every navigation by design, so an accessor-backed one\n * could answer differently between the two.\n *\n * Both faces of #1847 followed from that, and neither is within a pass:\n *\n * - `state.search` contradicting its own `state.path` — pass 1 built the\n * channel, pass 2 printed the URL, and a default that turned defined\n * between them printed a key the state does not carry;\n * - `buildPath` disagreeing with `navigate` on one intent — the INVARIANTS\n * row \"href equals destination\" (#1578) — because the two doors shipped\n * different reads of the same default.\n *\n * One merge per call is therefore the fix, not a smaller read count: a\n * per-pass snapshot leaves both faces standing (measured), and snapshotting at\n * registration is refused by the config contract, which states that nested\n * config aliases the live store and is read on every navigation.\n *\n * The LITERAL form: `buildPath` does not follow `forwardTo` (A.5 —\n * `buildPath(\"src\")` stays `/src`, a deliberate asymmetry with `navigate`), so\n * stage ① is skipped and the seam is never entered.\n *\n * ⚑ Interceptors run ABOVE this, exactly once: the facade puts the caller's\n * intent through the `forwardState` seam before canonicalising (#2087), and\n * nothing interceptable stands between the port and the engine (#1938). What\n * a plugin injects therefore lands on the same side of the merge as it does\n * for `navigate`.\n *\n * ⚑ It is a TRADE, and the halves point opposite ways. Measured against\n * `origin/master`, alternating processes, medians of 9 rounds × 150k ops, with\n * both floors taken (A/A ≤ 2 %, B/B up to 10 % — so the first row is at the\n * floor and its magnitude is not established, only its direction, which held\n * in both runs of each side):\n *\n * buildPath, no defaults 156 vs 146 ns +7 %\n * buildPath, with defaultSearch 620 vs 567 ns +9 %\n * buildNavigationState, no defaults 397 vs 514 ns −23 %\n * buildNavigationState, defaultSearch 687 vs 1088 ns −37 %\n *\n * This door pays one extra hop (through `buildURL` → `port.buildPath` → the\n * namespace primitive) plus the copy `withholdFilledSlots` now always\n * returns; the state-producing doors stop running `canonicalize` a second\n * time. Both are the same edit seen from two sides, and the side that got\n * cheaper is the one every navigation takes.\n */\n buildPathFromIntent(\n route: string,\n params: Params,\n search?: SearchParams,\n ): string {\n // ⚑ `params` is REQUIRED, and its net lives one layer up. The href door is\n // the only caller, and since #2087 it runs the `forwardState` seam first —\n // so the bag reaching here has already passed that seam's own\n // `?? EMPTY_PARAMS`. A second default here would be an unreachable branch.\n if (route === constants.UNKNOWN_ROUTE) {\n // Nothing to canonicalise — the URL is the payload, not an intent. The\n // executor owns that branch, and going through the port puts this arc on\n // the same printer every other route reaches.\n //\n // ⚠ Which is why the normalise is HERE, and the reason is the SHAPE ⑤a\n // reads rather than any seam. This is the one arc that skips\n // `canonicalize`, so nothing else copies the bag — the href door hands\n // this method the caller's own object (#2087). The printer's read on this\n // route is `params.path`, and a bare property access walks the prototype\n // chain: without this call an INHERITED `path` would print here and\n // nowhere else. `normalizeChannel` copies own enumerable keys and\n // collapses an empty bag onto the shared frozen singleton — the same two\n // things `canonicalize` does on every other arc.\n return this.#deps.port.buildPath(\n route,\n normalizeChannel(params, EMPTY_PARAMS),\n search ?? EMPTY_SEARCH,\n );\n }\n\n return buildURL(\n canonicalize(this.#deps.port, route, params, search, {\n resolveForward: false,\n }),\n this.#deps.port,\n );\n }\n\n /**\n * Matches a URL path to a route in the tree.\n * Note: Argument validation is done by facade (Router.ts) via validateMatchPathArgs.\n */\n matchPath<P extends Params = Params>(\n path: string,\n options?: AnyOptions,\n ): State<P> | undefined {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Router.ts always passes options\n const opts = options!;\n\n const matchResult = this.#store.matcher.match(path);\n\n if (!matchResult) {\n return undefined;\n }\n\n const routeState = createRouteState(matchResult);\n const { name } = routeState;\n\n // ⚑ `withoutUnsafeKey` BEFORE the decoder, not after it, and on BOTH\n // channels (#1904). Everything below this line — the route's own\n // `decodeParams`, and the `forwardState` seam `canonicalize` reaches — is\n // application code, while these bags are ones core BUILT by parsing the URL.\n // The published channels drop the key anyway, much further down; the door is\n // here, where core stops owning the container.\n //\n // ⚠ `params` is NOT redundant beside `search`. A route may declare a path\n // SLOT named `__proto__` (`/q/:__proto__` — registration accepts it), and\n // measured there, the decoder receives `params` with own keys `[\"__proto__\"]`\n // while the committed state has `[]`. ⚠ A probe on a fixture with no such\n // slot shows `params` clean and clears nothing — it never builds the shape.\n const params = withoutUnsafeKey(routeState.params);\n // The matcher always carries a search bag (a frozen `{}` when empty) but types\n // its values as `unknown`; narrow it to the query channel once here so the\n // codec / forwardState / rebuild uses it without per-site casts.\n const search = withoutUnsafeKey(routeState.search as SearchParams);\n\n // Two-channel decode (RFC-4 M2 / #1548, §4): the route codec sees BOTH the\n // path params AND the parsed query — `decodeParams({ params, search })` →\n // `{ params, search }` — restoring v1's reach (v1 ran the whole path+query bag\n // through the decoder). Runs here, inside match, BEFORE any search-schema\n // plugin validation (the v1 order: engine codec → plugin). With no decoder the\n // channels pass through untouched.\n const decoder = this.#store.config.decoders[name];\n let decoded: { params: Params; search: SearchParams };\n\n if (typeof decoder === \"function\") {\n decoded = decoder({ params, search });\n\n // The ONE boundary on this path where a value core is about to build a\n // state from came out of USER code (#1582). The matcher's own output above\n // needs no check — it is router-produced and plain by construction — but a\n // decoder may hand back anything: an array becomes `params {\"0\":\"a\"}`, a\n // string becomes `{\"0\":\"o\",\"1\":\"o\",…}`, a `Map` or a prototyped object\n // becomes `{}`, and every one of those commits silently. Opt-in, like every\n // DX check: `null` in bare core, where the behaviour is unchanged either\n // way. Labelled for THIS entry point — the pre-pipeline call sat inside a\n // `forwardState` dep wrapper and blamed that method for a `matchPath` fault.\n this.#deps\n .getValidator()\n ?.routes.validateStateBuilderArgs(name, decoded.params, \"matchPath\");\n\n // The channel half of the same boundary — always on, unlike the shape\n // check above. A decoder that moves a declared `?key` into the params bag\n // is REFUSED rather than repaired by the seam and shipped as if it had\n // been written correctly. Checked HERE as well as at the seam so the\n // message names the decoder rather than the chain it later flows through —\n // same single assertion, applied where the fault was actually authored.\n assertChannelCorrect(\n \"matchPath\",\n name,\n decoded.params,\n this.getQueryParams(name),\n \"the `params` returned by this route's `decodeParams`\",\n );\n } else {\n decoded = { params, search };\n }\n\n // Stages ① + ③ + the mode gate, one pass through the pipeline (nav-pipeline\n // Phase 2, step 2-2). `canonicalize` reaches the `forwardState` seam\n // through `port.resolveForward`, which IS `ctx.forwardState` — so a\n // search-schema interceptor validates the query on the URL→State path here\n // (the `routeSearch` argument is defined, marking this as a re-parse, not a\n // navigate), and the seam checks the channels on the way through. The\n // route's own default split (#1549), the default merge and the mode gate\n // (#1575) all happen once,\n // inside `canonicalize`, from the same read-model `navigate` uses.\n // ⚠ There is no stage ② on this path: the seam REFUSES a mis-channelled bag\n // rather than repairing one. A `forwardState` interceptor injecting a\n // declared query key into `result.params` throws here exactly as it does on\n // `navigate`.\n const canonical = canonicalize(\n this.#deps.port,\n name,\n decoded.params,\n decoded.search,\n );\n const routeName = canonical.name;\n\n // The canonical channels, ready for BOTH halves of the state: `canonical.path`\n // is path-only (the seam REFUSED any declared `?key` a plugin injection or a\n // decoder left in the params bag, and stage ③ layered the route's own\n // defaults under it, each slot in its own channel — #1549),\n // `canonical.query` is the full canonical query with the mode gate already\n // applied (#1575). Both are read from ONE object, so the\n // rebuilt `state.path` and the committed `state.search` cannot derive from\n // differently-merged bags (INVARIANTS makeState #6).\n assertShippedChannelCorrect(\n \"matchPath\",\n canonical.name,\n canonical.path,\n this.getQueryParams(canonical.name),\n );\n\n const routeParams = canonical.path;\n const forwardedSearch = canonical.query;\n\n let builtPath = path;\n\n if (opts.rewritePathOnMatch) {\n // Two-channel encode for the URL rebuild (RFC-4 M2 / #1548, §4): path slots\n // from `routeParams` (canonical, path-only), the query string from\n // `forwardedSearch` (the full canonical query) — never the raw matched\n // query — so the rebuilt `state.path` stays in step with `state.search`.\n // ⚑ The TWIN of the bind above (#1889). `routeName` is `canonical.name`,\n // which a plugin's `forwardState` interceptor may hand back as a\n // non-string, and every use of it coerces again — the local holds the\n // OBJECT, not a name.\n //\n // ⚠ What this bind closes here is the `typeof`/invoke pair, and NOT the\n // divergence that matters most at this site. Measured by sweeping the read\n // index (flip the answer after read N, N = 0…7): before the bind, flips 4\n // and 5 published a State named for one route and BUILT WITH ANOTHER\n // ROUTE'S ENCODER; after it, flips 3 and 4 still do. The window moved by\n // one read and did not close, because the surviving split is between the\n // ENCODER read and the read that produces the published `state.name` —\n // a different pair, and the criterion's \"an object whose own fields\n // disagree\" rather than a refusal on the way out.\n //\n // That residue is the terminal question #1883 owns, at a second door: this\n // one publishes a State whose `name` IS the interceptor's object. Recorded\n // here so the next reader does not take the bind for a closure it is not.\n const encoder = this.#store.config.encoders[routeName];\n const encoded =\n typeof encoder === \"function\"\n ? encoder({\n params: routeParams,\n search: forwardedSearch,\n })\n : { params: routeParams, search: forwardedSearch };\n\n const ts = opts.trailingSlash;\n\n try {\n // Explicit two-channel build (RFC-4 M2 / #1548): path slots from\n // `encoded.params`, the query string from `encoded.search`. The channels\n // stay separate, so a `/coll/:id?id` collision keeps its path slot and\n // query twin independent — no single-bag reunification, no\n // search-wins-for-declared-query fixup.\n // ⚑ The narrowing is {@link narrowTrailingSlash}'s, shared with\n // `#getBuildPathOptions` — the rule has ONE home. The BAG is built here\n // rather than taken from that cache, and the difference is not stylistic:\n // `#getBuildPathOptions` caches its first input forever, and `opts` here\n // is whatever the caller of the published `getInternals().matchPath`\n // passed. Feeding it to the cache lets one doctored bag rewrite what\n // `router.buildPath()` prints for the rest of the router's life, while\n // `getOptions()` keeps reporting the real value.\n builtPath = this.#store.matcher.buildPath(\n routeName,\n encoded.params,\n encoded.search,\n {\n trailingSlash: narrowTrailingSlash(ts),\n queryParamsMode: opts.queryParamsMode,\n },\n );\n\n // The value the narrowing drops, and this arc's own business: the\n // matcher never sees it, so it is read from the RAW option.\n if (ts === \"preserve\") {\n builtPath = matchSourceTrailingSlash(path, builtPath);\n }\n } catch {\n // The match already succeeded (route found, params decoded); only the\n // post-match path rewrite threw — e.g. a custom encoder handed buildPath\n // a query value its codec cannot serialise. Keep the source path\n // un-rewritten rather than discard a valid match (#1157). Opposite of the\n // parse side (#737): there a throw means \"URL not understood\" → unmatched;\n // here the URL WAS matched and only re-canonicalisation failed.\n builtPath = path;\n }\n }\n\n // Stage ⑤b. `materialize` over `makeState` because the intent is ALREADY\n // canonical: `makeState` would re-run the default merge (idempotent, but a\n // wasted pass) and would rebuild the path when none is handed to it, which is\n // exactly the work ⑤a above just did. The generic rides through\n // (`matchPath<P>` → `materialize<P>` → `State<P>`), so a consumer's typed\n // params survive the migration.\n return materialize<P>(canonical, builtPath);\n }\n\n /**\n * Applies `forwardTo` and returns the resolved name with the CHAIN's defaults\n * layered under the caller's channels.\n *\n * Order, per channel: every forwarding HOP's defaults (the earliest hop wins),\n * then the caller's value on top. The TARGET route's own defaults are\n * deliberately NOT part of it — the body says why they cannot be, and the\n * body says why, and this summary must not list them as a step.\n */\n forwardState<\n P extends Params = Params,\n S extends SearchParams = SearchParams,\n >(\n name: string,\n params: P,\n search?: S,\n ): { name: string; params: P; search: S } {\n // TARGET-route defaults are NOT applied here for EITHER channel — they are\n // merged strictly BELOW the user channels at the pipeline's single terminal\n // (`canonicalize`, reached alike by `makeState`, `matchPath` and\n // `buildPath`). Folding a\n // target default into this result would ride ABOVE a user params-twin at the\n // terminal merge, inverting the priority — a\n // `navigate(x, { page: 2 })` on a `?page` route with `defaultSearch{page:1}`\n // would wrongly commit `page=1`. So both channels pass through as the user\n // gave them: `search` stays `resolvedSearch`, `params` stays the raw bag.\n //\n // The ONE default forwardState still merges is the forwardTo CHAIN's own\n // defaults (`#layerChainDefaults` in the forward branches): when `a` forwards\n // to `b`, `a`'s `defaultParams` fill in before the redirect and flow to `b`.\n // This canNOT move to the terminal — `canonicalize` sees only the RESOLVED\n // target `b` and cannot reconstruct source `a`'s defaults. Each of a hop's\n // slots keeps its own channel (`defaultParams` the path, `defaultSearch` the\n // query) whatever the target declares; #1548 retired #1570's routing by\n // the target's declaration, and a hop default naming a key the TARGET\n // declares with `?` is refused at the seam instead. Frozen empty search\n // singleton when absent.\n const resolvedSearch = (search ?? EMPTY_SEARCH) as S;\n\n if (hasOwn(this.#store.config.forwardFnMap, name)) {\n const dynamicForward = this.#store.config.forwardFnMap[name];\n // ⚑ `ownParams` — the resolver's own read, not the caller's bag (#2143).\n // The callback decided from it, so the URL has to be built from it too.\n const {\n target,\n chain,\n params: ownParams,\n } = this.#resolveDynamicForward(name, dynamicForward, params);\n\n return this.#layerChainDefaults(\n target,\n chain,\n ownParams as P,\n resolvedSearch,\n );\n }\n\n const staticForward = this.#store.resolvedForwardMap[name] ?? name;\n\n if (\n staticForward !== name &&\n hasOwn(this.#store.config.forwardFnMap, staticForward)\n ) {\n const targetDynamicForward =\n this.#store.config.forwardFnMap[staticForward];\n const {\n target,\n chain,\n params: ownParams,\n } = this.#resolveDynamicForward(\n staticForward,\n targetDynamicForward,\n params,\n );\n\n // The static prefix stops AT `staticForward` (it has no static forward of\n // its own), and the dynamic walk starts THERE — so the two halves\n // concatenate without repeating a hop.\n return this.#layerChainDefaults(\n target,\n [...this.#collectStaticChain(name), ...chain],\n ownParams as P,\n resolvedSearch,\n );\n }\n\n if (staticForward !== name) {\n return this.#layerChainDefaults(\n staticForward,\n this.#collectStaticChain(name),\n params,\n resolvedSearch,\n );\n }\n\n return {\n name,\n params,\n search: resolvedSearch,\n };\n }\n\n /**\n * Builds a RouteTreeState from an already-resolved route name and params.\n *\n * ⚠ Its ONE caller — `getPluginApi().buildNavigationState` — uses it as an\n * EXISTENCE probe and discards the object (`if (!ctx.buildStateResolved(…))\n * return;`). Since Phase 2 that entry point materialises its state from the\n * `Canonical`, so what this returns is built and dropped; what the caller\n * needs is the `undefined` arm, placed BEFORE `buildURL` because the matcher\n * throws on an unknown route while that entry point must answer `undefined`.\n * Whether this should therefore collapse into a `hasRoute`-shaped predicate is\n * an open question, not a settled design — the same dead-surface shape\n * coverage surfaced for `makeState`'s `skipFreeze` arm in Phase 4.\n */\n buildStateResolved(\n resolvedName: string,\n resolvedParams: Params,\n ): RouteTreeState | undefined {\n const segments = this.#store.matcher.getSegmentsByName(resolvedName);\n\n if (!segments) {\n return undefined;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const meta = this.#store.matcher.getMetaByName(resolvedName)!;\n\n return createRouteState(\n // `resolvedParams` is path-only by the PRODUCER's contract, not by a repair\n // upstream: the seam refuses a declared `?key` in the params bag rather\n // than moving one out (stage ② is gone — #1548). The `search: {}`\n // placeholder threads nowhere; the caller discards this object.\n { segments, params: resolvedParams, search: {}, meta },\n resolvedName,\n );\n }\n\n // =========================================================================\n // Query operations\n // =========================================================================\n\n /**\n * Checks if a route is currently active.\n *\n * Two arms, `literal || destination` (#1573). The literal arm is the whole\n * predicate below, unchanged. The destination arm repeats THAT SAME predicate\n * on the full output of stage ① — the resolved terminal name together with\n * the forwarding chain's defaults, each hop's slot in its own channel — so a\n * `<Link to=\"alias\">` reads active on the page it actually navigates to.\n *\n * It is a FALLBACK, never a pre-resolution: resolving before comparing would\n * send a section link (`users` forwarding to `users.list`) to the leaf and\n * darken it while a sibling descendant (`users.profile`) is active.\n *\n * It repeats the predicate on ①'s OUTPUT rather than substituting the name,\n * because the chain's `defaultParams` live on the forwarding SOURCE and are\n * layered by `forwardState` (#1566/#1570) — never by the forward map — and a\n * dynamic `forwardTo` is not in that map at all. Name substitution therefore\n * fixes neither, and it also carries no `search`, which is the channel a hop's\n * own `defaultSearch` lands in.\n */\n isActiveRoute(\n name: string,\n // Singletons, not fresh literals (#1589). `= {}` minted TWO throwaway objects\n // on every call — i.e. on every `<Link>` on every re-render across six\n // adapters — and the `search` one was worse than an allocation: being a fresh\n // `{}` it is neither `undefined` nor `EMPTY_SEARCH`, so it defeated\n // `canonicalize`'s own empty-query fast path and this predicate ran the full\n // merge + mode-gate tail every time. Measured: the flame graph showed\n // `withholdFilledSlots` and the drop-sink getter live under a route with\n // nothing to withhold and nothing to drop.\n params: Params = EMPTY_PARAMS,\n searchArg: SearchParams = EMPTY_SEARCH,\n strictEquality = false,\n ignoreQueryParams = true,\n ): boolean {\n if (\n this.#matchesActiveState(\n name,\n params,\n searchArg,\n strictEquality,\n ignoreQueryParams,\n )\n ) {\n return true;\n }\n\n // O(1) gate: only a route that actually forwards can have a second arm.\n // Every `<Link>` in six adapters runs this predicate on every render, so a\n // non-forwarding route must not pay for the arm at all.\n //\n // TREE-WIDE first, per-route second (#1595). The two maps below are\n // `Object.create(null)` dictionaries — V8 keeps those in dictionary mode\n // whatever their size, empty ones included — and the pair of lookups measured\n // ~14 ns, i.e. 1.75x this predicate's pre-pipeline cost on the shape that\n // reaches here: an INACTIVE link, which is most links on a page. The cost is\n // not the `Object.hasOwn` form (replacing it with a plain property read\n // measured identical) but touching the dictionaries at all, so the fix is to\n // not touch them when no route in the tree forwards. `hasAnyForward` is\n // maintained beside `resolvedForwardMap`, never separately.\n if (!this.#store.hasAnyForward) {\n return false;\n }\n\n // ONE read of the name, and it is the read the two lookups below share\n // (#1946). `hasOwn(map, name)` runs `ToPropertyKey`, so asking twice asks\n // the same question twice of a value whose `toString` is application code:\n // it can answer one map and then the other, and it can throw — into the\n // render this predicate promises never to throw into. The gate sits OUTSIDE\n // both protected regions: after `#matchesActiveState` has returned, and\n // before the `forwardState` try below. Neither boundary reaches it.\n let forwardKey: string;\n\n try {\n forwardKey =\n typeof name === \"string\"\n ? name\n : // Reached only when the value is NOT a string; the rule reads the\n // declared type, which is exactly what this line distrusts.\n // eslint-disable-next-line unicorn/no-useless-coercion -- see above\n String(name);\n } catch {\n return false;\n }\n\n if (\n !hasOwn(this.#store.config.forwardMap, forwardKey) &&\n !hasOwn(this.#store.config.forwardFnMap, forwardKey)\n ) {\n return false;\n }\n\n let forwarded;\n\n try {\n // The NAMESPACE primitive, not the interceptable seam: a predicate on the\n // render path must not run the plugin interceptor chain once per `<Link>`.\n forwarded = this.forwardState(name, params, searchArg);\n } catch (error) {\n // A dynamic `forwardTo` is user code and may throw. A predicate answers,\n // it never throws from inside a render — same policy as `canNavigateTo`\n // on a throwing guard (#959): honest `false` plus an operational log.\n this.#deps.logger.warn(\n \"router.isActiveRoute\",\n `Dynamic forwardTo of route \"${describeRouteName(name)}\" threw while resolving the active-link destination; treating the link as inactive.`,\n error,\n );\n\n return false;\n }\n\n // No `forwarded.name === name` guard: the O(1) gate above already proved the\n // route forwards, and a self-returning dynamic callback throws on the cycle\n // check before `forwardState` can hand one back — so the terminal name is\n // always a different route (measured; the branch was unreachable).\n return this.#matchesActiveState(\n forwarded.name,\n forwarded.params,\n forwarded.search,\n strictEquality,\n ignoreQueryParams,\n );\n }\n\n getMetaForState(\n name: string,\n ): Record<string, Record<string, \"url\" | \"query\">> | undefined {\n return this.#store.matcher.hasRoute(name)\n ? this.#store.matcher.getMetaByName(name)\n : undefined;\n }\n\n getUrlParams(name: string): readonly string[] {\n return urlParamsOf(this.#store, name);\n }\n\n /**\n * Declared query param names of a route (`?a&b` across its segments,\n * ancestors included) that are NOT also path params — the query-channel twin\n * of {@link getUrlParams}. THE registry (#1556): the always-on channel guard,\n * the literal form's default withholding and the mode gate all classify\n * through it, and the URL build prints from it. The slot IS the channel\n * (#1548), so this registry classifies and nothing routes. A colliding name (`/items/:id?id` —\n * legal under M2, the channels\n * coexist) is path-owned for routing purposes: excluding it here keeps the\n * path slot's value in `state.params` and the rebuild's #843 precedence\n * intact. Same cache lifecycle: cleared on every matcher rebuild.\n *\n * Reads the matcher's `declaredQueryParams` — the SAME registry the\n * query-string build uses — rather than walking `matchSegments` (#1556). The\n * segment walk missed the ROOT node's `?`-declarations (`setRootPath(\"?a&b\")`,\n * how persistent-params declares its keys), because the root is captured\n * separately at `registerTree` and never appears in `matchSegments`. That\n * made a root-declared key print as query but classify as a path param: it\n * landed in `state.params`, vanished from `state.path` on the intent side,\n * and no `isActiveRoute` spelling matched a link to the active page. One\n * registry classifies and prints, so the two cannot drift again.\n */\n getQueryParams(name: string): readonly string[] {\n return queryParamsOf(this.#store, name);\n }\n\n getStore(): RoutesStore<Dependencies> {\n return this.#store;\n }\n\n /**\n * The pipeline's read-model, for entry points that live on the FACADE rather\n * than in a namespace (`canNavigateTo`). The port is created during wiring,\n * after `registerInternals` has already run, so the facade cannot hold it —\n * it reaches it through the namespace that does, exactly as the resolver\n * itself reaches the store through {@link getStore}.\n */\n getPort(): RouteResolver {\n return this.#deps.port;\n }\n\n /**\n * The literal arm of {@link isActiveRoute} — unchanged by #1573.\n *\n * Safe boundary (#1577): both branches below READ the caller's bags — since\n * nav-pipeline Phase 2 step 2-5 through `canonicalize` in its literal form,\n * which walks `params` key by key, and then again in the descendant branch's\n * channel-by-channel `paramsMatch` — so an accessor-backed key, a `Proxy` or a\n * framework's reactive object throws HERE, on the render path. The\n * predicate's\n * stated policy is that it answers and never throws from inside a render (see\n * the `forwardState` wrap below), and #1573 implemented that for the\n * destination arm only. One boundary around the whole walk rather than a\n * `try` per read —\n * the same shape `isParams` took for the same class of hostile input (#1052).\n */\n #matchesActiveState(\n name: string,\n params: Params,\n searchArg: SearchParams,\n strictEquality: boolean,\n ignoreQueryParams: boolean,\n ): boolean {\n try {\n return this.#matchesActiveStateUnsafe(\n name,\n params,\n searchArg,\n strictEquality,\n ignoreQueryParams,\n );\n } catch (error) {\n this.#deps.logger.warn(\n \"router.isActiveRoute\",\n `Reading the arguments for route \"${describeRouteName(name)}\" threw while resolving the active-link state; treating the link as inactive.`,\n error,\n );\n\n return false;\n }\n }\n\n #matchesActiveStateUnsafe(\n name: string,\n params: Params,\n searchArg: SearchParams,\n strictEquality: boolean,\n ignoreQueryParams: boolean,\n ): boolean {\n // Note: empty string check is handled by Router.ts facade\n const activeState = this.#deps.getState();\n\n if (!activeState) {\n return false;\n }\n\n const activeName = activeState.name;\n\n // Fast path: check if routes are related before expensive operations\n if (\n activeName !== name &&\n !activeName.startsWith(`${name}.`) &&\n !name.startsWith(`${activeName}.`)\n ) {\n return false;\n }\n\n // The comparison target, built by the SAME pipeline every producer uses\n // (nav-pipeline Phase 2, step 2-5) — in the LITERAL form: this predicate asks\n // about the route it was NAMED. `forwardTo` is resolved by the caller's\n // second arm (`isActiveRoute`), through the NON-interceptable namespace\n // primitive, and `{ resolveForward: false }` is what keeps it that way here:\n // the literal form never touches the port, so a plugin's interceptor chain\n // does not run once per `<Link>` per render.\n //\n // ⚠ No stage ② at this point either — the rule is stated at the\n // `canonicalize` call above. What it means HERE: a declared query key handed\n // in the `params` bag is not moved to the query channel before comparison,\n // so a v1 single-bag call does not match. Channel-correctness is the\n // caller's contract, exactly as for `navigate` (which throws on that shape)\n // and `buildPath` (which prints without it).\n const canonical = canonicalize(this.#deps.port, name, params, searchArg, {\n resolveForward: false,\n });\n\n // Exact match case. Path \"\" skips the URL build — `areStatesEqual` compares\n // channels and never reads the URL, which is also why `materialize` needs no\n // port argument here (the fork milestone 1 left open, settled in step 2-3).\n if (strictEquality || activeName === name) {\n // Pending (#1589): the state exists for the length of one comparison.\n // `areStatesEqual` reads `.name` / `.params` / `.search` and nothing else,\n // so freezing it buys a guarantee no one can observe. The CHANNELS are still frozen;\n // that happens in `canonicalize`, and it is the part that matters\n // (canonicalize invariant #4). Measured at 926 µs, ~5 % of this benchmark.\n const pending = materializePending(canonical, \"\");\n\n // The PATH channel is asked with `ignoreQueryParams: true` WHATEVER the\n // caller passed: that polarity compares the route's DECLARED path params,\n // i.e. the location. The other answers state IDENTITY over the whole\n // `params` bag (#515 / #478) — a different question, and borrowing it made\n // this predicate contradict itself (#1978).\n //\n // What the flag reaches, and what the two arms share, is DERIVED —\n // `tests/functional/is-active-route-scope-authority-1978.test.ts`.\n //\n // ⚠ A DECLARED query name spelled into the params bag is not the inert\n // key this is about: it WITHHOLDS a `defaultSearch` for that slot, so the\n // href loses `?name=value` and the answer is `false`. Pinned by \"honours\n // a query default the params-bag twin withholds\".\n if (!this.#deps.areStatesEqual(pending, activeState, true)) {\n return false;\n }\n\n if (!locationParamsMatch(canonical.path, activeState.params)) {\n return false;\n }\n\n // By EQUALITY rather than the subset above: an ancestor link legitimately\n // says less than the state, a link to THIS route does not.\n return (\n ignoreQueryParams ||\n recordsShallowEqual(pending.search, activeState.search)\n );\n }\n\n // The fast path above lets through three relations: exact (handled in\n // the previous block), `activeName` descendant of `name`, and `name`\n // descendant of `activeName`. Only the first two count as \"active\" —\n // a link pointing DEEPER than the current state is a navigation option,\n // not an active state. Reject the descendant-of-active case explicitly.\n if (!activeName.startsWith(`${name}.`)) {\n return false;\n }\n\n // Hierarchical check: activeState is a descendant of target (name). Compared\n // CHANNEL BY CHANNEL (step 2-5) instead of over one recombined bag: the\n // canonical target already carries the route's defaults, each merged under\n // the caller's value in the channel its own SLOT names (#1549), so the\n // separate `paramsMatchExcluding` passes over `defaultParams` /\n // `defaultSearch` are not needed — a default that survived into\n // `canonical` is exactly a default the caller did not override.\n //\n // Restricted to the keys the COMMITTED STATE carries (#1978) — the same call\n // the exact arm above makes.\n if (!locationParamsMatch(canonical.path, activeState.params)) {\n return false;\n }\n\n // The query channel obeys `ignoreQueryParams`, the same flag the exact arm\n // hands to `areStatesEqual`. The recombined-bag form could not: it folded\n // query into the path bag before matching, so an ancestor link compared its\n // query even when the caller asked to ignore it — the two arms disagreed\n // about the flag.\n if (\n !ignoreQueryParams &&\n !paramsMatch(canonical.query as Params, activeState.search as Params)\n ) {\n return false;\n }\n\n return true;\n }\n\n /**\n * Every node on a `forwardTo` chain that FORWARDS, in walk order — the\n * terminal is excluded (its own defaults belong to the state builder, #1549).\n * `forwardMap` is proven acyclic at registration (`refreshForwardMap` runs\n * `resolveForwardChain`, which throws on a cycle), so the walk terminates.\n */\n #collectStaticChain(name: string): string[] {\n const chain: string[] = [];\n let current = name;\n\n while (hasOwn(this.#store.config.forwardMap, current)) {\n chain.push(current);\n current = this.#store.config.forwardMap[current];\n }\n\n return chain;\n }\n\n /**\n * Layers the defaults of every forwarding hop UNDER the caller's channels.\n * Folding in walk order makes an EARLIER hop win over a later one (each merge\n * puts the next hop's defaults below what is already accumulated), and the\n * caller wins over all of them. Only the entered route was consulted before,\n * so a default declared on an intermediate hop never reached the target and\n * a required slot was left empty (#1566).\n *\n * Each slot IS its channel: a hop's `defaultParams` is the path channel and\n * its `defaultSearch` the query channel, whatever the resolved target\n * declares. A hop's `defaultParams` naming a key the TARGET declares with `?`\n * is refused at the `forwardState` seam, where the target is finally known.\n *\n * (#1570 routed this fold by the target's declaration for one release;\n * #1548 retired that with the rest of stage ②. The body below records\n * why — it is not repeated here, and the retired rule is not restated in the\n * present tense above it.)\n */\n #layerChainDefaults<\n P extends Params = Params,\n S extends SearchParams = SearchParams,\n >(\n target: string,\n chain: readonly string[],\n params: P,\n search: S,\n ): { name: string; params: P; search: S } {\n let hopDefaults: Params | undefined;\n let hopSearchDefaults: SearchParams | undefined;\n\n for (const routeName of chain) {\n // `undefined` is absence on both sides (#1550 / #1551): a source default\n // carrying `undefined` must not ride out of `forwardState` as an own key.\n hopDefaults = mergeDefined(\n this.#store.config.defaultParams[routeName] as Params | undefined,\n hopDefaults,\n );\n // A hop's `defaultSearch` folds here beside `defaultParams` (#1549), and\n // the symmetry is the point: each slot has to mean the same thing on a\n // forwarding node and on a terminal, so neither can be live in one\n // position and silently inert in the other.\n hopSearchDefaults = mergeDefined(\n this.#store.config.defaultSearch[routeName] as SearchParams | undefined,\n hopSearchDefaults,\n );\n }\n\n // Each slot IS its channel — no split, here or anywhere else. A hop's\n // `defaultParams` is the path channel and its `defaultSearch` the query\n // channel, whatever the resolved target declares.\n //\n // Routing the fold by the TARGET's declaration instead, on the argument\n // that a hop \"can only spell a default in `defaultParams`\", is false on its\n // face — the fold reads `defaultSearch` two lines above — and does real\n // damage: a hop author cannot tell which channel their own config lands in\n // without reading a target that a `forwardTo` CALLBACK may not determine\n // until navigation time. Instead\n // they can: the slot they wrote is the channel they get.\n //\n // A hop whose `defaultParams` names a key the TARGET declares with `?` is\n // still caught — not here, but at the `forwardState` seam, where the target\n // is finally known and the check can name both routes. Registration cannot\n // see it (a dynamic `forwardTo` has no target yet), which is exactly why\n // that check lives at resolution rather than being guessed here.\n // No cross-channel withholding any more, and its removal is the point.\n // #1570 needed it because the split put a caller's params-twin and the\n // query half of the same default in DIFFERENT bags, where no merge ranks\n // them — so the default had to be withheld by hand or it won. Nothing is\n // split now: a key in the caller's `params` and a key in the chain's\n // `search` are different channels holding different names, because a caller\n // who puts a declared query name in `params` is refused outright. Each\n // channel merges against its own default and nothing has to look sideways.\n\n return {\n name: target,\n // ⚑ `normalizeChannel` on the way out, and it is what #1848 pinned. On a\n // hop carrying NO defaults the merge hands the caller's own bag straight\n // back, and this is what stops it reaching every `forwardState`\n // interceptor — a public seam `persistent-params-plugin` sits on. These\n // bags are accessor-backed in practice without anyone writing a getter\n // (Vue `reactive()`, Svelte `$props()`).\n //\n // ⚠ Measured by mutation: removing it reds both cells of\n // `hop-bag-read-once-1848`. A short-circuit around the merge on an absent\n // default buys nothing — that arm returns its argument.\n params: normalizeChannel(\n mergeDefined<P>(hopDefaults as P | undefined, params),\n EMPTY_PARAMS,\n ),\n // `mergeDefined`, not a spread: an explicit `undefined` from the caller is\n // ABSENCE (#1550 / #1551), so it must not delete the hop default.\n search: normalizeChannel(\n mergeDefined<S>(hopSearchDefaults as S | undefined, search),\n EMPTY_SEARCH,\n ),\n };\n }\n\n #getBuildPathOptions(options?: AnyOptions): CachedBuildPathOpts {\n // Stryker disable next-line BlockStatement: equivalent — cache short-circuit; emptying the early-return rebuilds the identical buildPath options (deterministic) and re-caches them. (ConditionalExpression stays live: `→false` always rebuilds but a real consumer test pins the cached identity.)\n if (this.#cachedBuildPathOpts) {\n /* v8 ignore next 5 -- @preserve: dev assertion guarding a future caller that passes per-call varying options; the sole caller (Router.buildPath, always via this.#options.get()) passes the same frozen per-instance options object, so this branch is unreachable through the public API by construction (#957) */\n if (options !== this.#cachedOptionsSource) {\n this.#deps.logger.warn(\n \"router.buildPath\",\n \"`options` differs from the cached source reference; router options are immutable per router instance, so the first-cached buildPath options are reused (#957).\",\n );\n }\n\n return this.#cachedBuildPathOpts;\n }\n\n this.#cachedOptionsSource = options;\n\n this.#cachedBuildPathOpts = freeze({\n trailingSlash: narrowTrailingSlash(options?.trailingSlash),\n queryParamsMode: options?.queryParamsMode,\n });\n\n return this.#cachedBuildPathOpts;\n }\n\n #resolveDynamicForward(\n startName: string,\n startFn: ForwardToCallback<Dependencies>,\n callerParams: Params,\n ): { target: string; chain: string[]; params: Params } {\n // ⚑ ONE read of the caller's bag, taken BEFORE the first callback is\n // consulted, and RETURNED so the hop defaults layer over the same object\n // (#2143). The callback picks the destination from what it reads here and\n // `#layerChainDefaults` prints the URL from what it reads there — two\n // questions of an application-owned bag, and a value that answered\n // differently between them chose one route and shipped another. Measured\n // through the plugin seam: the callback saw `id: \"1\"` and `{ id: \"999\" }`\n // came back; through `isActiveRoute`, a `<Link>` pointing exactly where the\n // user already stood reported itself inactive.\n //\n // ⚠ Here rather than in `forwardState`, so the copy is paid ONLY on the\n // branches that consult a callback. A route that does not forward reaches\n // no reader at all through that seam and keeps handing its container back\n // by identity — the #2134 measurement, pinned by\n // `handed-out-containers-1957`.\n const params = adoptChannel(callerParams);\n const visited = new Set<string>([startName]);\n // Every node that forwards, in walk order — `startName` does by definition.\n // The terminal is never pushed, so the caller can layer hop defaults without\n // pulling in the target's (#1566/#1549).\n const chain: string[] = [startName];\n\n let current = startFn(this.#deps.getDependency, params);\n let depth = 0;\n const MAX_DEPTH = 100;\n\n if (typeof current !== \"string\") {\n throw new TypeError(\n `forwardTo callback must return a string, got ${typeof current}`,\n );\n }\n\n while (depth < MAX_DEPTH) {\n if (this.#store.matcher.getSegmentsByName(current) === undefined) {\n throw new Error(`Route \"${current}\" does not exist`);\n }\n\n if (visited.has(current)) {\n const cycle = [...visited, current].join(\" → \");\n\n throw new Error(`Circular forwardTo: ${cycle}`);\n }\n\n visited.add(current);\n\n if (hasOwn(this.#store.config.forwardFnMap, current)) {\n const fn = this.#store.config.forwardFnMap[\n current\n ] as ForwardToCallback<Dependencies>;\n\n chain.push(current);\n current = fn(this.#deps.getDependency, params);\n\n depth++;\n continue;\n }\n\n const staticForward = this.#store.config.forwardMap[current];\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (staticForward !== undefined) {\n chain.push(current);\n current = staticForward;\n depth++;\n continue;\n }\n\n return { target: current, chain, params };\n }\n\n throw new Error(`forwardTo exceeds maximum depth of ${MAX_DEPTH}`);\n }\n}\n","// packages/core/src/namespaces/NavigationNamespace/constants.ts\n\nimport { errorCodes } from \"../../constants\";\nimport { RouterError } from \"../../RouterError\";\n\nimport type { State } from \"../../types\";\n\n// =============================================================================\n// Cached Errors & Rejected Promises (Performance Optimization)\n// =============================================================================\n// Pre-created error instances and rejected promises for the sync error paths in\n// navigate(), which eliminates a `new RouterError()` (object + stack capture,\n// ~500ns-2μs), a `Promise.reject()` and a suppression `.catch()` per call.\n//\n// Trade-off: every instance shares one stack trace, pointing here. Acceptable\n// because these are expected conditions rather than internal bugs, and code +\n// message identify them.\n// =============================================================================\n\nexport const CACHED_NOT_STARTED_ERROR = new RouterError(\n errorCodes.ROUTER_NOT_STARTED,\n);\n\nexport const CACHED_ROUTE_NOT_FOUND_ERROR = new RouterError(\n errorCodes.ROUTE_NOT_FOUND,\n);\n\nexport const CACHED_SAME_STATES_ERROR = new RouterError(errorCodes.SAME_STATES);\n\n/**\n * The boot window's sentence. Same code as `CACHED_NOT_STARTED_ERROR` — this IS\n * the not-started window — with a message that names it, because the bare code\n * reads as \"you forgot to call start()\" while the caller is INSIDE `start()`.\n *\n * The REFUSAL is the table's (`canNavigate()` is false in STARTING,\n * `SYSTEM_COMMIT` is undeclared there); only the message is chosen here, at the\n * refusal site, by `deps.isStarting()` — an ordinary never-started router keeps\n * the plain error (#1647).\n */\nexport const CACHED_PRE_BOOT_COMMIT_ERROR = new RouterError(\n errorCodes.ROUTER_NOT_STARTED,\n {\n message:\n \"[router] cannot commit before the start navigation does — the boot would overwrite it; defer with queueMicrotask/await, or navigate after start() resolves\",\n },\n);\n\n// #1606 backstop: these instances are handed to arbitrary consumer code (every\n// `.catch()`, `onTransitionError`, leave-signal `reason`) process-wide, so an\n// in-place write — core's own `setCode` was one — rewrites the error every\n// OTHER consumer sees, across routers (SSR: across requests). Freezing turns\n// that corruption into a strict-mode TypeError at the writer (sloppy-mode\n// writes become silent no-ops); reading, including `stack`, is unaffected.\nObject.freeze(CACHED_NOT_STARTED_ERROR);\nObject.freeze(CACHED_ROUTE_NOT_FOUND_ERROR);\nObject.freeze(CACHED_SAME_STATES_ERROR);\nObject.freeze(CACHED_PRE_BOOT_COMMIT_ERROR);\n\n// Pre-suppressed rejected promises — see `PRE_SUPPRESSED` at the bottom for\n// what their identity buys the producer.\nexport const CACHED_NOT_STARTED_REJECTION: Promise<State> = Promise.reject(\n CACHED_NOT_STARTED_ERROR,\n);\n\nexport const CACHED_ROUTE_NOT_FOUND_REJECTION: Promise<State> = Promise.reject(\n CACHED_ROUTE_NOT_FOUND_ERROR,\n);\n\nexport const CACHED_SAME_STATES_REJECTION: Promise<State> = Promise.reject(\n CACHED_SAME_STATES_ERROR,\n);\n\nexport const CACHED_PRE_BOOT_COMMIT_REJECTION: Promise<State> = Promise.reject(\n CACHED_PRE_BOOT_COMMIT_ERROR,\n);\n\n// Suppress once at module load — prevents unhandled rejection events.\n// Subsequent .catch() / await by user code still works correctly:\n// a rejected promise stays rejected forever, each .catch() creates\n// its own derived promise and fires its handler.\nCACHED_NOT_STARTED_REJECTION.catch(() => {}); // NOSONAR -- intentional suppression, not a promise chain\nCACHED_ROUTE_NOT_FOUND_REJECTION.catch(() => {}); // NOSONAR\nCACHED_SAME_STATES_REJECTION.catch(() => {}); // NOSONAR\nCACHED_PRE_BOOT_COMMIT_REJECTION.catch(() => {}); // NOSONAR\n\n// =============================================================================\n// Fire-and-forget suppression policy (#721) — shared, not per-caller\n// =============================================================================\n\n/**\n * Rejection codes that are EXPECTED, caller-owned outcomes, not internal bugs.\n * The fire-and-forget safety net stays silent for them and lets an awaiting\n * caller see the rejection. `CANNOT_ACTIVATE` / `CANNOT_DEACTIVATE` belong here:\n * a guard blocking (or a plugin's guard-blocked `back()`/`forward()`) is a\n * normal result, so a call without `await` must not emit a spurious \"Unexpected\n * navigation error\".\n *\n * Most of them are navigation outcomes, but not all — `ROUTER_ALREADY_STARTED`\n * is a LIFECYCLE precondition (#1605), and it is the same kind of thing:\n * `start()` called twice says \"already done\", exactly as `SAME_STATES` does for\n * `navigate()`. Silence is the symmetric answer; logging it would report a\n * caller's own no-op as an internal fault. It costs nothing on the navigation\n * side — `navigate()` cannot produce that code.\n *\n * SHARED, which is why it is here and not in the namespace: `Router.start()`\n * classifies its own failures by the same policy (`#onSuppressedStartError`).\n */\nexport const SUPPRESSED_ERROR_CODES: ReadonlySet<string> = new Set([\n errorCodes.SAME_STATES,\n errorCodes.TRANSITION_CANCELLED,\n errorCodes.ROUTER_NOT_STARTED,\n errorCodes.ROUTE_NOT_FOUND,\n errorCodes.CANNOT_ACTIVATE,\n errorCodes.CANNOT_DEACTIVATE,\n errorCodes.ROUTER_ALREADY_STARTED,\n]);\n\n/** Module-level, so classifying allocates nothing per navigate()/start(). */\nexport function isExpectedRejection(error: unknown): boolean {\n return error instanceof RouterError && SUPPRESSED_ERROR_CODES.has(error.code);\n}\n\n/**\n * The four cached rejections ABOVE, by identity.\n *\n * They carry a `.catch()` from module load already, so a second one prevents\n * nothing and only allocates a derived promise (measured: ~40 ns, ~12.5% of a\n * SAME_STATES `navigate()`); this set lets the producer skip it.\n *\n * Identity, not a flag, because the two fail in OPPOSITE directions: a missed\n * identity costs 40 ns, while a flag left stale-true skips suppression on a\n * LATER navigation and leaks the rejection — #721 exactly. Fail-safe by\n * construction: anything not recognised here gets suppressed.\n */\nexport const PRE_SUPPRESSED: ReadonlySet<unknown> = new Set([\n CACHED_NOT_STARTED_REJECTION,\n CACHED_ROUTE_NOT_FOUND_REJECTION,\n CACHED_SAME_STATES_REJECTION,\n CACHED_PRE_BOOT_COMMIT_REJECTION,\n]);\n","import { errorCodes, constants } from \"../../../constants\";\nimport { RouterError, freezeThrownError } from \"../../../RouterError\";\n\nimport type { State, TransitionMeta } from \"../../../types\";\nimport type { NavigationDependencies, NavigationContext } from \"../types\";\n\n// ⚑ Captured at module load — same rule as `helpers.ts` and `materialize.ts`.\n// Measured with the global neutered: this file handed back an unfrozen\n// `transition` and `segments` on a committed state, and no test saw it, because\n// a freeze that is a no-op changes no outcome. Walked here from the level below\n// (#1928 made `materialize`'s capture load-bearing and the question \"where else\n// is it read raw?\" answered with this file).\nconst freeze = Object.freeze;\n\ntype MutableTransitionMeta = {\n -readonly [K in keyof TransitionMeta]: TransitionMeta[K];\n};\n\n/**\n * Built entirely from the PLAN: the three flags were snapshotted at the entry\n * (#1719), so nothing here reads the caller's `NavigationOptions`.\n */\nfunction buildTransitionMeta(nav: NavigationContext): TransitionMeta {\n const { fromState, toDeactivate, toActivate, intersection } = nav;\n\n freeze(toDeactivate);\n freeze(toActivate);\n\n const segments = freeze({\n deactivated: toDeactivate,\n activated: toActivate,\n intersection,\n });\n\n const meta: MutableTransitionMeta = {\n phase: \"activating\",\n reason: \"success\",\n segments,\n };\n\n if (fromState?.name !== undefined) {\n meta.from = fromState.name;\n }\n\n if (nav.reload !== undefined) {\n meta.reload = nav.reload;\n }\n\n if (nav.replace !== undefined) {\n meta.replace = nav.replace;\n }\n\n if (nav.redirected !== undefined) {\n meta.redirected = nav.redirected;\n }\n\n return freeze(meta);\n}\n\nexport function completeTransition(\n deps: NavigationDependencies,\n nav: NavigationContext,\n): State {\n const { toState, fromState, toDeactivate, toActivate } = nav;\n\n if (\n toState.name !== constants.UNKNOWN_ROUTE &&\n !deps.hasRoute(toState.name)\n ) {\n const err = new RouterError(errorCodes.ROUTE_NOT_FOUND, {\n routeName: toState.name,\n });\n\n deps.sendTransitionFail(fromState, err, nav);\n\n throw freezeThrownError(err);\n }\n\n // ⚑ No literal: the navigation's own context IS the commit payload (#1648) —\n // it is the object the machine adopted on NAVIGATE, so `mayCommit` recognises\n // it by reference, and building a second one here would force the caller to\n // copy an identity into it by hand. It carries `opts` UNSTRIPPED;\n // sanitising that for SUBSCRIBERS is the announcement's job, in the action.\n const commit = nav;\n\n // ⚑ **This function reads no `opts` field at all, which is what makes the\n // window between the ask and the send empty STRUCTURALLY** rather than by\n // care: built out of the CALLER's accessor- or Proxy-backed object\n // (`navigate/edge-cases-proxy` pins three such getters), the meta lets a\n // getter calling `stop()` under it invalidate a verdict already given —\n // `COMPLETE` finds no edge, the send is a silent no-op, and `navigate()`\n // resolves a state nobody committed (#1719).\n //\n // ⚠ Not \"no application code runs in `completeTransition`\" — the ANNOUNCE\n // below the verdict runs plenty, synchronously into every plugin hook and\n // every `router.subscribe` listener, and so does the `ROUTE_NOT_FOUND` arm\n // above. The claim is narrower: between the ask and the send there is\n // bookkeeping and nothing else.\n\n // A SECOND literal, deliberately (#2144). Every door that hands the pending\n // target to application code seals it, so the shell arrives frozen and\n // attaching the meta by mutation is not available here — and must not be:\n // a writable shell at the handout is a slot a guard can assign to, and\n // whatever occupies it at commit time is what gets committed, so `name` and\n // `path` can be made to disagree. Measured on the `_reverify`\n // alternating-process harness at 150 000 × 15: the extra object sits at the\n // noise floor (deltas −0.8 % and +0.1 % against an A/A floor of +0.3 %).\n //\n // ⚠ `finalState` is NOT the object the guards saw. The two readers of state\n // identity are safe by construction: `transitionPath`'s cache keys on the\n // PENDING pair inside the transition, and `createTransitionSource` compares an\n // event argument with itself.\n // ⚑ Annotated AT the literal, like `#copyChannels`: both censuses in\n // `state-freeze-authority` key on the TYPE, so the contextual form would make\n // this State constructor — and the shell freeze taking it — invisible to the\n // scans that exist to count them.\n const committed: State = {\n ...toState,\n transition: buildTransitionMeta(nav),\n };\n const finalState = freeze(committed);\n\n // ⚑ Written back onto the navigation context, and that is what keeps #1648\n // intact: `commit` IS `nav`, the machine recognises it BY REFERENCE, so the\n // sealed state has to arrive through the field rather than through a second\n // payload. This is core writing to core's own object — the thing #2144 closed\n // is the write application code could make to the SHELL, not this one.\n nav.toState = finalState;\n\n // ONE ask, unconditional, and it stands HERE: after the last application code,\n // before the post-leave cleanup, with nothing but bookkeeping between it and\n // the send (RFC-10a §7.4). The cleanup below is DESTRUCTIVE — it unregisters\n // the departing route's external `canDeactivate` — so a cancelled navigation\n // must not reach it and eat the guard of the route the user is STAYING on.\n // Measured with the type defeated to get there: an ask below the loop reds 16\n // tests across five files. The two conditions look mutually exclusive (a\n // non-empty cleanup means the departing route HAS a guard, and such a\n // navigation is fenced long before the commit) — `forceDeactivate` is what\n // separates them, skipping the deactivate PHASE while `planPhases` still\n // fills `canDeactivateFunctions`.\n //\n // ⚑ The ordering is the TYPE's job, not this comment's: the ask hands back a\n // `CommitPermit` and the clear below demands one, so moving it down is\n // `TS2448` rather than a red test — a lock earned by making the mistake twice.\n const permit = deps.canCommitTransition(commit);\n\n if (!permit) {\n throw freezeThrownError(new RouterError(errorCodes.TRANSITION_CANCELLED));\n }\n\n // No `fromState` / `hasSlotToClear` gate: with `fromState === undefined`\n // `computeTransitionPath`'s first arm returns `FROZEN_EMPTY_ARRAY` for\n // `toDeactivate`, so the loop is already a no-op there, and\n // the pre-scan existed only to keep the now-deleted second `canSend` off the\n // common path.\n for (const name of toDeactivate) {\n if (toActivate.includes(name) || !nav.canDeactivateFunctions.has(name)) {\n continue;\n }\n\n deps.clearCanDeactivate(name, permit);\n }\n\n // ⚠ Reading the verdict from the effect instead (\"did `getState()` become my\n // state?\") looks cleaner and is WRONG — measured, not reasoned: a `subscribe`\n // listener may legitimately `replace()` during the success emit, which commits\n // a different state on top. That is a successful commit followed by another\n // one, and identity cannot tell it from a refusal.\n deps.sendTransitionDone(commit);\n\n return finalState;\n}\n","// packages/core/src/namespaces/NavigationNamespace/transition/errorHandling.ts\n\nimport { errorCodes, UNSAFE_KEY } from \"../../../constants\";\nimport { RouterError, freezeThrownError } from \"../../../RouterError\";\nimport { putField } from \"../../../utils/ingest\";\n\nimport type { State } from \"../../../types\";\nimport type { NavigationDependencies } from \"../types\";\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 objectEntries = Object.entries;\n\n/**\n * Is this thrown value already the router's quiet-cancel outcome?\n *\n * Asked by {@link handleGuardError} (a guard signalling a quiet cancel by\n * throwing the RouterError directly, #933) and by {@link asCancellation}, which\n * must not re-wrap one.\n */\nfunction isTransitionCancelled(error: unknown): boolean {\n return (\n error instanceof RouterError &&\n error.code === errorCodes.TRANSITION_CANCELLED\n );\n}\n\n/**\n * Restate a failure as the cancellation it actually was (#1609).\n *\n * Both failure arcs call it for a navigation that has LOST liveness, and every\n * way to lose it is a cancellation the navigation already announced. Carrying\n * the guard's verdict instead put a `FAIL` in the FSM under a navigation still\n * running, because {@link routeTransitionError} filters by error CODE. Liveness\n * itself is asked at the call sites, from different facts\n * (`finishAsyncNavigation` off the signal, `handleNavigateError` off the FSM).\n *\n * A value already carrying the code is returned untouched — two pinned cells:\n * #1197's leave rejection keeps its `reason` (#943), and the resolve path's\n * cancellation is not wrapped twice.\n *\n * ⚠ The table does not make this redundant. `STARTING --FAIL--> IDLE` is how a\n * failed `start()` unwinds — unconditional, so a stale `FAIL` there kills a\n * RESTART — and which error `navigate()` rejects with is a contract, not an\n * edge. Measured: neutralising this reds 6 tests, five in\n * `superseded-guard-rejection-1609.test.ts`.\n */\nexport function asCancellation(error: unknown): unknown {\n return isTransitionCancelled(error)\n ? error\n : new RouterError(errorCodes.TRANSITION_CANCELLED, { reason: error });\n}\n\nexport function routeTransitionError(\n deps: NavigationDependencies,\n error: unknown,\n fromState: State | undefined,\n nav: object,\n): void {\n const routerError = error as RouterError;\n\n if (\n routerError.code === errorCodes.TRANSITION_CANCELLED ||\n routerError.code === errorCodes.ROUTE_NOT_FOUND\n ) {\n return;\n }\n\n deps.sendTransitionFail(fromState, routerError, nav);\n}\n\nexport function handleGuardError(\n error: unknown,\n errorCode: string,\n segment: string,\n): never {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw freezeThrownError(new RouterError(errorCodes.TRANSITION_CANCELLED));\n }\n\n // A guard can also signal a quiet cancel by throwing\n // RouterError(TRANSITION_CANCELLED) directly — the same intent as a thrown\n // AbortError. Preserve it as-is instead of letting rethrowAsRouterError\n // overwrite the code with CANNOT_ACTIVATE / CANNOT_DEACTIVATE: that code\n // drives the downstream suppression (routeTransitionError early-returns,\n // fire-and-forget stays silent), so re-coding would surface the intended\n // quiet cancel as a reported transition error (#933).\n if (isTransitionCancelled(error)) {\n throw error;\n }\n\n rethrowAsRouterError(error, errorCode, segment);\n}\n\n/**\n * Error metadata structure for transition errors.\n * Contains information extracted from caught exceptions.\n */\nexport interface SyncErrorMetadata {\n [key: string]: unknown;\n message?: string;\n stack?: string | undefined;\n cause?: unknown;\n segment?: string;\n}\n\n/**\n * Re-throws a caught error as a RouterError with the given error code.\n * If the error is already a RouterError, re-codes a COPY of it — never the\n * caught instance itself. Otherwise wraps it with wrapSyncError metadata.\n *\n * Never mutate an error this function does not own (#1606): the three cached\n * rejection errors are module-level singletons, and a guard that merely awaits\n * a navigation rejecting with one of them propagates it here — `setCode` on the\n * caught instance would rewrite that singleton's code for every later consumer\n * in the process (SSR: across requests). The copy is built with the original\n * code and re-coded via its own `setCode`, so the message keeps the exact\n * setCode semantics (a standard-code message follows the new code, a custom one\n * is preserved); `toJSON()` carries `segment` / `path` / custom fields over.\n * The allocation is fine here: this is the guard-refusal path, whose common\n * arm (a guard returning `false`) already allocates a fresh RouterError.\n */\nexport function rethrowAsRouterError(\n error: unknown,\n errorCode: string,\n segment: string,\n): never {\n if (error instanceof RouterError) {\n const { code, message, ...meta } = error.toJSON();\n\n const copy = new RouterError(code as string, {\n ...meta,\n message: message as string,\n });\n\n copy.setCode(errorCode);\n copy.stack = error.stack ?? \"\";\n\n throw freezeThrownError(copy);\n }\n\n throw freezeThrownError(\n new RouterError(errorCode, wrapSyncError(error, segment)),\n );\n}\n\n// Own-enumerable keys that must never be copied from a thrown object onto the\n// RouterError metadata:\n// - `code` / `segment` / `path` are reserved — the RouterError constructor\n// throws a TypeError on them (#39).\n// - `then` would make the RouterError itself thenable, so a consumer that\n// awaits it (or passes it through Promise.resolve / returns it from an async\n// function) would have it assimilated as a Promise instead of treated as a\n// plain rejection reason (#947).\nconst reservedRouterErrorProps = new Set([\"code\", \"segment\", \"path\", \"then\"]);\n\n/**\n * Wraps a synchronously thrown value into structured error metadata.\n *\n * This helper extracts useful debugging information from various thrown values:\n * - Error instances: extracts message, stack, and cause (ES2022+)\n * - Plain objects: spreads properties into metadata\n * - Primitives (string, number, etc.): returns minimal metadata\n *\n * @param thrown - The value caught in a try-catch block\n * @param segment - Route segment name (for lifecycle hooks)\n * @returns Structured error metadata for RouterError\n */\nexport function wrapSyncError(\n thrown: unknown,\n segment: string,\n): SyncErrorMetadata {\n const base: SyncErrorMetadata = { segment };\n\n // Handle Error instances - extract all useful properties\n if (thrown instanceof Error) {\n // ⚑ ONE read per slot (#2085). `thrown` is whatever application code threw,\n // so each of these is a call into it — and the conditional below needs its\n // answer twice, for the test and for the value.\n //\n // ⚠ All three are hoisted, not just the one the conditional needs, and the\n // reason is ORDER rather than counting: reading `cause` above the literal\n // moves the caller's getters out of the sequence a plain literal gives them.\n // The order is what an instrumented Error observes, so it is part of the\n // door's behaviour and is pinned with the counts.\n const message = thrown.message;\n const stack = thrown.stack;\n // Error.cause requires ES2022+ - safely access if present\n const cause = \"cause\" in thrown ? thrown.cause : undefined;\n\n return {\n ...base,\n message,\n stack,\n ...(cause !== undefined && { cause }),\n };\n }\n\n // Handle plain objects - spread properties into metadata, filtering reserved props\n if (thrown && typeof thrown === \"object\") {\n const filtered: Record<string, unknown> = {};\n\n for (const [key, value] of objectEntries(thrown)) {\n // Skip reserved / hazardous keys: #39 (constructor TypeError on code/\n // segment/path) and #947 (`then` would make the error thenable).\n // ⚑ `UNSAFE_KEY` skipped, the same decision the state channels take and for\n // the same reason (#1852). A `RouterError` is a CONTAINER core builds and\n // hands out: it reaches the `navigate()` rejection, every plugin's\n // `onTransitionError`, and `JSON.stringify` through `toJSON`. Measured with\n // the key carried, a guard throwing a plain object put `\"__proto__\"` into\n // the serialized error — so an error log shipped to a server became a\n // prototype-swap primitive after `JSON.parse` there.\n if (key !== UNSAFE_KEY && !reservedRouterErrorProps.has(key)) {\n // ⚑ `putField` (#1852): every key here is chosen by the application\n // code that THREW this object, so the name is entirely outside core's\n // control. Measured with an ambient accessor under one of them, the\n // navigation rejected with a `TypeError` from this line — a diagnostic\n // becoming the thing that fails — and with a setter the field's value\n // was replaced by the accessor's in the reported metadata.\n putField(filtered, key, value);\n }\n }\n\n return { ...base, ...filtered };\n }\n\n // Primitives (string, number, boolean, null, undefined, symbol, bigint)\n // Return base metadata only - the primitive value isn't useful as metadata\n return base;\n}\n\n/**\n * Settle a guard's Promise into the pipeline's terms: `false` and a rejection\n * both become the phase's `RouterError`, nothing else escapes.\n *\n * Lives here rather than with the interpreter because that IS this module's\n * concern — turning a guard's refusal into the right error (#1607). The\n * interpreter only decides WHERE it stopped.\n */\nexport async function resolveAsyncGuard(\n promise: Promise<boolean>,\n errorCode: string,\n segment: string,\n): Promise<void> {\n let result: boolean;\n\n try {\n result = await promise;\n } catch (error: unknown) {\n handleGuardError(error, errorCode, segment);\n\n return; // unreachable — handleGuardError returns never\n }\n\n if (!result) {\n throw freezeThrownError(new RouterError(errorCode, { segment }));\n }\n}\n","import { handleGuardError, resolveAsyncGuard } from \"./errorHandling\";\nimport { errorCodes } from \"../../../constants\";\nimport { RouterError, freezeThrownError } from \"../../../RouterError\";\n\nimport type { GuardFn, State } from \"../../../types\";\n\n/**\n * The guard pipeline as ONE program and TWO interpreters (RFC two-pipelines,\n * cut B): three fixed phases — deactivate, leave, activate — walked by a\n * cursor of two numbers. `runFrom` stops at the first Promise and reports\n * where; `resumeFrom` settles it and hands the cursor back, so switching\n * pipelines is one act — give up the cursor.\n *\n * The step exists for ONE cancellation check, and `runStep` carries what holds\n * it. The two positions at the end of the async walk deliberately have none:\n * `finishAsyncNavigation` already asks there, so a check in either spot reds\n * nothing.\n *\n * ⚑ Every function here takes its parameters FLAT rather than in a bag, and\n * carries a `NOSONAR` for it: the walk runs per guard step on the #307 path,\n * and an options object would be an allocation per call.\n */\n\nconst PHASE_DEACTIVATE = 0;\nconst PHASE_LEAVE = 1;\nconst PHASE_ACTIVATE = 2;\n\n/**\n * Where the synchronous interpreter stopped, and what it is waiting on.\n *\n * Allocated ONLY when a step actually suspends — a fully synchronous navigation\n * never builds one, which is what keeps the hot path where it was. `index` is\n * already the cursor for the NEXT step, so resuming needs no off-by-one\n * reasoning.\n */\ninterface Suspension {\n phase: number;\n index: number;\n pending: Promise<unknown>;\n}\n\n/**\n * ONE step of the program: the cancellation check, then the step's own work.\n *\n * Returns a {@link Suspension} when the step handed back a Promise, `undefined`\n * when it finished synchronously, and throws when it refused — a guard returning\n * `false`, a guard throwing, or the navigation having been cancelled.\n */\nfunction runStep( // NOSONAR -- see the note on flat parameters at the top of this file\n phase: number,\n index: number,\n segments: string[],\n guards: Map<string, GuardFn>,\n errorCode: string,\n toState: State,\n fromState: State | undefined,\n signal: AbortSignal | undefined,\n isLive: () => boolean,\n emitLeaveApprove: () => Promise<void> | undefined,\n): Suspension | undefined {\n // THE cancellation check — the only one in this file, and the head of the step\n // is where it has to stay: the leave phase returns before any guard lookup, so\n // a fence one statement lower stops covering it. Measured both ways — removing\n // it reds 13 tests, moving it below the leave branch reds 3 — and nothing\n // guards this position or the one downstream (`routerFSM.ts` records why the\n // LEAVE_APPROVE edge's `when` could never fire).\n //\n // Without it a superseded navigation walks on and sends its LEAVE_APPROVE, and\n // when it gets there FIRST (the named test's order — the reverse one diverges\n // in nothing) the machine enters LEAVE_APPROVED under the DEAD payload.\n // Measured on both channels: `onTransitionLeaveApprove` fires from the edge's\n // action, so it names the dead destination while the survivor's approval is a\n // table no-op; `subscribeLeave` is dispatched by the pipeline, so it hears\n // BOTH, dead first — the half `useRouteExit` sits on in six adapters. The\n // committed state is not the casualty; what diverges is what subscribers were\n // told, the #1609 silent-commit shape one event earlier, and the test naming\n // the symptom is `leave-approve-integration.test.ts` \"the LEAVE_APPROVE event\n // names the SURVIVING navigation\".\n if (!isLive()) {\n throw freezeThrownError(new RouterError(errorCodes.TRANSITION_CANCELLED));\n }\n\n if (phase === PHASE_LEAVE) {\n const leaveResult = emitLeaveApprove();\n\n return leaveResult === undefined\n ? undefined\n : { phase, index: index + 1, pending: leaveResult };\n }\n\n const segment = segments[index];\n const guardFn = guards.get(segment);\n\n if (!guardFn) {\n return undefined;\n }\n\n // Stryker disable next-line BooleanLiteral: equivalent — guardResult is unconditionally reassigned by guardFn() on the next line, or the catch calls handleGuardError() (returns never), so the init value is never read.\n let guardResult: boolean | Promise<boolean> = false;\n\n try {\n guardResult = guardFn(toState, fromState, signal);\n } catch (error: unknown) {\n handleGuardError(error, errorCode, segment);\n }\n\n if (guardResult instanceof Promise) {\n return { phase, index: index + 1, pending: guardResult };\n }\n\n if (!guardResult) {\n throw freezeThrownError(new RouterError(errorCode, { segment }));\n }\n\n return undefined;\n}\n\n/**\n * One PHASE of the program: its short-circuit, then its steps.\n *\n * Split from the walk so neither function carries the whole program — phase-level\n * and step-level nesting in one body exceeds the complexity budget and trips\n * `unicorn/no-break-in-nested-loop`, which asks for exactly this (\"move this\n * nested loop into a function instead\").\n */\nfunction runPhase( // NOSONAR -- see the note on flat parameters at the top of this file\n phase: number,\n from: number,\n deactivateGuards: Map<string, GuardFn>,\n activateGuards: Map<string, GuardFn>,\n toDeactivate: string[],\n toActivate: string[],\n shouldDeactivate: boolean,\n shouldActivate: boolean,\n toState: State,\n fromState: State | undefined,\n signal: AbortSignal | undefined,\n isLive: () => boolean,\n emitLeaveApprove: () => Promise<void> | undefined,\n): Suspension | undefined {\n const isLeave = phase === PHASE_LEAVE;\n const isDeactivate = phase === PHASE_DEACTIVATE;\n\n // Both halves are contracts and both DECIDE: `shouldDeactivate` carries\n // `!opts.forceDeactivate`, `shouldActivate` carries `toState.name !==\n // UNKNOWN_ROUTE`. Reaching either takes a guard on the OTHER phase — a false\n // short-circuit also disarms `hasGuards`, so the navigation takes cut A and\n // never arrives here — which is why the tier fired this 29 times on an EMPTY\n // segment list until `phase-short-circuits.test.ts` wrote the two cells.\n if (!isLeave && !(isDeactivate ? shouldDeactivate : shouldActivate)) {\n return undefined;\n }\n\n const segments = isDeactivate ? toDeactivate : toActivate;\n // The leave phase is a single step, so one loop shape covers all three phases\n // — which is what gives the cancellation check exactly one home.\n const stepCount = isLeave ? 1 : segments.length;\n const guards = isDeactivate ? deactivateGuards : activateGuards;\n const errorCode = isDeactivate\n ? errorCodes.CANNOT_DEACTIVATE\n : errorCodes.CANNOT_ACTIVATE;\n\n for (let index = from; index < stepCount; index++) {\n const suspension = runStep(\n phase,\n index,\n segments,\n guards,\n errorCode,\n toState,\n fromState,\n signal,\n isLive,\n emitLeaveApprove,\n );\n\n if (suspension !== undefined) {\n return suspension;\n }\n }\n\n return undefined;\n}\n\n/**\n * The synchronous interpreter: walk the program from `startPhase`/`startIndex`\n * until a step suspends (returns the {@link Suspension}) or the program ends\n * (returns `undefined` — done).\n *\n * Parameters are flat rather than a context object on purpose: a bag would be an\n * allocation on every guarded navigation, and this is the #307 hot path.\n */\nfunction runFrom( // NOSONAR -- see the note on flat parameters at the top of this file\n deactivateGuards: Map<string, GuardFn>,\n activateGuards: Map<string, GuardFn>,\n toDeactivate: string[],\n toActivate: string[],\n shouldDeactivate: boolean,\n shouldActivate: boolean,\n toState: State,\n fromState: State | undefined,\n signal: AbortSignal | undefined,\n isLive: () => boolean,\n emitLeaveApprove: () => Promise<void> | undefined,\n startPhase: number,\n startIndex: number,\n): Suspension | undefined {\n for (let phase = startPhase; phase <= PHASE_ACTIVATE; phase++) {\n const suspension = runPhase(\n phase,\n phase === startPhase ? startIndex : 0,\n deactivateGuards,\n activateGuards,\n toDeactivate,\n toActivate,\n shouldDeactivate,\n shouldActivate,\n toState,\n fromState,\n signal,\n isLive,\n emitLeaveApprove,\n );\n\n if (suspension !== undefined) {\n return suspension;\n }\n }\n\n return undefined;\n}\n\n/**\n * The asynchronous interpreter: settle what the synchronous one stopped on, then\n * hand the cursor straight back to it.\n *\n * It owns no phase logic of its own, and that is the point — everything it knows\n * about the program it learns from the cursor, so the two interpreters cannot\n * drift apart the way the three continuation functions did.\n */\nasync function resumeFrom( // NOSONAR -- see the note on flat parameters at the top of this file\n suspension: Suspension,\n deactivateGuards: Map<string, GuardFn>,\n activateGuards: Map<string, GuardFn>,\n toDeactivate: string[],\n toActivate: string[],\n shouldDeactivate: boolean,\n shouldActivate: boolean,\n toState: State,\n fromState: State | undefined,\n signal: AbortSignal | undefined,\n isLive: () => boolean,\n emitLeaveApprove: () => Promise<void> | undefined,\n): Promise<void> {\n let at: Suspension | undefined = suspension;\n\n while (at !== undefined) {\n if (at.phase === PHASE_LEAVE) {\n await at.pending;\n } else {\n const isDeactivate = at.phase === PHASE_DEACTIVATE;\n\n await resolveAsyncGuard(\n at.pending as Promise<boolean>,\n isDeactivate\n ? errorCodes.CANNOT_DEACTIVATE\n : errorCodes.CANNOT_ACTIVATE,\n (isDeactivate ? toDeactivate : toActivate)[at.index - 1],\n );\n }\n\n at = runFrom(\n deactivateGuards,\n activateGuards,\n toDeactivate,\n toActivate,\n shouldDeactivate,\n shouldActivate,\n toState,\n fromState,\n signal,\n isLive,\n emitLeaveApprove,\n at.phase,\n at.index,\n );\n }\n}\n\n/**\n * Runs the guard program. Returns `undefined` when it completed synchronously,\n * or the Promise that finishes it otherwise — one contract for all three\n * phases, in one place.\n */\nexport function executeGuardPipeline( // NOSONAR -- see the note on flat parameters at the top of this file\n deactivateGuards: Map<string, GuardFn>,\n activateGuards: Map<string, GuardFn>,\n toDeactivate: string[],\n toActivate: string[],\n shouldDeactivate: boolean,\n shouldActivate: boolean,\n toState: State,\n fromState: State | undefined,\n signal: AbortSignal,\n isLive: () => boolean,\n emitLeaveApprove: () => Promise<void> | undefined,\n): Promise<void> | undefined {\n const suspension = runFrom(\n deactivateGuards,\n activateGuards,\n toDeactivate,\n toActivate,\n shouldDeactivate,\n shouldActivate,\n toState,\n fromState,\n signal,\n isLive,\n emitLeaveApprove,\n PHASE_DEACTIVATE,\n 0,\n );\n\n return suspension === undefined\n ? undefined\n : resumeFrom(\n suspension,\n deactivateGuards,\n activateGuards,\n toDeactivate,\n toActivate,\n shouldDeactivate,\n shouldActivate,\n toState,\n fromState,\n signal,\n isLive,\n emitLeaveApprove,\n );\n}\n","import { completeTransition } from \"./completeTransition\";\nimport { asCancellation, routeTransitionError } from \"./errorHandling\";\nimport { executeGuardPipeline } from \"./guardPhase\";\nimport { errorCodes, constants } from \"../../../constants\";\nimport { adoptNavigationOptions, ownFlag, ownSignal } from \"../../../helpers\";\nimport { RouterError, freezeThrownError } from \"../../../RouterError\";\nimport { getTransitionPath } from \"../../../transitionPath\";\nimport {\n CACHED_SAME_STATES_ERROR,\n CACHED_SAME_STATES_REJECTION,\n} from \"../constants\";\n\nimport type { GuardFn, NavigationOptions, State } from \"../../../types\";\nimport type {\n AnnouncedPlan,\n NavigationContext,\n NavigationDependencies,\n NavigationPlan,\n} from \"../types\";\n\n/**\n * The orchestration of one navigation, end to end — free functions, no `this`:\n * these were methods until the per-navigation state they shared was named\n * (#1607), and the namespace above is left with what it actually is, the entry\n * points, their fire-and-forget checkpoint and the DI bag.\n *\n * ⚑ The navigation's `AbortController` is a field of the PLAN (#1684) — the\n * machine adopts the plan on `NAVIGATE`, so the `CANCEL` action reaches it\n * through `ctx.inflight` rather than through an injected effect. Inside this\n * file it is still passed by hand where the plan is not enough:\n * `finishAsyncNavigation` takes it as a parameter, from both of its callers.\n */\n\n// Captured at module load, same reason as its six siblings across `src`: the\n// freeze below owns a published guarantee — the bag every plugin hook receives —\n// and a guarantee is only as strong as the intrinsic it reads WHEN IT RUNS\n// (#1970 / #1971).\nconst freeze = Object.freeze;\n\n// Write-once placeholders for `NavigationPlan`'s pass-2 fields. Module-level so\n// building a plan allocates nothing beyond the plan itself; never mutated —\n// `planPhases` overwrites the SLOTS, it does not write through them.\nconst NO_SEGMENTS: string[] = Object.freeze([]) as unknown as string[];\nconst NO_GUARDS = new Map<string, GuardFn>();\n\n/**\n * The substituting half of the UNKNOWN_ROUTE force, taking the DECISION rather\n * than making it (#1817).\n *\n * Deciding here too — `fromState?.name === UNKNOWN_ROUTE && !opts.replace` —\n * reads the caller's `replace` one read BEFORE the entry hoists it. On a\n * drifting getter the two disagree and the forced replace is LOST: the\n * predicate saw `true` (\"the caller already asked, nothing to substitute\") while\n * the meta recorded `false`, so a URL plugin reading `transition.replace` pushes\n * a history entry where this mechanism exists to replace one.\n *\n * ⚑ It cannot be dropped in favour of the `replace` that already travels to\n * `beginTransition` as its own positional slot — the reading that filed #1979.\n * That slot is CORE-internal; the BAG is what leaves core, because a plugin's\n * `onTransitionSuccess(toState, fromState, opts)` receives it as the third\n * argument and a history plugin reads `replace` from THERE. Measured by\n * neutralising the substitution: `navigation/navigate/unknown-route.test.ts`\n * reds two cells, both asserting `objectContaining({ replace: true })` on the\n * hook. `opts.replace` is read exactly once in `src`, ABOVE this call, so the\n * static picture really does look redundant — the consumer is out of frame.\n */\nfunction substituteForcedReplace(\n opts: NavigationOptions,\n forced: boolean,\n): NavigationOptions {\n if (!forced) {\n return opts;\n }\n\n // ⚑ Both arms now hand back a frozen record CORE owns, because the entry door\n // ran above this (#1962): the pass-through returns core's own copy, and the\n // spread below re-freezes what the spread thawed. Uniformity is the point —\n // this substitution must not be the reason one arc's hook gets a writable bag.\n //\n // ⚠ `dropUnsafeKey` is gone from here, and its absence is not a relaxation:\n // the source is core's copy, which the entry door already stripped, so a\n // spread of it cannot carry an own `\"__proto__\"` (#1957). Re-dropping would be\n // an unfalsifiable no-op — the shape #1957's own guard exists to refuse.\n //\n // ⚑ The early return is also what keeps `forced` from being a SELECTOR\n // parameter now that the two arms differ in shape — `sonarjs/no-selector-parameter`\n // fires on the ternary form, and the project's own rule says a boolean\n // argument is a hypothesis to justify.\n return freeze({ ...opts, replace: true });\n}\n\nfunction isSameNavigation(\n fromState: State | undefined,\n reload: boolean | undefined,\n force: boolean | undefined,\n toState: State,\n): boolean {\n return !!fromState && !reload && !force && fromState.path === toState.path;\n}\n\n/**\n * Does any segment this phase walks carry a guard?\n *\n * The empty-Map check first, so a router with no guards at all pays one load —\n * and a router that HAS guards pays one `Map.has` per segment on the path,\n * against the ~40 ns + 482 B an AbortController costs when the answer is no.\n */\nfunction hasGuardOnPath(\n guards: Map<string, GuardFn>,\n segments: string[],\n): boolean {\n if (guards.size === 0) {\n return false;\n }\n\n for (const segment of segments) {\n if (guards.has(segment)) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Materialise the navigation's `AbortController` — the ONE door, so a\n * cancellation that arrived before the first consumer is not lost (#1706).\n *\n * The controller is allocated lazily and by three different consumers (the\n * guard fork, the guard-free leave arc, and a leave listener registered from\n * inside the announce), and `CANCEL` can land in front of any of them. It has\n * nowhere to abort then, so it records `cancelReason` on the navigation\n * instead and this function replays it onto the controller the moment one\n * exists. Without the replay the fresh controller is born UNABORTED, the\n * liveness fence reads `!signal.aborted` as \"still live\", and the guards of a\n * navigation that already announced its `TRANSITION_CANCEL` run anyway.\n *\n * ⚠ Idempotent, and what that buys is the allocation COUNT, not the signal's\n * identity: `handleNoGuardsLeave` opens one before the announce and asks again\n * after it, and it is the SECOND ask whose signal the leave listeners are\n * handed. Both allocation pins (`controller-allocation`, `guards-off-path`)\n * read 2 without this early return.\n */\nfunction openController(plan: NavigationContext): AbortController {\n const existing = plan.controller;\n\n if (existing !== undefined) {\n return existing;\n }\n\n const controller = new AbortController();\n\n // Born aborted when the machine already cancelled this navigation, with the\n // reason the `CANCEL` action recorded — the sender's own where there is one\n // (an external `opts.signal`, #943), a synthesised `TRANSITION_CANCELLED`\n // where there is not (`stop()` / `dispose()` / supersede).\n if (plan.cancelReason !== undefined) {\n controller.abort(plan.cancelReason);\n }\n\n plan.controller = controller;\n\n return controller;\n}\n\n/**\n * SECOND of the bridge's two moments (#1690); the FIRST is the `NAVIGATE`\n * edge's action (#1724). Two because `hasGuards` is unknowable when the edge\n * fires, and registering unconditionally there measured **+23…30 %**.\n *\n * The terms differ in kind. **No signal** is a fast path, NOT protection:\n * `bridgeExternalSignal` refuses it itself (dropping it reds nothing), it\n * only saves the call — ~1 %. **`cancelReason`**: the machine already\n * cancelled, so a listener installed now is one nothing would remove (4 leaked;\n * `cancellability-scope-1716`). **No guards**: nothing could abort\n * (`bridge-only-when-the-band-can-abort-1690`). No fourth term — \"is a bridge\n * standing?\" is `bridgeExternalSignal`'s (a duplicate drops coverage), and\n * \"already aborted?\" is asked once, inline right after the announce (#1704) —\n * not here.\n */\nfunction bridgeLateIfOnlyGuardsCanAbort(\n deps: NavigationDependencies,\n plan: NavigationPlan,\n): void {\n if (\n plan.externalSignal === undefined ||\n plan.cancelReason !== undefined ||\n !plan.hasGuards\n ) {\n return;\n }\n\n deps.bridgeExternalSignal(plan);\n}\n\n/**\n * Pass 1 of the shared prologue: reserve the navigation, then announce it.\n *\n * ⚑ **Every value from the caller's `opts` arrives as a PARAMETER**, so the\n * order of those reads is not this function's problem: `opts` is accessor- or\n * Proxy-backed by contract, and the window between the previous navigation's\n * cancel and this one's announce is where such a call is dangerous — a getter\n * starting a nested navigation there parks the machine back in the band, and\n * this `send(NAVIGATE)` then takes a self-loop the table documents as never\n * traversed. `entry-reads-opts-once.test.ts` keeps the reads at the entry.\n *\n * The listener counts behind `suspendable` sit between the cancel and the\n * announce, and only the LOWER boundary is measured: reading them after the\n * announce reds `bridge-implies-suspendable-1705`, reading them above the cancel\n * reds nothing — and a probe for the case that ordering guards (a\n * `subscribeLeave` registered from `onTransitionCancel`) does not separate the\n * two either, since the leave emit asks `hasLeaveListeners()` again in its own\n * moment. After the cancel is the conservative order, not a proven one.\n *\n * ⚑ Hence TEN parameters, and a `NOSONAR` for S107 — the trade `guardPhase`\n * states once at the top of its file, measured here rather than assumed. Six of\n * the ten are those entry reads, and folding them into a bag costs an object\n * literal per navigation on cut A plus bytecode on the `beginTransition` +\n * `planPhases` pair — the pair #1728's model gates the arc on, and it sits close\n * enough to that model's edge that the bag spends real headroom without buying\n * anything. Re-measure with esbuild + `node --no-opt --no-lazy --print-bytecode`\n * before trading on the margin; `--no-lazy` is required or neither function is\n * compiled at all.\n */\nfunction beginTransition( // NOSONAR -- S107: see the note on flat parameters above\n deps: NavigationDependencies,\n toState: State,\n fromState: State | undefined,\n opts: NavigationOptions,\n externalSignal: AbortSignal | undefined,\n abortedAtEntry: AbortSignal | undefined,\n reload: boolean | undefined,\n replace: boolean | undefined,\n redirected: boolean | undefined,\n forceDeactivate: boolean,\n): AnnouncedPlan {\n abortPreviousNavigation(deps, abortedAtEntry);\n\n // Two of the three ways application code can run between announce and settle\n // (the third, `hasGuards`, is why the bridge registers in two moments — see\n // `bridgeLateIfOnlyGuardsCanAbort`).\n const announceOrLeaveCanAbort =\n deps.hasLeaveListeners() || deps.hasPreCommitListeners();\n\n // `suspendable` is true only when a synchronous supersede is reachable — an\n // external `opts.signal`, `subscribeLeave` listeners, or a pre-commit plugin\n // listener; a navigate with none of these is uncancellable, which is what\n // keeps the #307 hot path perf-neutral.\n //\n // ⚠ The `externalSignal` term also carries \"bridge registered ⟹ suspendable\"\n // (#1705): both registration sites require it, so the two predicates stay\n // coincident. Dropping it leaks nothing and no behavioural test can see it —\n // `bridge-implies-suspendable-1705.test.ts` asserts the disjunct by name and\n // holds the registration sites as a closed set.\n const plan: NavigationPlan = {\n toState,\n fromState,\n opts,\n suspendable: externalSignal !== undefined || announceOrLeaveCanAbort,\n forceDeactivate,\n // Write-once placeholders — pass 2 fills them (see `NavigationPlan`).\n toDeactivate: NO_SEGMENTS,\n toActivate: NO_SEGMENTS,\n intersection: \"\",\n canDeactivateFunctions: NO_GUARDS,\n canActivateFunctions: NO_GUARDS,\n shouldDeactivate: false,\n shouldActivate: false,\n hasGuards: false,\n // The OPTIONAL fields are DECLARED, not left to their first write: the plan\n // is per-navigation, and a write to an absent property transitions its\n // hidden class, making every downstream `plan.*` read polymorphic. Measured\n // on the runner: leaving them out cost 10–27% across every `navigate/*`\n // benchmark and 42% on `navigate/sync-baseline` alone (#1693).\n // `plan-born-in-final-shape.test.ts` pins it.\n controller: undefined,\n cancelReason: undefined,\n detachExternalBridge: undefined,\n externalSignal,\n reload,\n replace,\n redirected,\n };\n\n // The plan IS the payload, and the machine adopts it as this navigation's\n // identity — there is no epoch to read back afterwards (#1648).\n //\n // ⚑ **The send's OUTCOME is checked, and it asks \"where is the machine NOW\",\n // not \"did the edge fire\".** `FSM.send` reports the state after the update,\n // the action AND the listeners, so two things land in the branch below: a\n // navigation the table never adopted (BORN DEAD — a `stop()` from a\n // `forwardState` interceptor leaves the machine in IDLE), and one whose own\n // announce moved the machine (9 of the tier's 16 arrivals). Neither changes\n // the OUTCOME, so what the check removes is WORK — and since #1706 that work\n // is an ALLOCATION, not a walk: `CANCEL` records `cancelReason`,\n // `openController` is born aborted from it, and the one-term `runStep` fence\n // refuses. Measured with the seam neutered on the guard arc, external abort\n // from `onTransitionStart`: 1 controller, guards `[]` — against 0 and `[]`\n // with the seam. Nothing is closed here\n // either way: the second group opened a cancellability scope, and the `CANCEL`\n // that moved the machine closed it on the way out. Counted, never traced, by\n // `born-dead-navigation-1648.test.ts` — both of its `describe`s.\n if (!deps.startTransition(plan)) {\n throw freezeThrownError(new RouterError(errorCodes.TRANSITION_CANCELLED));\n }\n\n // The one place this brand is minted, and it is below the send.\n return plan as AnnouncedPlan;\n}\n\n/**\n * The uninterruptible navigation, end to end (RFC §5.1, cut A).\n *\n * Reached only when `!hasGuards && !suspendable`, so the cancellation machinery\n * is not *skipped* here — it is ABSENT: no `AbortController`, no liveness\n * closure, no commit-gate, and the bare `State` return says the rest.\n *\n * ⚠ Only the controller ANNOUNCES itself (`guards-off-path` counts\n * allocations). A liveness closure or a gate added here changes no outcome and\n * reds nothing — both are tautologies on this arc — so the body is scanned\n * instead: `immediate-arc-stays-empty.test.ts` asserts it is exactly these two\n * calls.\n *\n * `LEAVE_APPROVE` stays: every navigation makes that transition, and with no\n * leave listeners there is nothing to await behind it.\n */\nfunction completeImmediate(\n deps: NavigationDependencies,\n plan: NavigationPlan,\n): State {\n deps.sendLeaveApprove(plan);\n\n return completeTransition(deps, plan);\n}\n\n/**\n * Pass 2: work out the shape of the transition, now that it is announced.\n *\n * It takes an `AnnouncedPlan` because it must run AFTER the announce — a\n * `TRANSITION_START` listener may still register a guard, and the maps read\n * below have to reflect that. Passing a plan that has not been announced does\n * not compile; the brand is minted in one place, below the send.\n */\nfunction planPhases(deps: NavigationDependencies, plan: AnnouncedPlan): void {\n const [canDeactivateFunctions, canActivateFunctions] =\n deps.getLifecycleFunctions();\n\n const { toDeactivate, toActivate, intersection } = getTransitionPath(\n plan.toState,\n plan.fromState,\n (name) => deps.getMetaForState(name),\n );\n\n plan.canDeactivateFunctions = canDeactivateFunctions;\n plan.canActivateFunctions = canActivateFunctions;\n plan.toDeactivate = toDeactivate;\n plan.toActivate = toActivate;\n plan.intersection = intersection;\n plan.shouldDeactivate =\n !!plan.fromState && !plan.forceDeactivate && toDeactivate.length > 0;\n plan.shouldActivate =\n plan.toState.name !== constants.UNKNOWN_ROUTE && toActivate.length > 0;\n // The guards of THIS transition, not of the router: asking the Maps for their\n // size armed the cancellation machinery for every public navigation whenever\n // the app had one `canActivate` anywhere — measured at +643 B and +97.7 ns\n // per navigation that never touches the guarded route, with the method and\n // the after-figures in `core/CLAUDE.md`. Both terms mirror the\n // interpreter — a phase whose short-circuit is false runs no step — which is\n // what keeps this a gate rather than a second policy. `guards-off-path`\n // counts controllers on both halves.\n plan.hasGuards =\n (plan.shouldDeactivate &&\n hasGuardOnPath(canDeactivateFunctions, toDeactivate)) ||\n (plan.shouldActivate && hasGuardOnPath(canActivateFunctions, toActivate));\n}\n\nexport function executeNavigation(\n deps: NavigationDependencies,\n toState: State,\n opts: NavigationOptions,\n): State | Promise<State> {\n let fromState: State | undefined;\n // Hoisted because the catch below cannot see `plan`, and the error path needs\n // the navigation's IDENTITY — to name its FAIL with, and to ask the machine\n // whether it is still the one in flight. `undefined` means \"no navigation was\n // ever announced\", which is precisely when a FAIL must not name one, and it is\n // the marker a supersession token carries as `myId === 0` (#1648/#1664).\n let nav: NavigationPlan | undefined;\n\n try {\n fromState = deps.getState();\n\n // Read FIRST and once: everything that happens to the signal after this\n // point happened INSIDE the navigation, and must reach it through the\n // machine rather than through a throw.\n // ⚑ OWN, and this is the one flag whose gate cannot be the copy (#2132) —\n // it is read above that call, off the caller's own object.\n const externalSignal = ownSignal(opts);\n const abortedAtEntry =\n externalSignal?.aborted === true ? externalSignal : undefined;\n\n // ⚑ THE ENTRY DOOR (#1962). One walk of the caller's bag, into a frozen\n // record core owns — and everything below reads that, never the caller's\n // object again.\n //\n // What it closes is not the aliasing but the INCONSISTENCY: without it a\n // plugin receives the application's own literal on three arcs and a copy on\n // two, discriminated by whether the caller passed a `signal` — which the\n // plugin never sees. Annotating the hook argument, the cheapest way to pass\n // a flag from one hook to the next, then writes into the application's\n // object or into a private copy depending on an unrelated detail of the\n // call.\n //\n // ⚑ It sits BELOW the signal read and ABOVE every other one, and that order\n // is what keeps #1817's guarantee intact rather than merely unbroken: the\n // walk skips `signal` (already read, once), and the six flags hoisted below\n // now come off core's own data, so the caller's accessors are entered\n // exactly once per key — including keys core never names.\n opts = adoptNavigationOptions(opts);\n\n // ⚑ EVERY flag is hoisted here, above both readers (#1719 / #1817), on the\n // stated ground that `opts` is accessor- or Proxy-backed BY CONTRACT, so\n // each read is a call into application code. The entry is the only site that\n // reads any of them, and `entry-reads-opts-once.test.ts` derives that set.\n //\n // ⚠ What this is and is not. It finishes a rule the codebase already states\n // about itself, and adds no check. It is NOT a safety fix: making two reads\n // disagree needs a getter that answers differently between them, and the\n // accessor-backed bags that occur in practice (Vue `reactive()`, Svelte\n // `$props()`) are pass-through and stable. The drift is the INSTRUMENT the\n // guard cells use to make the read count observable, exactly as a stable\n // `toString` measures nothing in the route-name family.\n //\n // ⚠ `force` is read unconditionally rather than lazily, so its per-arc count\n // is not uniformly lower than a lazy read's. One read per field at the entry\n // is the rule this file is held to, and reproducing the pre-check's own\n // short-circuit here would put the hoist back in the business of knowing what\n // that pre-check does internally — the coupling #1719 undoes.\n // ⚑ OWN, not inherited (#2132). These come off core's own copy, but that\n // copy carries `Object.prototype`, so a slot the caller never wrote is\n // otherwise answered by an ambient one — measured, an ambient\n // `forceDeactivate` made core ignore a route's refusal to deactivate.\n const reload = ownFlag(opts, \"reload\");\n const force = ownFlag(opts, \"force\");\n const replaceRequested = ownFlag(opts, \"replace\");\n const redirected = ownFlag(opts, \"redirected\");\n const forceDeactivate = ownFlag(opts, \"forceDeactivate\") === true;\n\n const forcedReplace =\n fromState?.name === constants.UNKNOWN_ROUTE && !replaceRequested;\n const replace = forcedReplace || replaceRequested;\n\n opts = substituteForcedReplace(opts, forcedReplace);\n\n if (isSameNavigation(fromState, reload, force, toState)) {\n deps.emitTransitionError(toState, fromState, CACHED_SAME_STATES_ERROR);\n\n return CACHED_SAME_STATES_REJECTION;\n }\n\n const plan = beginTransition(\n deps,\n toState,\n fromState,\n opts,\n externalSignal,\n abortedAtEntry,\n reload,\n replace,\n redirected,\n forceDeactivate,\n );\n\n nav = plan;\n\n // The scope is open — the machine adopted this plan — and the first thing it\n // does is ADOPT what the caller's signal already says (#1704).\n //\n // `addEventListener` never fires retroactively, so a bridge is only as good\n // as the instant it was registered, and a window stands in front of the\n // earliest one: AFTER the entry snapshot, `forceReplaceFromUnknown`'s spread\n // and `isSameNavigation` read the caller's object again — reads that ARE\n // application code when `opts` is accessor- or Proxy-backed. An abort raised\n // there is what `entry-abort-boundary` pins as announced-then-cancelled;\n // without this line it left the bridge on a dead signal and the machine\n // untold.\n //\n // ⚠ Here and not beside the registration it protects: `CANCEL` is declared\n // in the band only, so asking earlier is a table no-op — moving this above\n // `startTransition` reds the same set of tests as deleting it does.\n //\n // ⚠ **Not a helper.** As a function these four lines measured\n // `navigate/sync-baseline` 8.2720 → 9.5540 ms on the runner, and 8.2728\n // inlined again — the same pair #1728 later explained as a step of\n // CodSpeed's valgrind MODEL rather than of the clock, where the same arc has\n // master faster natively. Keep the inline shape, but weigh any step this arc\n // reports against a wall-clock run first.\n const abortedSignal = plan.externalSignal;\n\n if (abortedSignal?.aborted === true) {\n deps.cancelNavigation(abortedSignal.reason);\n }\n\n // Post-`startTransition` supersession is caught by `when: mayCommit` on the\n // COMPLETE edge, asked inside `completeTransition`: a `stop()`/`dispose()`\n // from the TRANSITION_START listener leaves the FSM in IDLE/DISPOSED, where\n // COMPLETE is not declared at all, and an aborted external `opts.signal` is\n // read straight off the commit payload. (Async supersession is additionally\n // caught in `finishAsyncNavigation` / the guard pipeline's `isLive`;\n // a reentrant navigate() is banned — REENTRANT_NAVIGATION.)\n\n planPhases(deps, plan);\n\n bridgeLateIfOnlyGuardsCanAbort(deps, plan);\n\n // Cut A (RFC §5.1). `immediate` is the RFC's four-term predicate\n // written in the terms that already exist: `suspendable` IS\n // `signal || leaveListeners || preCommitListeners`, so the whole thing is\n // `!hasGuards && !suspendable`. Nothing can interrupt such a navigation and\n // nothing in it can suspend, so the machinery for both is not skipped —\n // `#completeImmediate` does not contain it.\n //\n // Decided HERE and not at the entry point, deliberately: `hasGuards` is\n // only knowable after `startTransition`, because a `TRANSITION_START`\n // listener may still register a guard. Hoisting the read would change\n // behaviour, not just shape.\n if (!plan.hasGuards && !plan.suspendable) {\n return completeImmediate(deps, plan);\n }\n\n const {\n canDeactivateFunctions,\n canActivateFunctions,\n toDeactivate,\n toActivate,\n shouldDeactivate,\n shouldActivate,\n hasGuards,\n } = plan;\n\n const confirmedToState = toState;\n\n if (!hasGuards) {\n const asyncLeave = handleNoGuardsLeave(deps, plan);\n\n if (asyncLeave !== undefined) {\n return asyncLeave;\n }\n }\n\n // NOT equivalent, and a `Stryker disable: equivalent` here would be\n // wrong: with `if (true)` the guard branch runs AFTER\n // `#handleNoGuardsLeave` already emitted LEAVE_APPROVE, so\n // `emitLeaveApproveCallback` dispatches every `subscribeLeave` listener a\n // SECOND time (measured: 2 calls, 1 expected). The whole suite stayed\n // green because the shared `createTestRouter` fixture carries definition\n // guards, so no test ever reached this branch with `hasGuards` false —\n // `guard-phase-emit-leave-approve.test.ts` now builds a guard-free router\n // and kills both mutants.\n // eslint-disable-next-line unicorn/prefer-else-if -- two exhaustive `if`s read clearer here than an else-if; merging cascades into no-negated-condition / no-unnecessary-condition in this hot guard-setup branch\n if (hasGuards) {\n // Onto the PLAN, which the machine adopted on NAVIGATE — so the CANCEL\n // action finds it by identity for as long as this navigation is the one\n // in flight, on the synchronous arc exactly as on the asynchronous one.\n //\n // ⚑ Through `openController`, not `new` (#1706). A `CANCEL` can already\n // have landed — `bridgeLateIfOnlyGuardsCanAbort`, earlier in this\n // function, sends one itself when the caller's signal was aborted in the announce\n // window — and it had no controller to abort. Born unaborted, this one\n // would satisfy `isLive` below and the walk would ask the guards of\n // a navigation whose `TRANSITION_CANCEL` has already been emitted.\n const controller = openController(plan);\n // The liveness the guard walk is fenced on, and the same predicate\n // `finishAsyncNavigation` asks (#1687). ONE term, because `aborted` is what\n // decides on every source: a supersede, a `stop()`/`dispose()` and the\n // caller's own `opts.signal` all reach the controller through FSM\n // `CANCEL`. `CANCEL` carries no `update`, so `ctx.inflight` still names\n // this navigation on the way out (#1671) and the machine lands in `READY`\n // — which is why identity cannot answer here and `aborted` must.\n //\n // ⚑ Scope, deliberately: this stops GUARDS. The `subscribeLeave` dispatch\n // is NOT fenced and must not be — a leave listener is documented to fire\n // when the FSM enters `LEAVE_APPROVED` (INVARIANTS `subscribeLeave`, the\n // section's own statement) and to receive a signal that aborts on\n // cancellation (row 8), i.e. being called\n // with `aborted === true` is its contract, not a leak.\n const isLive = () => !controller.signal.aborted;\n\n const signal = controller.signal;\n\n const emitLeaveApproveCallback = (): Promise<void> | undefined => {\n deps.sendLeaveApprove(plan);\n\n if (deps.hasLeaveListeners()) {\n return deps.awaitLeaveListeners(confirmedToState, fromState, signal);\n }\n\n return undefined;\n };\n\n const guardCompletion = executeGuardPipeline(\n canDeactivateFunctions,\n canActivateFunctions,\n toDeactivate,\n toActivate,\n shouldDeactivate,\n shouldActivate,\n toState,\n fromState,\n signal,\n isLive,\n emitLeaveApproveCallback,\n );\n\n if (guardCompletion !== undefined) {\n // The plan IS the `NavigationContext` (a superset of it), so the\n // second literal a separate context would need is unnecessary — one\n // bag per navigation.\n return finishAsyncNavigation(deps, guardCompletion, plan, controller);\n }\n\n if (!isLive()) {\n throw freezeThrownError(\n new RouterError(errorCodes.TRANSITION_CANCELLED),\n );\n }\n\n // ⚑ Nothing to release: the controller dies with the plan, and the plan\n // stays in `ctx.inflight` until the machine clears it — on `COMPLETE`, or\n // with both state cells at `dispose()`.\n }\n\n // The commit gate is `when: mayCommit` on the COMPLETE edge, and closing the\n // cancellability scope is that same edge's action — both happen one frame\n // deeper, inside the call below.\n const finalState = completeTransition(deps, plan);\n\n // A bare `State`, not `Promise.resolve(state)` — the RETURN TYPE is what\n // announces \"this navigation already settled, synchronously\", and a type\n // cannot announce it early: no window exists in which a value reads\n // \"resolved\" while `completeTransition` may still throw, because the value\n // only exists once that call returns. The Promise wrap moves up to the facade, which owes callers\n // `Promise<State>`; the allocation is the same one, one frame higher.\n return finalState;\n } catch (error) {\n const outcome = handleNavigateError(deps, error, { nav, fromState });\n\n // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- preserve original throw shape from guards or transition pipeline\n return Promise.reject(outcome);\n }\n}\n\nasync function finishAsyncNavigation(\n deps: NavigationDependencies,\n guardCompletion: Promise<void>,\n nav: NavigationContext,\n controller: AbortController,\n): Promise<State> {\n // The same predicate as the guard walk's fence — see the comment above the\n // other `isLive`, in `executeNavigation`'s guard branch.\n const isLive = () => !controller.signal.aborted;\n\n // The SAME object the bridge was attached to, not a re-read of\n // `nav.opts.signal` (#1690). With a Proxy-backed `opts` the second read can\n // hand back a different signal, and this pre-check would then be asking a\n // stranger whether this navigation was cancelled.\n const externalSignal = nav.externalSignal;\n let onInternalAbort: (() => void) | undefined;\n let succeeded = false;\n let failureReason: unknown;\n\n // #1018: race the guard completion against the controller's abort so a\n // non-cooperative guard whose Promise never settles (and ignores `signal`)\n // cannot wedge navigate() forever. `abortRace` RESOLVES on abort, so the\n // post-race `isLive()` check below throws TRANSITION_CANCELLED — the same\n // path that already handles a guard which swallows the abort and resolves\n // `true`. `stop()`/`dispose()`/supersede all abort the controller. Mirrors\n // the leave-path protection `settleLeavePromises` (#663/#673).\n const abortRace = new Promise<void>((resolve) => {\n if (controller.signal.aborted) {\n resolve();\n\n return;\n }\n\n onInternalAbort = () => {\n resolve();\n };\n\n controller.signal.addEventListener(\"abort\", onInternalAbort, {\n once: true,\n });\n });\n\n // Consume `guardCompletion`: on most arcs `Promise.race` below already\n // subscribes to it, but when the pre-check throws the race is never built and\n // a late settlement has no handler at all — under Node 22+ that kills the\n // process. Reached by a guard that aborts the signal itself before returning;\n // pinned by `late-guard-rejection-is-consumed`.\n guardCompletion.catch(() => {\n /* settlement consumed — the race already decided the navigation */\n });\n\n try {\n // Reaching here with an aborted signal means the bridge — live since the\n // `NAVIGATE` action — already fired and the machine has already cancelled.\n // Refusing right away carries the caller's own `reason` into the rejection\n // instead of waiting for the post-race check to synthesize one.\n if (externalSignal?.aborted) {\n throw freezeThrownError(\n new RouterError(errorCodes.TRANSITION_CANCELLED, {\n reason: externalSignal.reason,\n }),\n );\n }\n\n // The race settles two ways and BOTH have to consult liveness. Only this\n // arm did: a rejection throws straight past the check into the `catch`,\n // which is where #1609 lived.\n await Promise.race([guardCompletion, abortRace]);\n\n if (!isLive()) {\n throw freezeThrownError(new RouterError(errorCodes.TRANSITION_CANCELLED));\n }\n\n const state = completeTransition(deps, nav);\n\n succeeded = true;\n\n return state;\n } catch (error) {\n // Liveness on the OTHER arm of the race (#1609). A guard that rejects one\n // or two microtasks before a superseding `navigate()` would otherwise\n // report FAIL for a navigation cancelled several microtasks earlier —\n // `routeTransitionError` filters by error CODE, and `CANNOT_ACTIVATE` is\n // not `TRANSITION_CANCELLED`. Into a `READY` FSM that is observability\n // noise (a terminal event for a dead navigation); into the LIVE one it is\n // silent corruption, because `TRANSITION_STARTED --FAIL--> READY` is a real\n // edge, so the superseding navigation's later `COMPLETE` became a table\n // no-op: state committed, `TRANSITION_SUCCESS` never emitted, subscribers\n // never notified.\n const outcome = isLive() ? error : asCancellation(error);\n\n failureReason = outcome;\n\n routeTransitionError(deps, outcome, nav.fromState, nav);\n\n throw outcome;\n // NB: emptying the `finally` is NOT equivalent (#1684). This IS the abort\n // for every failure the machine never hears about — a rejecting guard, a\n // leave listener that threw — so removing it leaves a captured leave signal\n // unaborted on a navigation that failed.\n } finally {\n // Detach the abort-race listener before the release below aborts the\n // controller below, so the cleanup abort cannot re-fire it. `undefined`\n // only when the controller was already aborted at setup (the early-resolve\n // branch above registered no listener).\n if (onInternalAbort) {\n controller.signal.removeEventListener(\"abort\", onInternalAbort);\n }\n\n // Success drops the controller without aborting (the subscribeLeave signal\n // must stay unaborted, #722); cancel/error aborts it with the originating\n // reason so captured signals expose the real cause via `signal.reason`\n // (#943). `abort()` is idempotent, so a controller the CANCEL action has\n // already aborted keeps its first — also meaningful — reason.\n if (!succeeded) {\n controller.abort(failureReason);\n }\n }\n}\n\n/**\n * What a failed navigation still knows about itself.\n *\n * These two are mirrored OUTSIDE the `try` on purpose — `plan` is declared\n * inside it, so a throw from the prologue (before `beginTransition` returns)\n * leaves nothing else to report with, and `nav === undefined` is precisely how\n * the handler asks \"did this navigation ever announce itself\". Grouping them is\n * what keeps the handler at five parameters instead of six, and it is FREE:\n * the object is built in the `catch`, so the happy path allocates nothing.\n */\ninterface AttemptedNavigation {\n /**\n * The navigation itself — the plan object the machine adopted, which is what\n * a FAIL names (#1648). `undefined` when the throw came from the prologue,\n * before the navigation was announced.\n */\n readonly nav: NavigationContext | undefined;\n readonly fromState: State | undefined;\n}\n\n/**\n * Settle a failed navigation on the SYNCHRONOUS arc: abort its controller,\n * report it while the machine is still in the transition, and hand back the\n * outcome the caller's promise should carry.\n *\n * `nav === undefined` means `TRANSITION_START` never fired, so there is no\n * announced navigation for a terminal event to pair with — the error goes back\n * untouched, and the report can NAME the navigation without an assertion\n * (#1648).\n *\n * **Has the FSM left my transition?** is the precondition for sending `FAIL`,\n * and `isActive()` is the looser approximation that gets a case wrong: a\n * listener running `stop()` and then a `start()` PARKED in an async interceptor\n * leaves the FSM in `STARTING`, where `isActive()` is true again — for a\n * different lifecycle, whose start the stale `FAIL` would kill\n * (`STARTING --FAIL--> IDLE`). Measured: swapping the predicate for\n * `isActive()` reds 115 tests. The predicate stands alone — an identity term\n * beside it decides nothing, in any configuration (#1734).\n *\n * The restatement below is `asCancellation` — see `./errorHandling` for what it\n * measures.\n */\nfunction handleNavigateError(\n deps: NavigationDependencies,\n error: unknown,\n attempted: AttemptedNavigation,\n): unknown {\n const { nav } = attempted;\n\n // The failing navigation's OWN controller, read off the plan so both arcs are\n // covered (#1684); `undefined` is the born-dead case — nothing was announced,\n // so there is nothing to abort. Pinned by `leave-signal-cancellation` (#722).\n nav?.controller?.abort(error);\n\n if (nav !== undefined) {\n const outcome = deps.isTransitioning() ? error : asCancellation(error);\n\n routeTransitionError(deps, outcome, attempted.fromState, nav);\n\n return outcome;\n }\n\n return error;\n}\n\n/**\n * The leave phase for a navigation with no guards, but with something that can\n * suspend it: a `subscribeLeave` listener, a pre-commit plugin listener, or the\n * caller's own `signal`.\n */\nfunction handleNoGuardsLeave(\n deps: NavigationDependencies,\n plan: NavigationPlan,\n): Promise<State> | undefined {\n const { toState, fromState } = plan;\n\n // Opened BEFORE the announce (#1697), so the signal handed to the listeners\n // below does not depend on the replay in `openController` having worked.\n //\n // ⚠ No BEHAVIOUR test holds this line — a cancel from inside the announce\n // reaches a controller opened afterwards too, since #1706 makes it born\n // aborted, so deleting the open reds no behaviour test at all. **The coverage\n // gate holds it**: this is the only site that opens twice, so without it\n // `openController`'s idempotence arm (the `existing !== undefined` early\n // return) is unreachable and the 100 % thresholds fail. Measure BRANCHES,\n // not just red tests,\n // before calling it redundant. The GATE around it is load-bearing separately:\n // cut A allocates nothing, and this is where that is decided\n // (`controller-allocation`, `guards-off-path`).\n if (deps.hasLeaveListeners()) {\n openController(plan);\n }\n\n deps.sendLeaveApprove(plan);\n\n if (deps.hasLeaveListeners()) {\n // Reuse the one the announce may already have aborted. This allocates only\n // for a listener REGISTERED from inside the announce, which the gate above\n // could not have counted — and since #1706 that late allocation is no\n // longer a hole either: `openController` replays a `cancelReason` the\n // announce recorded, so such a listener is handed an already-aborted signal\n // like every other one.\n const controller = openController(plan);\n\n let leaveResult: Promise<void> | undefined;\n\n // This catches the SYNCHRONOUS throw only — a leave listener that throws\n // inline, before any Promise exists (three tests in\n // `leave-signal-cancellation.test.ts` fail without it: the sync-throw\n // rejection, its `signal.reason` (#943), and the reentrant-navigate abort).\n // A rejection of the RETURNED Promise is deliberately not caught here: it is\n // awaited in `finishAsyncNavigation`, which routes it through\n // `routeTransitionError` with the controller released as cancelled. Sonar's\n // S4822 flags the shape (a promise-returning call inside `try`) and cannot\n // see that split.\n try {\n // NOSONAR -- S4822: the try is for the sync listener throw, never for the Promise\n leaveResult = deps.awaitLeaveListeners(\n toState,\n fromState,\n controller.signal,\n );\n } catch (error) {\n // A sync listener threw — the navigation fails; abort the leave signal\n // with the thrown value so a listener that captured the signal sees the\n // real cause via `signal.reason`, not a generic AbortError (#943).\n // `handleNavigateError` would abort it too, with the same value and\n // idempotently; kept here because this is where the reason is KNOWN to be\n // the thrown one, and the three pins in `leave-signal-cancellation.test.ts`\n // are about that, not about the unwind.\n controller.abort(error);\n\n throw error;\n }\n\n if (leaveResult !== undefined) {\n return finishAsyncNavigation(deps, leaveResult, plan, controller);\n }\n\n // Sync listeners settled. Nothing to release: the controller belongs to the\n // plan and the plan is still the navigation in flight, which is what lets a\n // cancellation between here and the commit still reach it (#1684). Success\n // never aborts it, so the captured leave signal stays live (#722).\n return undefined;\n }\n\n // No leave listeners: nothing synchronous could have superseded this\n // navigation during the LEAVE_APPROVE emit (reentrant navigate is banned).\n return undefined;\n}\n\nexport function abortPreviousNavigation(\n deps: NavigationDependencies,\n abortedAtEntry: AbortSignal | undefined,\n): void {\n if (deps.isTransitioning()) {\n deps.logger.warn(\n \"router.navigate\",\n \"Concurrent navigation detected on shared router instance. \" +\n \"For SSR, use cloneRouter() to create isolated instance per request.\",\n );\n // The FSM CANCEL action aborts the previous controller — no\n // direct controller.abort here (RFC navigation-cancellation-unification §5).\n deps.cancelNavigation();\n }\n\n // Refuse without announcing ONLY for a signal that was already dead when the\n // router received it: nothing has been announced, so nothing is owed a\n // terminal event (`external-signal-bridge-1684`). An abort that lands LATER —\n // from an `opts` getter, during the prologue — is a cancellation OF this\n // navigation, and the caller is owed the pair: the announce, then `CANCEL`.\n // The entry read is what tells the two apart; asking `aborted` here instead\n // conflated them, and four of the five `opts` fields silently took the\n // refusal path.\n if (abortedAtEntry !== undefined) {\n throw freezeThrownError(\n new RouterError(errorCodes.TRANSITION_CANCELLED, {\n reason: abortedAtEntry.reason,\n }),\n );\n }\n}\n","import { abortPreviousNavigation } from \"./executeNavigation\";\nimport {\n EMPTY_PARAMS,\n EMPTY_SEARCH,\n constants,\n errorCodes,\n} from \"../../../constants\";\nimport { RouterError, freezeThrownError } from \"../../../RouterError\";\nimport { nameToIDs } from \"../../../transitionPath\";\n\nimport type { NavigationOptions, State, TransitionMeta } from \"../../../types\";\nimport type { NavigationDependencies } from \"../types\";\n\n// ⚑ Captured at module load — same rule as `helpers.ts`, `materialize.ts` and\n// `completeTransition.ts`. Measured with the global neutered: the 404's\n// `segments` and its `deactivated` array came back unfrozen. The module-level\n// constants below are frozen at import, before any application code runs, so\n// they need no capture; these four call sites do.\nconst freeze = Object.freeze;\n\n/**\n * The one commit primitive that is NOT a transition.\n *\n * It hand-builds an `UNKNOWN_ROUTE` state, freezes it and emits only\n * `TRANSITION_SUCCESS` — no plan, no controller, no activation guards, since\n * there is nothing to activate at `UNKNOWN_ROUTE`. What it DOES share with a\n * navigation is supersession, the departing route's `canDeactivate` (#1643) and\n * the machine: the commit rides a `SYSTEM_COMMIT` edge (#1641).\n */\n\nconst FROZEN_ACTIVATED: string[] = Object.freeze([\n constants.UNKNOWN_ROUTE,\n]) as unknown as string[];\nconst FROZEN_REPLACE_OPTS: NavigationOptions = Object.freeze({ replace: true });\n\nfunction commitNotFound(\n deps: NavigationDependencies,\n path: string,\n consultDeactivation: boolean,\n): State {\n // Supersede first, exactly as `navigate` does before its guards run, so a\n // refused 404 cancels an in-flight navigation the same way a refused\n // `navigate` does. No caller signal to pass — this primitive takes none.\n //\n // ⚑ The liveness gate #1186 put here is now the ask half of `systemCommit`\n // below, which makes it structural (a caller cannot forget it) while the\n // explicit throw stays, because a table refusal is silent and the contract\n // promises an error. It is not the same predicate: `SYSTEM_COMMIT` is\n // declared from READY ALONE, so it also refuses in STARTING — a call from\n // inside an async `start` interceptor throws rather than committing.\n abortPreviousNavigation(deps, undefined);\n\n const fromState = deps.getState();\n const deactivated: string[] = fromState\n ? nameToIDs(fromState.name).toReversed()\n : [];\n\n freeze(deactivated);\n\n const segments: TransitionMeta[\"segments\"] = {\n deactivated,\n activated: FROZEN_ACTIVATED,\n intersection: \"\",\n };\n\n freeze(segments);\n\n const transitionMeta: TransitionMeta = {\n phase: \"activating\",\n ...(fromState && { from: fromState.name }),\n reason: \"success\",\n replace: true,\n segments,\n };\n\n freeze(transitionMeta);\n\n const state: State = {\n name: constants.UNKNOWN_ROUTE,\n params: EMPTY_PARAMS,\n search: EMPTY_SEARCH,\n path,\n transition: transitionMeta,\n context: {},\n };\n\n freeze(state);\n\n // ⚑ The 404 is a DEPARTURE, and one the user can refuse (#1643). Only the\n // ACTIVATION half of \"bypasses the pipeline\" follows from being a 404; the\n // gap was reachable by pressing Back, since the shipped URL plugins call this\n // from their popstate handlers — an editor with unsaved changes lost them to\n // any URL that no longer matched a route, with the app's own confirm dialog\n // never shown. Refusing is what those handlers ALREADY expect: the\n // matched-route branch beside this one rejects and its `catch` rolls the URL\n // back, and the strict-mode branch throws for the same purpose.\n //\n // Which of the two doors called decides this, and the doors are NAMED rather\n // than told apart by a flag (#1981): `revalidateToNotFound` does not consult,\n // for the rule stated at its three call sites in `replace()`'s revalidation\n // (#1652) — a tree swap is not a departure the user chose, so the arms where\n // the route vanished, where the consulted guard refused, and where (#1753)\n // application code removed the route inside the window all skip it alike.\n // ⚠ This does NOT make a later caller of `commitRevalidated` safe — that\n // function IS a revalidation door and anything it calls inherits its lane.\n // What the split removes is the shared door where an ARGUMENT chose the lane:\n // there is no longer a call one can write correctly-looking and get the other\n // behaviour from.\n if (\n consultDeactivation &&\n fromState !== undefined &&\n !deps.canDeactivateCurrent(deactivated, state, fromState)\n ) {\n const error = new RouterError(errorCodes.CANNOT_DEACTIVATE, {\n path,\n message: `[router.navigateToNotFound] a canDeactivate guard on \"${fromState.name}\" refused to leave for ${path}`,\n });\n\n // Report before throwing, so an observer sees the refusal on the same\n // channel a blocked `navigate` uses — the popstate handler's own `catch`\n // is written against \"navigate() already emitted $$error\".\n deps.emitTransitionError(undefined, fromState, error);\n\n throw freezeThrownError(error);\n }\n\n // Write AND announce as one table fact — this is the second of the two\n // ruptures of \"every channel that changes committed state goes through the\n // machine\" (plan §6.1), and closing it is what finally lets `fromState` of\n // the next navigation equal `toState` of the previous one (plan §12.3).\n // The door returns what it COMMITTED, which is not `state` — it copies both\n // channels AND `context`, and builds its own shell (#1792). Returning the argument would hand application code a\n // state the router never holds: value-equal, and not `===` `getState()`.\n return deps.systemCommit(state, fromState, FROZEN_REPLACE_OPTS);\n}\n\n/**\n * A user-initiated departure to `UNKNOWN_ROUTE`: the current route's\n * `canDeactivate` is consulted and may refuse (#1643).\n */\nexport function navigateToNotFound(\n deps: NavigationDependencies,\n path: string,\n): State {\n return commitNotFound(deps, path, true);\n}\n\n/**\n * The `replace()` revalidation's departure to `UNKNOWN_ROUTE`: `canDeactivate`\n * is NOT consulted, because a tree swap is not a departure the user chose\n * (#1652) and there is no \"stay\" branch to offer — after the swap the old route\n * may not exist, or may live at another path.\n */\nexport function revalidateToNotFound(\n deps: NavigationDependencies,\n path: string,\n): State {\n return commitNotFound(deps, path, false);\n}\n","import {\n CACHED_NOT_STARTED_REJECTION,\n CACHED_PRE_BOOT_COMMIT_REJECTION,\n CACHED_ROUTE_NOT_FOUND_ERROR,\n CACHED_ROUTE_NOT_FOUND_REJECTION,\n isExpectedRejection,\n PRE_SUPPRESSED,\n} from \"./constants\";\nimport { executeNavigation } from \"./transition/executeNavigation\";\nimport {\n navigateToNotFound,\n revalidateToNotFound,\n} from \"./transition/navigateToNotFound\";\nimport { findMisChanneledKey, misChanneledKeyMessage } from \"../../channels\";\nimport {\n constants,\n DEFAULT_TRANSITION,\n EMPTY_PARAMS,\n EMPTY_SEARCH,\n errorCodes,\n} from \"../../constants\";\nimport { adoptForeignBag, freezeStateShell } from \"../../helpers\";\nimport { RouterError } from \"../../RouterError\";\n\nimport type { NavigationDependencies } from \"./types\";\nimport type {\n NavigationOptions,\n Params,\n SearchParams,\n State,\n} from \"../../types\";\n\n/**\n * The navigation entry points, their fire-and-forget checkpoint and the DI bag —\n * and NO per-navigation state: the controller is a field of the plan the machine\n * carries (#1684), the supersession token is the plan's identity (#1664).\n *\n * Performance: navigate() runs optimistically synchronously — guards run inline\n * until one returns a Promise — so the common case pays for no Promise and no\n * AbortController.\n */\nexport class NavigationNamespace {\n #deps!: NavigationDependencies;\n #onSuppressed!: (error: unknown) => void;\n\n // Depth of the PRE-START window — see `#prepare`. Interim form of what\n // becomes a machine state in the state-ownership plan (§10, phase 4).\n #preparingDepth = 0;\n\n // =========================================================================\n // Dependency injection\n // =========================================================================\n\n setDependencies(deps: NavigationDependencies): void {\n this.#deps = deps;\n // Built once here rather than per call: the closure needs THIS router's\n // logger, so it cannot be static, and `setDependencies` is a pure\n // assignment that runs exactly once at wiring (#1331).\n this.#onSuppressed = (error: unknown): void => {\n if (isExpectedRejection(error)) {\n return;\n }\n\n deps.logger.error(\n \"router.navigate\",\n \"Unexpected navigation error\",\n error,\n );\n };\n }\n\n // =========================================================================\n // Instance methods\n // =========================================================================\n\n navigate(\n name: string,\n params: Params,\n search: SearchParams | undefined,\n opts: NavigationOptions,\n ): State | Promise<State> {\n return this.#settle(this.#navigate(name, params, search, opts));\n }\n\n /**\n * Navigate to a fully-built `State` directly, skipping `buildNavigateState`\n * (forwardState + buildPath + meta lookup). Used by URL plugins after they\n * have already produced a `State` from a browser-initiated event via\n * `api.matchPath(url)` — see issue #525.\n *\n * Semantics vs. `navigate(name, params, opts)`:\n * - `forwardState` is NOT re-applied. matchPath already runs it; reapplying\n * is redundant in the idempotent case and can race in the dynamic case.\n * - The URL is NOT re-printed. The caller's `state.path` is used as-is — so\n * `trailingSlash:\"preserve\"` matchedState paths flow through unchanged\n * (closes #525 Q2); the URL the user navigated to is the source of truth\n * for this code path.\n * - All other pipeline steps run unchanged: SAME_STATES check, FSM\n * transition, guards, `subscribeLeave`, `completeTransition`,\n * plugin lifecycle hooks.\n */\n navigateToState(\n state: State,\n opts: NavigationOptions,\n ): State | Promise<State> {\n return this.#settle(this.#navigateToState(state, opts));\n }\n\n navigateToDefault(opts: NavigationOptions): State | Promise<State> {\n return this.#settle(this.#navigateToDefault(opts));\n }\n\n navigateToNotFound(path: string): State {\n return navigateToNotFound(this.#deps, path);\n }\n\n revalidateToNotFound(path: string): State {\n return revalidateToNotFound(this.#deps, path);\n }\n\n /**\n * Is a navigation between its entry point and its announce — the PRE-START\n * window (#1610)?\n *\n * Exactly two stretches raise `#preparingDepth`, and both run application code\n * before `TRANSITION_START`: `buildNavigateState` (the `forwardState`\n * interceptor chain, a dynamic `forwardTo`, a route's `encodeParams` — but\n * NOT `decodeParams`, which serves the URL→state direction and prepares no\n * navigation) and `resolveDefault` (each of the\n * three default options may be a dependency-resolved callback). Everything\n * else before the announce is core's own code, except `abortPreviousNavigation`'s\n * `CANCEL` emit, which the dispatch depth covers already. That depth is why\n * the reentrancy ban missed this window: it keys off the emitter, and there\n * has been no emit yet.\n *\n * A DEPTH raised inline, not a boolean set by a wrapper: the wrapper allocated\n * a closure per navigation on the #307 hot path. Each site lowers it in a\n * `finally`, because the early refusals exit through this window too\n * (`ROUTE_NOT_FOUND`, `WRONG_CHANNEL`, any throw from user code) and a marker\n * left raised deadlocks the router against its own next call.\n *\n * Read by `Router.#assertNotReentrant` beside `EventBus.isProcessing()`;\n * between them they span every window where application code runs inside a\n * navigation core has not finished setting up. A GUARD is deliberately in\n * neither: it runs after the announce, so a guard-redirect stays a supersede.\n *\n * Interim form — pre-start becoming a STATE of the machine absorbs it, and a\n * nested navigation is then an ordinary supersede needing no marker.\n */\n isPreparing(): boolean {\n return this.#preparingDepth > 0;\n }\n\n /**\n * The producer's own fire-and-forget guarantee (#721): whatever leaves a\n * public method here is safe to drop on the floor.\n *\n * ONE checkpoint per public method, deliberately — not a `.catch()` at each of\n * the six return sites. A forgotten site is invisible until it leaks, which is\n * the bug this replaces; a single choke point cannot be forgotten. And the\n * discriminator is the returned VALUE's identity, not a flag the facade reads\n * afterwards: `PRE_SUPPRESSED` promises already carry a module-load handler, so\n * re-suppressing them buys nothing and costs a derived promise.\n *\n * A synchronously-returned `State` needs nothing at all — there is no rejection\n * to suppress, which is precisely what makes `lastSyncResolved` unnecessary:\n * the TYPE carries what a `lastSyncResolved` flag would announce.\n */\n #settle(result: State | Promise<State>): State | Promise<State> {\n if (result instanceof Promise && !PRE_SUPPRESSED.has(result)) {\n result.catch(this.#onSuppressed);\n }\n\n return result;\n }\n\n /**\n * `navigate`'s body, minus the fire-and-forget checkpoint.\n *\n * Split out because `navigateToDefault` delegates HERE, not to the public\n * method: routing it through `navigate` would run `#settle` twice per default\n * navigation and attach a second, pointless `.catch()` — an extra derived\n * promise on a path that has none today.\n */\n #navigate(\n name: string,\n params: Params,\n search: SearchParams | undefined,\n opts: NavigationOptions,\n ): State | Promise<State> {\n const deps = this.#deps;\n\n // Fast-path sync rejections: cached error + cached Promise.reject.\n // No allocations, no throw/catch overhead; `#settle` recognises the\n // singleton by identity and skips its `.catch()`.\n if (!deps.canNavigate()) {\n // The boot WINDOW gets its own sentence (#1647): the bare code reads as\n // \"you forgot to call start()\" while the caller is inside start() — a\n // start interceptor navigating before `next()`. Selected here rather than\n // refused on the facade, because the refusal is already decided one line\n // up and the boot itself never lands in this branch.\n return deps.isStarting()\n ? CACHED_PRE_BOOT_COMMIT_REJECTION\n : CACHED_NOT_STARTED_REJECTION;\n }\n\n let toState: State | undefined;\n\n // PRE-START window (#1610) — user code runs in here, before any emit. See\n // `isPreparing`. The `finally` is load-bearing: the early refusals exit\n // through it too.\n this.#preparingDepth++;\n\n try {\n toState = deps.buildNavigateState(name, params, search);\n } catch (error) {\n // Live region, covered: the always-on channel guard (#1572) throws from\n // core's own `buildNavigateState`, so this is not a validator-only path.\n // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- preserve original throw shape from user-provided buildNavigateState\n return Promise.reject(error);\n } finally {\n this.#preparingDepth--;\n }\n\n if (!toState) {\n deps.emitTransitionError(\n undefined,\n deps.getState(),\n CACHED_ROUTE_NOT_FOUND_ERROR,\n );\n\n return CACHED_ROUTE_NOT_FOUND_REJECTION;\n }\n\n return executeNavigation(this.#deps, toState, opts);\n }\n\n #navigateToState(\n state: State,\n opts: NavigationOptions,\n ): State | Promise<State> {\n const deps = this.#deps;\n\n if (!deps.canNavigate()) {\n // Boot-window sentence, as in `#navigate` above (#1647).\n return deps.isStarting()\n ? CACHED_PRE_BOOT_COMMIT_REJECTION\n : CACHED_NOT_STARTED_REJECTION;\n }\n\n // ⚑ ONE read of the caller's name (#2085), taken above every consumer.\n // The State is a plugin's, so each read is a call into application code —\n // and three consumers below need the answer: the existence check, the P3\n // channel registry, and the copy that COMMITS. Asking them separately lets\n // the door commit a route it never checked.\n const name = state.name;\n\n // Reject states whose route no longer exists (e.g. the route tree was\n // mutated between matchPath and navigateToState). UNKNOWN_ROUTE is\n // structurally legal — it is the navigateToNotFound output shape.\n if (name !== constants.UNKNOWN_ROUTE && !deps.hasRoute(name)) {\n const err = new RouterError(errorCodes.ROUTE_NOT_FOUND, {\n routeName: name,\n });\n\n deps.emitTransitionError(undefined, deps.getState(), err);\n\n // A FRESH reject (it carries `routeName`), so it is deliberately NOT in\n // `PRE_SUPPRESSED` and `#settle` will attach its `.catch()`. Adding it to\n // that set — the modern shape of the mistake that caused #721 — would skip\n // suppression on a promise nobody else handles and leak it.\n return Promise.reject(err);\n }\n\n // Channel guard, position P3 (#1572). `navigateToState` is the ONE producer\n // that takes a ready-made `State` instead of a `params` argument, so the\n // predicate reads `state.params ∩ queryNames(state.name)`. What it commits\n // becomes `getState()`, so a pre-M2 layout would be silent corruption: the\n // key sits in `state.params` and never reaches `state.path`.\n //\n // Costs nothing on healthy flows — a state produced by core (`matchPath`,\n // `makeState`) is channel-correct by construction, so the predicate is\n // empty on every popstate / memory-restore / SSR-hydration commit. `start()`\n // commits THROUGH here (`RouterLifecycleNamespace`), which is why the guard\n // lives in the namespace rather than on the plugin-API door.\n //\n // Rejects rather than throwing, mirroring the ROUTE_NOT_FOUND guard above:\n // this method returns `Promise<State>` and its URL-plugin callers invoke it\n // from popstate handlers, where a new synchronous throw would be a change\n // of failure shape rather than a new failure.\n const misChanneled = findMisChanneledKey(\n state.params,\n deps.getQueryParams(name),\n );\n\n if (misChanneled !== undefined) {\n const err = new RouterError(errorCodes.WRONG_CHANNEL, {\n routeName: name,\n message: `[router.navigateToState] ${misChanneledKeyMessage(\n name,\n misChanneled,\n \"`state.params`\",\n )}`,\n });\n\n deps.emitTransitionError(undefined, deps.getState(), err);\n\n return Promise.reject(err);\n }\n\n // States from `matchPath` are deeply frozen (`freezeStateShell`).\n // `completeTransition` mutates `toState.transition` and `context` is\n // intentionally extensible for plugin claim writes, so we hand the\n // pipeline a writable shell — the same shape `materializePending` produces,\n // `transition` included: completeTransition OVERWRITES it (#1976).\n //\n // ⚑ Both channels are committed as core's OWN frozen copies (#1792). The\n // argument is a State a PLUGIN built, so its bags belong to the caller: the\n // shell carrying them by reference would leave the committed `state.search`\n // writable through a reference the plugin still holds, so any later mutation\n // lands in the committed state.\n //\n // ⚠ ONE idiom for both, deliberately. Running `params` through\n // `normalizeChannel` and `search` through a spread makes the two channels\n // disagree about symbol-keyed entries — dropped from one, kept in the other.\n // Same call, same answer.\n //\n // ⚠ This does NOT close the read-twice window at this door: the P3 channel\n // guard above reads `state.params` first, and the copy reads it again. A bag\n // that changes between those two reads is outside the guarantee by design —\n // INVARIANTS \"Supported input shapes\" states it. Saying so is the honest\n // form: \"each channel read ONCE\" is not true of this door.\n // ⚠ Wrapped, because the copies READ every value of both bags while this\n // method's contract is to REJECT, never to throw synchronously — the note\n // at the top of this method says a sync throw would change the failure\n // shape for URL-plugin callers invoking it from popstate handlers, and\n // `memory-plugin`'s `#go()` attaches only `.catch()`, so an unwrapped\n // throw escapes into `router.back()`. A bag whose accessor throws is\n // outside the `__proto__` guarantee by design; its failure SHAPE is still\n // this door's promise.\n let writableState: State;\n\n try {\n writableState = this.#copyChannels(state, name);\n } catch (error) {\n // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- the caller's own throw surfaces unchanged; wrapping it would move the origin of an existing failure, which is the rule the channel guard states beside its own read\n return Promise.reject(error);\n }\n\n return executeNavigation(this.#deps, writableState, opts);\n }\n\n /**\n * The three copies {@link NavigationNamespace.navigateToState} commits: both\n * channels, and `context` — which a spread would carry by reference.\n */\n #copyChannels(state: State, name: string): State {\n // ⚠ An annotated `const`, not a bare `return { … }`. The literal's type\n // would otherwise come from this method's return annotation, and\n // `state-freeze-authority`'s constructor census keys on the type AT the\n // literal — so the contextual form makes a State constructor invisible to\n // the scan that exists to count them. Measured: dropping the annotation\n // removes this file from the census with the suite otherwise green.\n const copy: State = {\n name,\n params: adoptForeignBag(state.params, EMPTY_PARAMS) as Params,\n // Carry the query channel through the writable shell (RFC-4 M2 / #1548) —\n // without this, start()'s navigateToState(matchPath(...)) would drop the\n // matched query from the committed state.\n search: adoptForeignBag(state.search, EMPTY_SEARCH) as SearchParams,\n path: state.path,\n context: { ...state.context },\n // Core's own frozen singleton, NOT `state.transition`: the caller's meta\n // is a plugin object this door must not carry by reference (#1792), and\n // it would be overwritten at the commit regardless. Attaching it here is\n // what lets this literal be annotated rather than cast (#1976) — the\n // shape is now identical to `materialize`'s, so the two producers cannot\n // drift apart.\n transition: DEFAULT_TRANSITION,\n };\n\n // Sealed before it leaves, like every other door that hands the pending\n // target to application code (#2144). `context` stays extensible — it is\n // the slot plugin claims write into, and only the SHELL is closed here.\n return freezeStateShell(copy);\n }\n\n #navigateToDefault(opts: NavigationOptions): State | Promise<State> {\n const deps = this.#deps;\n const options = deps.getOptions();\n\n if (!options.defaultRoute) {\n return Promise.reject(\n new RouterError(errorCodes.ROUTE_NOT_FOUND, {\n routeName: \"defaultRoute not configured\",\n }),\n );\n }\n\n let route: string;\n let params: Params;\n let search: SearchParams;\n\n // PRE-START window too, and for the same reason (#1610): `defaultRoute` /\n // `defaultParams` / `defaultSearch` may each be a dependency-resolved\n // CALLBACK, so this runs user code before there is even a route name to\n // navigate to. Sequential with `#navigate`'s window below, never nested.\n this.#preparingDepth++;\n\n try {\n ({ route, params, search } = deps.resolveDefault());\n } catch (error) {\n // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- preserve original throw shape from user-provided resolveDefault callback\n return Promise.reject(error);\n } finally {\n this.#preparingDepth--;\n }\n\n if (!route) {\n return Promise.reject(\n new RouterError(errorCodes.ROUTE_NOT_FOUND, {\n routeName: \"defaultRoute resolved to empty\",\n }),\n );\n }\n\n // ⚑ A route NAME, or nothing (#1876). `defaultRoute` is declared\n // `string | DefaultRouteCallback`, and the callback's return IS type-checked\n // (a `() => 42` fails TS2322) — the hole is an `any`-typed callback, a\n // JavaScript consumer, or a config assembled at runtime. Refusing here keeps\n // four property-key coercions from running on the value, and a value that\n // answered differently between them was admitted as one route and indexed as\n // another. `packages/core/CLAUDE.md` owns the site inventory — it lives in\n // two other files and #1883 will change it.\n //\n // ⚠ REFUSE, not coerce: coercing turns a value that cannot name a route into\n // a successful navigation — measured, a forwarding name does exactly that\n // without this gate.\n if (typeof route !== \"string\") {\n return Promise.reject(\n new RouterError(errorCodes.ROUTE_NOT_FOUND, {\n routeName: \"defaultRoute did not resolve to a route name\",\n }),\n );\n }\n\n // Both channels, never one bag (RFC-4 M2 / #1548): passing the query here is\n // what makes the default route's query defaults independent of the\n // `forwardState` seam, which separates nothing.\n return this.#navigate(route, params, search, opts);\n }\n}\n","// packages/core/src/namespaces/NavigationNamespace/types.ts\n\nimport type {\n GuardFn,\n NavigationOptions,\n AnyOptions,\n Params,\n RouterLogger,\n SearchParams,\n State,\n} from \"../../types\";\n\ndeclare const COMMIT_PERMIT: unique symbol;\n\n/**\n * Proof that the table was asked, and that it was asked FIRST (#1649).\n *\n * ⚠ The post-leave cleanup in `completeTransition` is DESTRUCTIVE — it\n * unregisters the departing route's external `canDeactivate` — so it is\n * legitimate only for a navigation the table has already agreed to commit. That\n * ordering has been got wrong twice: once in review (the #1641 BLOCKER, where\n * the clear ran ahead of any verdict) and once in the #1649 write-up, which\n * prescribed keeping the single surviving ask BELOW the cleanup. Both are the\n * same mistake, and its cost is silent: a refused navigation eats the guard of\n * the route the user stays on, and the app's unsaved-changes dialog stops\n * existing.\n *\n * So the ordering is expressed in the types rather than in a comment. The clear\n * demands a permit, the permit exists only as the ask's return value, and a\n * `const` cannot be read above its own declaration — putting the ask back below\n * the cleanup is `TS2448`, not a test failure. The symbol is never exported, so\n * the brand cannot be forged outside this module.\n *\n * ⚠ It proves the ask HAPPENED, not that it is still true. That is sufficient\n * here and nowhere else by default: nothing runs between the ask and the clear\n * — the cleanup is `Map` bookkeeping over the compiled forms #1649 stored, so\n * no code exists that could invalidate the verdict in between.\n *\n * The permit expresses no SECOND ordering rule, and needs none:\n * `completeTransition` reads no `opts` field at all\n * (`commit-window-empty-1719.test.ts` counts the getter\n * invocations — the story is told at that function and at\n * {@link NavigationContext.reload}). Reuse the permit across anything that CAN\n * run user code and it would be theatre.\n */\nexport interface CommitPermit {\n readonly [COMMIT_PERMIT]: true;\n}\n\n/**\n * The one permit in the process. The brand is erased at runtime, so it carries\n * no information — its whole job is to exist only where the ask has already\n * answered yes. Module-level because it is a token, not a value: a fresh object\n * per navigation would allocate on the #307 hot path for nothing.\n */\nexport const COMMIT_PERMIT_TOKEN = {} as CommitPermit;\n\ndeclare const ANNOUNCED: unique symbol;\n\n/**\n * A plan the machine has ADOPTED — the transition is announced.\n *\n * ⚑ **The brand makes \"after the announce\" an ARGUMENT TYPE, not a rule:**\n * `beginTransition` mints core's only `as AnnouncedPlan` (measured: 1), below\n * its `send`, and `planPhases` demands one — so an unannounced plan does not\n * compile. WHY pass 2 must be late is `planPhases`'s own fact, stated in its\n * doc.\n */\nexport type AnnouncedPlan = NavigationPlan & { readonly [ANNOUNCED]: true };\n\n/**\n * ⚑ **There is no supersession token here, and that is the point (#1664).** A\n * navigation carrying a counter — an `#navigationId` or an\n * `InFlightNavigation.#id` — lets the pipeline ask \"am I still the one in\n * flight?\" while the machine answers the very same question about the very same\n * navigation by comparing the plan it adopted. Two counters for one fact, free\n * to disagree. The plan object is the only identity, and the machine compares it\n * by reference on the `COMPLETE` edge (`mayCommit`).\n *\n * The pipeline asks no IDENTITY question since #1734 (0 readers in `src`) — but\n * it did not collapse to ONE liveness question: the guard fence and\n * `finishAsyncNavigation` ask `!controller.signal.aborted`, while\n * `handleNavigateError` asks `deps.isTransitioning()`. That asymmetry is\n * deliberate and measured, and it is stated where it lives, above\n * `handleNavigateError`.\n */\nexport interface NavigationContext {\n toState: State;\n fromState: State | undefined;\n opts: NavigationOptions;\n toDeactivate: string[];\n toActivate: string[];\n intersection: string;\n canDeactivateFunctions: Map<string, GuardFn>;\n /**\n * ⚑ **The navigation owns its `AbortController`, and the machine owns the\n * navigation (#1684).** Ownership is therefore TRANSITIVE, with no second\n * slot to keep in step: the `CANCEL` action reads `ctx.inflight.controller`\n * off the very object it adopted on `NAVIGATE`, so \"which controller is\n * current\" is derived from identity rather than tracked beside it — the same\n * move that retired the supersession token above.\n *\n * Optional because allocating one is CONDITIONAL and stays that way: the\n * guard branch allocates unconditionally, the guard-free leave arc only when\n * there are leave listeners, and cut A allocates none at all. A `take()`\n * that filled this slot for every navigation is the regression Step 1b of\n * #1588 refused by measurement — still pinned by\n * `controller-allocation.test.ts`, whose four cases include the two that are\n * suspendable and allocate NOTHING (an external signal, a pre-commit\n * listener): those are the cells the forbidden edit reds.\n *\n * Declared on the CONTEXT rather than on {@link NavigationPlan} because both\n * readers are typed by it — `openController`, the one door every allocation\n * goes through, and `handleNavigateError`, which aborts `nav?.controller` for\n * a navigation that may never have been announced. `finishAsyncNavigation` is\n * NOT one of them: it takes the controller as a parameter, from both of its\n * callers.\n */\n controller?: AbortController | undefined;\n /**\n * The reason the `CANCEL` action recorded, or `undefined` while the\n * navigation has not been cancelled (#1706).\n *\n * ⚑ **This is the cancellability SCOPE, and the controller above is only its\n * materialisation.** Because allocation is lazy, \"was this navigation\n * cancelled?\" and \"is its signal aborted?\" are not the same question in the\n * window before the first consumer opens one — and the machine can only\n * answer the first. So `handleCancel` writes here unconditionally and aborts\n * the controller only `?.`; {@link openController} then aborts on birth. A\n * flag rather than an eagerly-allocated controller precisely so cut A and\n * the born-dead arcs keep allocating nothing.\n *\n * Written by the `CANCEL` action, read only by `openController`. Never\n * cleared: a cancelled navigation does not come back.\n */\n cancelReason?: unknown;\n /**\n * Drops the listener that routes the caller's `opts.signal` onto FSM `CANCEL`\n * (#1684). Present only while that bridge is standing, i.e. only for a\n * navigation that was given a signal.\n *\n * A closure rather than the handler itself, so the `signal` it was registered\n * on is captured with it — the closer does not have to re-derive which signal\n * this navigation was carrying.\n *\n * ⚑ **It is the cancellability SCOPE's closer now, and the machine calls it —\n * every time (#1716 + #1724).** The four pipeline settle sites are gone:\n * `CANCEL`, `FAIL` and `COMPLETE` each close the scope from their action, and\n * the one call that survived in `beginTransition` — for the navigation the\n * machine never ADOPTED, which no edge would ever fire for — went with the\n * OPENING moving into the `NAVIGATE` action, since a refused edge now opens\n * nothing. The closure CLEARS THIS FIELD ITSELF, so calling it twice is a\n * no-op — see `bridgeSignal` in `EventBusNamespace`, which owns that protocol\n * and is why \"no bridge standing\" stays expressible as `=== undefined`.\n */\n detachExternalBridge?: (() => void) | undefined;\n /**\n * The caller's `opts.signal`, read ONCE at the entry point and kept here\n * (#1690).\n *\n * The bridge onto it is registered at one of two moments — in the `NAVIGATE`\n * edge's action when something in the announce or the leave dispatch can\n * abort (#1724), and after the walk is planned when only guards can — so the\n * second site needs the same signal OBJECT the first would have used.\n * Re-reading `opts.signal` there is not equivalent: `opts` may be accessor- or\n * Proxy-backed, so a second read can hand back a different object, and the\n * bridge would then be detached from something it was never attached to. The\n * first site reads the same snapshot off `RouterPayloads[\"NAVIGATE\"]`, which\n * is this very field — the plan IS the payload.\n */\n externalSignal?: AbortSignal | undefined;\n /**\n * The caller's `reload` / `replace` / `redirected`, read ONCE at the entry\n * and kept here — the three inputs of `buildTransitionMeta` (#1719).\n *\n * ⚑ **They exist to empty a WINDOW, not to save a read.** Built from `opts`\n * inside `completeTransition`, the meta was the one step there that ran\n * application code, so the commit stayed sound only by an ordering RULE — one\n * got wrong twice on record. The snapshot empties that window structurally;\n * see {@link CommitPermit} for what the rule was protecting.\n *\n * Three flat slots rather than a nested record: `plan-born-in-final-shape`\n * puts every slot in the literal, and a record would allocate for an arc\n * (cut A) that has no use for it.\n *\n * ⚠ **CodSpeed reports ≈15 % on `navigate/sync-baseline` for this change, and\n * that step is the MODEL, not the clock — #1728, closed as not-planned.**\n * `cpuTotal` is a valgrind model in which this arc is gated by the COMBINED\n * bytecode of `beginTransition` + `planPhases`: slow inside 600…821 bytes,\n * fast outside, both edges found to the byte, and the snapshot added 28 and\n * crossed it. Wall-clock on the x64 runner has master FASTER by 2.9 %. So\n * there is no cost here to pay down, and any step this arc reports is worth a\n * wall-clock check before it is worth an investigation.\n *\n * ⚠ Snapshotting does NOT change what plugins receive — the announcement\n * still hands over `opts` ITSELF, because keys core never declared are a\n * shipped contract on it: `memory-plugin` round-trips its own `source` marker\n * through `navigate(...)` to `onTransitionSuccess`.\n */\n reload?: boolean | undefined;\n replace?: boolean | undefined;\n redirected?: boolean | undefined;\n}\n\n/**\n * Everything a navigation works out BEFORE any guard runs, in one bag.\n *\n * A superset of {@link NavigationContext}, so the same object is handed to\n * `completeTransition` / `finishAsyncNavigation` at the end instead of a second\n * literal being built there — the allocation count per navigation is unchanged\n * (one), which is why this is a refactor and not a hot-path regression.\n *\n * Filled in two passes across the FSM's `TRANSITION_START` emit, because the\n * order is observable: `suspendable` must be read BEFORE the pre-commit listener\n * windows (#1169 — a listener's `stop()` empties the listener lists), while the\n * guard maps must be read AFTER, since a `TRANSITION_START` listener may still\n * register a guard. The fields of the second pass therefore start as write-once\n * placeholders.\n */\nexport interface NavigationPlan extends NavigationContext {\n /** Whether a synchronous supersede is reachable at all (#1169 commit-gate). */\n suspendable: boolean;\n /**\n * `opts.forceDeactivate`, read ONCE at the entry point (#1690).\n *\n * First pass for the same reason {@link NavigationContext.externalSignal} is:\n * its only consumer, `planPhases`, runs AFTER the announce, so reading it\n * there put application code inside the window the bridge's late registration\n * assumes is empty. Measured on the un-hoisted form: a getter aborting the\n * signal from inside it reaches nobody — no `TRANSITION_CANCEL`,\n * `isLeaveApproved()` stuck true, the #1684 symptom on an exotic `opts`.\n */\n forceDeactivate: boolean;\n canActivateFunctions: Map<string, GuardFn>;\n shouldDeactivate: boolean;\n shouldActivate: boolean;\n hasGuards: boolean;\n}\n\n/**\n * Dependencies injected into NavigationNamespace.\n *\n * These are function references from other namespaces/facade,\n * avoiding the need to pass the entire Router object.\n */\nexport interface NavigationDependencies {\n /** Per-router logger instance (from `getInternals(router).logger`) */\n logger: RouterLogger;\n\n /** Get router options */\n // The erased view: these namespaces READ configuration (`defaultRoute`\n // truthiness, `allowNotFound`) — they never resolve a callback, which is\n // `resolveDefault`'s job. Taking `AnyOptions` keeps the dependency-map\n // generic out of every namespace that has no use for it.\n getOptions: () => AnyOptions;\n\n /** Check if route exists */\n hasRoute: (name: string) => boolean;\n\n /**\n * The route's DECLARED query-param names — the same registry the URL build\n * prints from (#1556), minus path slots. Feeds the always-on channel guard\n * (#1572); read here rather than re-derived, so classification cannot drift.\n */\n getQueryParams: (name: string) => readonly string[];\n\n /**\n * Per-segment param-source map for a route name (`{ segment: { param: \"url\" |\n * \"query\" } }`), read from the live matcher — the ownership channel for\n * `getTransitionPath` (RFC-4 M2 / #1548, replaced the removed `stateMetaStore`\n * WeakMap). `undefined` when the name is not in the tree.\n */\n getMetaForState: (\n name: string,\n ) => Record<string, Record<string, \"url\" | \"query\">> | undefined;\n\n /** Get current state */\n getState: () => State | undefined;\n\n /**\n * May the router LEAVE the committed state? Deactivation guards only — the\n * 404 has no route to activate.\n *\n * Routed through `RouteLifecycleNamespace.canNavigateTo` with an empty\n * activate list rather than walking the guard Maps here, so there is ONE\n * synchronous guard policy and `navigateToNotFound` cannot drift from\n * `canNavigateTo` (async guard → `false`, throwing guard → `false` + a\n * `logger.warn`).\n */\n canDeactivateCurrent: (\n deactivated: string[],\n toState: State,\n fromState: State,\n ) => boolean;\n\n /** Build complete navigate state: forwardState + route check + buildPath + makeState in one step */\n buildNavigateState: (\n routeName: string,\n routeParams: Params,\n routeSearch?: SearchParams,\n ) => State | undefined;\n\n /**\n * Resolve the `defaultRoute` / `defaultParams` / `defaultSearch` options\n * (each a static value or a callback). Two channels, never one bag — the\n * default route may be chosen dynamically, so its query defaults have to\n * travel in their own slot rather than be re-channelled downstream\n * (RFC-4 M2 / #1548).\n */\n resolveDefault: () => { route: string; params: Params; search: SearchParams };\n\n /**\n * Announce the transition: send NAVIGATE to the router FSM with the PLAN as\n * the payload, so the machine adopts the plan object as the navigation it is\n * carrying (#1648). Nothing is read back — the identity IS the object.\n *\n * **Returns whether the edge actually fired.** `false` means user code drove\n * the machine out of the transition band between `canNavigate()` and this\n * call (a `stop()` from a `forwardState` interceptor is the shipped way), so\n * the navigation was never announced and must not proceed.\n */\n startTransition: (plan: NavigationPlan) => boolean;\n\n /**\n * Commit a state that is NOT the product of a navigation, through the FSM\n * `SYSTEM_COMMIT` action (write + announce in one table fact). Throws when the\n * machine has no edge to take — `ROUTER_DISPOSED` only if it actually IS\n * disposed, otherwise `ROUTER_NOT_STARTED` with the phase in the message\n * (#1644 / #1647). The edge lives on `READY` alone, so this refuses live\n * routers too.\n */\n systemCommit: (\n toState: State,\n fromState: State | undefined,\n opts: NavigationOptions,\n ) => State;\n\n /**\n * Cancel the in-flight navigation via the FSM `CANCEL` event. The `CANCEL`\n * action aborts the current controller (with `reason`, if given — surfaces as\n * the leave signal's `reason`, #943) and emits `TRANSITION_CANCEL`. No-op when\n * nothing is cancellable.\n */\n cancelNavigation: (reason?: unknown) => void;\n\n /**\n * Open the cancellability scope onto the caller's `opts.signal` — the LATE of\n * the bridge's two moments (#1690), the only one the pipeline still asks for.\n *\n * The EARLY one is the `NAVIGATE` edge's own action (#1724), so this\n * dependency exists for the single fact the machine cannot know when that edge\n * fires: whether THIS transition walks a guard. `planPhases` answers it only\n * after the announce, because a `TRANSITION_START` listener may still register\n * one.\n *\n * Idempotent at the far end — the implementation owns \"is a bridge already\n * standing?\", so the arc that reaches both moments registers once.\n */\n bridgeExternalSignal: (plan: NavigationPlan) => void;\n\n /**\n * ask-half of the commit — same table row as {@link sendTransitionDone}.\n *\n * Returns a {@link CommitPermit} rather than `true` so the destructive work\n * downstream can DEMAND it; `undefined` is the refusal.\n */\n canCommitTransition: (payload: NavigationContext) => CommitPermit | undefined;\n sendTransitionDone: (payload: NavigationContext) => void;\n\n /**\n * Send FAIL event to routerFSM, naming the navigation the report belongs to,\n * so the table can refuse one from a navigation that has already been\n * superseded. The name is the plan OBJECT — the same one the machine adopted\n * on NAVIGATE — so there is no stamp to get wrong (#1648).\n */\n sendTransitionFail: (\n fromState: State | undefined,\n error: unknown,\n nav: object,\n ) => void;\n\n /** Emit TRANSITION_ERROR event to listeners */\n emitTransitionError: (\n toState: State | undefined,\n fromState: State | undefined,\n error: unknown,\n ) => void;\n\n /** Send LEAVE_APPROVE event to routerFSM and emit to listeners */\n sendLeaveApprove: (plan: NavigationPlan) => void;\n\n /** Check if navigation can begin (router is started) */\n canNavigate: () => boolean;\n\n /**\n * Is the machine still in STARTING, i.e. inside the boot window (#1647)? Read\n * ONLY to pick which cached not-started rejection to hand back — the refusal\n * itself is already decided by `canNavigate()` above, so this can never widen\n * it. The boot never reaches that branch: `completeStart()` leaves STARTING\n * before it navigates, so for the boot `canNavigate()` is true.\n */\n isStarting: () => boolean;\n\n /** Get lifecycle functions (canDeactivate, canActivate maps) */\n getLifecycleFunctions: () => [Map<string, GuardFn>, Map<string, GuardFn>];\n\n /** Check if a transition is currently in progress */\n isTransitioning: () => boolean;\n\n /**\n * Clear canDeactivate guard for a route — DESTRUCTIVE, hence the permit\n * ({@link CommitPermit}): it is only legitimate once the table has agreed to\n * the commit, and the parameter is what makes \"ask first\" unrepresentable\n * rather than remembered.\n */\n clearCanDeactivate: (name: string, permit: CommitPermit) => void;\n\n /** Check if any leave listeners are registered */\n hasLeaveListeners: () => boolean;\n\n /** Any pre-commit transition listener (onTransitionStart / onTransitionLeaveApprove) — #1169 gate */\n hasPreCommitListeners: () => boolean;\n\n /** Call all leave listeners — returns Promise if any are async, undefined otherwise */\n awaitLeaveListeners: (\n toState: State,\n fromState: State | undefined,\n signal: AbortSignal,\n ) => Promise<void> | undefined;\n}\n","// packages/core/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts\n\nimport { errorCodes } from \"../../constants\";\nimport { RouterError, freezeThrownError } from \"../../RouterError\";\n\nimport type { RouterLifecycleDependencies } from \"./types\";\nimport type { NavigationOptions, State } from \"../../types\";\n\nconst REPLACE_OPTS: NavigationOptions = Object.freeze({ replace: true });\n\n/**\n * Was this failure, at its root, \"the route is not there\"?\n *\n * ⚑ Asked one level deeper than the code, and that is the whole point. On the\n * synchronous guard walk `handleNavigateError` restates the failure as\n * `TRANSITION_CANCELLED` — the FSM has already left the transition by then, and\n * that predicate is deliberate (#1609 — `handleNavigateError`'s own docstring\n * owns the measurement of the alternative). `asCancellation` attaches the\n * original error WHOLE as `reason`, so the cause survives the restatement and a\n * classifier can read it without touching either #1609's predicate or the\n * two-code contract #1756 documents for `navigate()`.\n *\n * ⚠ Narrow on purpose: a `TRANSITION_CANCELLED` wrapping anything else — a real\n * supersede, a `stop()` — is not a missing route and is rethrown. Measured on\n * the boot arms, a window that stops or disposes the router fails `NOT_STARTED`\n * rather than a wrapped `ROUTE_NOT_FOUND`, so it never reaches here.\n */\nfunction isMissingRoute(error: unknown): boolean {\n // ⚠ Written over the two CANDIDATES rather than as an early-out on\n // `instanceof`: that shape carries a `return false` for a non-`RouterError`\n // the tier cannot reach — nothing in the commit path fails with a plain\n // `Error` — and an unreachable statement under a 100 % gate is a branch to\n // remove, not to `v8 ignore`. Here the same case falls out of the predicate.\n const candidates: unknown[] = [\n error,\n (error as { reason?: unknown } | null | undefined)?.reason,\n ];\n\n return candidates.some(\n (candidate) =>\n candidate instanceof RouterError &&\n candidate.code === errorCodes.ROUTE_NOT_FOUND,\n );\n}\n\n/**\n * Independent namespace for the router's START lifecycle.\n *\n * `start()` is the whole surface, and there is deliberately no `stop()` beside\n * it: stopping is the facade sending `STOP`, whose edge `update`\n * (`clearCurrent`) shifts the committed pair — a namespace method would be a\n * second writer of state the table owns. Lifecycle state is the machine's for\n * the same reason: `isActive()` reads the FSM, this namespace holds no flags.\n */\nexport class RouterLifecycleNamespace {\n #deps!: RouterLifecycleDependencies;\n\n // =========================================================================\n // Dependency injection\n // =========================================================================\n\n /**\n * Sets dependencies for lifecycle operations.\n * Must be called before using lifecycle methods.\n */\n setDependencies(deps: RouterLifecycleDependencies): void {\n this.#deps = deps;\n }\n\n // =========================================================================\n // Instance methods\n // =========================================================================\n\n /**\n * Starts the router with the given path.\n *\n * Guards (concurrent start, already started) are handled by the facade via\n * RouterFSM state checks before this method is called.\n */\n async start(startPath: string): Promise<State> {\n const deps = this.#deps;\n\n // #1185: this method is the start-interceptor target — it runs AFTER the\n // whole interceptor chain. A stop() during that window sent STOP\n // (STARTING → IDLE via the FSM table), so if the router is back at IDLE the\n // start was cancelled mid-window; reject instead of committing a state on a\n // stopped router (mirrors the guard phase, which cancels from\n // TRANSITION_STARTED). `isIdle()` is deliberate — a dispose() mid-window\n // leaves the FSM DISPOSED, which the navigateToState / navigateToNotFound\n // liveness gate rejects as ROUTER_DISPOSED (#1186), not conflated with a\n // cancel.\n if (deps.isIdle()) {\n throw freezeThrownError(new RouterError(errorCodes.TRANSITION_CANCELLED));\n }\n\n const options = deps.getOptions();\n\n // Invariant guard (#939): core is platform-agnostic, so the caller must\n // provide a string path. Without a browser-plugin start interceptor to\n // inject a location, a non-string `startPath` (e.g. `start(undefined)`)\n // would otherwise reach matchPath() and throw a cryptic, code-less\n // `TypeError: …codePointAt` deep inside path-matcher. This guard runs AFTER\n // the interceptor chain (browser-plugin substitutes the location upstream),\n // so it only fires when nothing supplied a path — turning the cryptic crash\n // into an actionable error. Symmetric with the subscribe / navigateToNotFound\n // type guards; the validator deliberately permits `undefined` at the facade\n // for exactly the browser-plugin-override case.\n if (typeof startPath !== \"string\") {\n throw new TypeError(\n `[router.start] path must be a string, got ${typeof startPath}`,\n );\n }\n\n const matchedState = deps.matchPath(startPath);\n\n if (!matchedState && !options.allowNotFound) {\n const err = new RouterError(errorCodes.ROUTE_NOT_FOUND, {\n path: startPath,\n });\n\n // No report is emitted here, deliberately. `#unwindFailedStart` already\n // sends FAIL for a start that threw while STARTING, and that is the edge\n // which names no navigation — the two sibling refusals above (the\n // cancelled-mid-window one and the path type guard) have always relied on\n // it. Reporting here as well put a SECOND navigation-less FAIL on the\n // table, and once the machine had moved on it landed on an in-band edge\n // and stole it from a live navigation.\n throw freezeThrownError(err);\n }\n\n deps.completeStart();\n\n if (matchedState) {\n // navigateToState commits matchedState's VALUES — same primitive URL\n // plugins use on popstate / navigate-event (#525). Keeps trailing-slash\n // and any other source-URL flavor that matchPath produced; skips the\n // redundant forwardState+buildPath round-trip in buildNavigateState.\n //\n // ⚑ The match above ran BEFORE `completeStart()`, and `completeStart()` is\n // what opens the second boot window — a plugin's `onStart`, a raw `$start`\n // listener. Route-CRUD is ungated there (`isTransitioning()` is `false`),\n // so `matchedState` can be a stale object by the time it is committed, and\n // the commit then fails on a route that no longer exists (#1750).\n //\n // The owner's decision for that window is DEGRADE, not gate: the mutation\n // the application asked for applies, and the boot reports the consequence\n // through the channel the caller already handles. Under `allowNotFound`\n // that channel is the router's own not-found state — the same answer a URL\n // matching nothing gets, and the same thing `replace()` does on a RUNNING\n // router when it drops the route the user is on (#950 / #1201).\n //\n // ⚠ The `instanceof` is the TYPE narrowing, not a runtime term: `error` is\n // `unknown`, and without it `error.code` does not compile. It is redundant\n // at runtime — anything that is not a `RouterError` has no matching `code`\n // and rethrows on the next clause anyway — so no test pins it and removing\n // it leaves the whole tier green. What it reds is `type-check`. Named here\n // because a silent redundancy reads as a guard that guards nothing.\n //\n // ⚑ `options` is read once at the top of this method and the window cannot\n // change it: the options object is frozen per instance and `allowNotFound`\n // sits on it, so a write from\n // `onStart` throws (measured). The gate therefore reads the value the\n // CALLER configured, not one the boot window could have flipped under it.\n //\n // ⚑ And the fallback cannot swap the reported failure. Measured on the four\n // arms where the window stops or disposes the router: every one of them\n // fails `NOT_STARTED`, never `ROUTE_NOT_FOUND`, so they are rethrown\n // untouched and `navigateToNotFound` is not reached. The pairing that would\n // matter — `ROUTE_NOT_FOUND` on a router the window already stopped — is\n // unreachable through them, and the widening that would reach it is pinned\n // by the `CANNOT_ACTIVATE` cell.\n //\n // ⚠ Narrow deliberately, on BOTH runtime terms. Without `allowNotFound` there is\n // no state to degrade into and the rejection is correct, so the option the\n // caller already chose decides. And only `ROUTE_NOT_FOUND` is caught: a\n // guard refusing the boot, a cancellation or a plugin throwing are other\n // failures and keep their codes.\n //\n // ⚑ Window ONE needs nothing here and never did: it runs before\n // `matchPath`, so a wipe there leaves no match at all and the\n // `allowNotFound` branch below has always taken over. The two windows\n // disagreed for that reason alone.\n // ⚠ `try`/`await`, not `.catch()`: the collaborator is declared\n // `State | Promise<State>` and the guard-free boot really does take the\n // synchronous arc (cut A, #1588), where a bare `State` comes back and\n // has no `.catch`. The CONTROL cell for an untouched boot is what caught\n // that — the defective form passed both windows and reds only there.\n try {\n return await deps.navigateToState(matchedState, REPLACE_OPTS);\n } catch (error) {\n if (!options.allowNotFound || !isMissingRoute(error)) {\n throw error;\n }\n\n return deps.navigateToNotFound(startPath);\n }\n }\n\n return deps.navigateToNotFound(startPath);\n }\n}\n","// packages/core/src/namespaces/EventBusNamespace/types.ts\n\nimport type {\n RouterEvent,\n RouterFSMContext,\n RouterPayloads,\n RouterState,\n} from \"../../routerFSM\";\nimport type { RouterEventMap } from \"../../types/internal\";\nimport type { EventEmitter } from \"../../utils/event-emitter\";\nimport type { FSM } from \"../../utils/fsm\";\n\nexport interface EventBusOptions {\n routerFSM: FSM<RouterState, RouterEvent, RouterFSMContext, RouterPayloads>;\n emitter: EventEmitter<RouterEventMap>;\n}\n\ndeclare const SCOPE_DECIDED: unique symbol;\n\n/**\n * Proof that the cancellability scope was DECIDED before the transition was\n * announced (#1724).\n *\n * The `NAVIGATE` action does two things in order: it settles whether this\n * navigation gets a bridge onto the caller's `AbortSignal`, and then it\n * announces the transition. The order is the whole point — `addEventListener`\n * does not fire retroactively, so a signal that aborts from inside the announce\n * (a plugin's `onTransitionStart`, measured: 4 times across the tier) reaches\n * nobody if the decision is taken afterwards. The failure is silent: the\n * navigation still commits, no `TRANSITION_CANCEL` is emitted, and the band sits\n * in `LEAVE_APPROVED` until something else moves it (#1030 / #1684).\n *\n * So the order is expressed in the TYPE rather than in a comment: the announce\n * demands this proof, the proof exists only as the decision's result, and a\n * `const` cannot be read above its own declaration — announcing first is\n * `TS2448`, not a red test. The symbol is never exported, so the brand cannot be\n * forged outside this module.\n *\n * ⚠ It proves the decision HAPPENED, not that a bridge stands: \"no bridge is\n * needed here\" is a decision too, and the guard-free, listener-free arc takes\n * exactly that branch. Reading it as \"a bridge exists\" is the one misuse the\n * type cannot prevent.\n *\n * Same shape as `CommitPermit` in `NavigationNamespace/types.ts`, which locks\n * the ask/cleanup order in `completeTransition` for the same reason: the mistake\n * had been made twice, and a pin only speaks after the fact.\n */\nexport interface ScopeDecision {\n readonly [SCOPE_DECIDED]: true;\n}\n\n/**\n * The one decision token in the process. The brand is erased at runtime, so it\n * carries no information — its whole job is to exist only where the scope's fate\n * has already been settled. Module-level because it is a token, not a value: a\n * fresh object per navigation would allocate on the #307 hot path for nothing.\n */\nexport const SCOPE_DECIDED_TOKEN = {} as ScopeDecision;\n","import type {\n FSMConfig,\n PayloadOf,\n TransitionInfo,\n TransitionListener,\n} from \"./types\";\n\n/** Captured like the deciding seven, but this one BUILDS the guarantee (#2072). */\nconst objectCreate = Object.create;\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 objectEntries = Object.entries;\nconst hasOwn = Object.hasOwn;\nconst objectValues = Object.values;\n\n/**\n * The NORMALIZED edge: every entry is an object, so the hot-path property load\n * in `send()` sees ONE shape. The string form stays first-class in the config —\n * it is widened here, once per distinct table (cached), not per send.\n */\ninterface NormEdge<TStates extends string, TContext, TPayload> {\n readonly target: TStates;\n readonly when: ((ctx: TContext, payload: TPayload) => boolean) | undefined;\n readonly update: ((ctx: TContext, payload: TPayload) => void) | undefined;\n}\n\ntype NormTable<TStates extends string> = Record<\n TStates,\n Record<string, NormEdge<TStates, never, never> | undefined>\n>;\n\nconst NORMALIZED = new WeakMap<object, NormTable<string>>();\n\ninterface RawEdge {\n target: string;\n when?: unknown;\n update?: unknown;\n}\n\n/**\n * Widen ONE config entry into the normalized edge shape, validating it on the\n * way (#1159): `when` / `update`, when present, must be functions — a loud\n * throw with a coordinate, symmetric with #885.\n */\nfunction normalizeEdge(\n state: string,\n event: string,\n declaration: string | RawEdge,\n): NormEdge<string, never, never> {\n if (typeof declaration === \"string\") {\n return { target: declaration, when: undefined, update: undefined };\n }\n\n // ⚑ ONE read per slot (#1930). The validation below and the record built from\n // it decide from the same value, so a declaration answering a function to the\n // check and something else to the store cannot reach the table — that store\n // is what `send` later invokes, after the state has swapped.\n //\n // ⚠ Not a hypothetical shape: the file's declared threat model is a table from\n // a JS or cast caller, which its `no-unnecessary-condition` disables say in\n // those words, and a getter is no more exotic than the non-function value the\n // throw below already handles.\n const when = declaration.when;\n const update = declaration.update;\n\n if (when !== undefined && typeof when !== \"function\") {\n throw new Error(\n `[FSM.constructor] transitions[\"${state}\"][\"${event}\"].when is not a function`,\n );\n }\n\n if (update !== undefined && typeof update !== \"function\") {\n throw new Error(\n `[FSM.constructor] transitions[\"${state}\"][\"${event}\"].update is not a function`,\n );\n }\n\n return {\n target: declaration.target,\n when: when as NormEdge<string, never, never>[\"when\"],\n update: update as NormEdge<string, never, never>[\"update\"],\n };\n}\n\n/**\n * Widen the config table into a single edge SHAPE and validate its closure.\n *\n * Two jobs, one cold pass: (1) the string form and the object form become the\n * SAME object, so the hot-path load in `send()` stays MONOMORPHIC — the\n * prototype gate measured the `string | object` union left in the hot path at\n * +12.3 % on `navigate/sync-baseline` against +3.0 % normalized; (2) every\n * target is checked for declaredness (#1159), so a dangling target cannot\n * silently enter an undeclared state and brick `canSend()`.\n *\n * Cached per distinct table OBJECT, so a shared module-level table (the router\n * FSM) is normalized once rather than once per instance — otherwise every SSR\n * clone would pay |states|×|events| allocations. The cache holds edges only;\n * the CONTEXT is per instance and never shared through it.\n */\nfunction normalizeTable(table: object): NormTable<string> {\n const cached = NORMALIZED.get(table);\n\n if (cached !== undefined) {\n return cached;\n }\n\n const raw = table as Record<string, Record<string, string | RawEdge>>;\n const out = objectCreate(null) as NormTable<string>;\n\n for (const [state, rawEdges] of objectEntries(raw)) {\n const edges = objectCreate(null) as Record<\n string,\n NormEdge<string, never, never> | undefined\n >;\n\n for (const [event, declaration] of objectEntries(rawEdges)) {\n // An explicit `undefined` is the declared \"no transition\" no-op — skipped,\n // not a dangling target.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime guard for JS / cast callers, whose table may carry explicit undefined\n if (declaration !== undefined) {\n edges[event] = normalizeEdge(state, event, declaration);\n }\n }\n\n out[state] = edges;\n }\n\n for (const edges of objectValues(out)) {\n for (const edge of objectValues(edges)) {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- `Object.values` widens to include the record's `| undefined` member\n if (edge !== undefined && out[edge.target] === undefined) {\n throw new Error(\n `[FSM.constructor] state \"${edge.target}\" is not declared in config.transitions`,\n );\n }\n }\n }\n\n NORMALIZED.set(table, out);\n\n return out;\n}\n\n/**\n * Synchronous finite state machine engine.\n *\n * Reentrancy: `send()` inside `onTransition` listener is allowed but unbounded —\n * callers are responsible for preventing infinite loops.\n *\n * Exceptions: if a listener throws, the exception propagates to the caller.\n * State is already updated before listeners fire, so `getState()` reflects the\n * new state even if the exception escapes `send()`. A throwing `when` is a\n * CONTRACT break, and it is the one case where the state is NOT yet changed —\n * conditions run before the swap (RFC-10a §6.2).\n */\nexport class FSM<\n TStates extends string,\n TEvents extends string,\n TContext,\n TPayloadMap extends Partial<Record<TEvents, unknown>> = Record<never, never>,\n> {\n #state: TStates;\n #currentTransitions: Record<\n string,\n NormEdge<TStates, TContext, never> | undefined\n >;\n #listenerCount = 0;\n #actions: Map<TStates, Map<TEvents, (payload: unknown) => void>> | null =\n null;\n readonly #context: TContext;\n readonly #transitions: Record<\n TStates,\n Record<string, NormEdge<TStates, TContext, never> | undefined>\n >;\n readonly #listeners: (TransitionListener<\n TStates,\n TEvents,\n TPayloadMap\n > | null)[] = [];\n\n constructor(config: FSMConfig<TStates, TEvents, TContext, TPayloadMap>) {\n // ⚑ ONE read (#1930). `#state` and `#currentTransitions` ARE the machine, and\n // a slot answering differently between the two writes desynchronises them:\n // `getState()` reports one row while `canSend` answers another's.\n const initial = config.initial;\n\n this.#state = initial;\n this.#context = config.context;\n this.#transitions = normalizeTable(config.transitions) as Record<\n TStates,\n Record<string, NormEdge<TStates, TContext, never> | undefined>\n >;\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime guard for JS / cast / string-typed callers\n if (this.#transitions[initial] === undefined) {\n throw new Error(\n `[FSM.constructor] state \"${initial}\" is not declared in config.transitions`,\n );\n }\n\n this.#currentTransitions = this.#transitions[initial];\n }\n\n /**\n * ⚠ The rest tuple lives in the OVERLOAD, the implementation takes a\n * positional parameter — and that split is load-bearing, not style.\n *\n * The tuple is what correlates the payload to the SPECIFIC event (#753):\n * a payload event requires its payload, a no-payload event rejects one, and\n * `send(\"A\", payloadForB)` does not compile. Only a conditional rest tuple\n * can say that. But a rest parameter MATERIALISES AN ARRAY on every call,\n * and this is the router's hottest entry point — `send` and `canSend`\n * together run several times per navigation. Measured on the alloc probe\n * (window 200, median of 31): dropping the rest form off `canSend` alone is\n * **-88 B per navigation**, and doing it on `send` too collapses the p90\n * tail from 2384 to 2133 B, i.e. the array was intermittently escaping.\n *\n * `@real-router/event-emitter` already made this trade the other way round\n * for the same reason (`emit(name, a?, b?, c?, d?)` rather than `...args`);\n * the FSM engine simply never received it. Keeping the tuple in the overload\n * is what lets the engine have both.\n */\n send<E extends TEvents>(\n event: E,\n ...args: E extends keyof TPayloadMap ? [TPayloadMap[E]] : [undefined?]\n ): TStates;\n send(event: TEvents, sentPayload?: unknown): TStates {\n const declaration = this.#currentTransitions[event];\n\n if (declaration === undefined) {\n return this.#state;\n }\n\n const payload = sentPayload as PayloadOf<TEvents, TPayloadMap, TEvents>;\n const when = declaration.when;\n\n // A refused condition is indistinguishable from an undeclared event by\n // every observable: no swap, no update, no action, no listeners, and the\n // return value is the unchanged state (RFC-10a §6.2).\n if (when !== undefined && !when(this.#context, payload as never)) {\n return this.#state;\n }\n\n const nextState = declaration.target;\n const update = declaration.update;\n\n const from = this.#state;\n\n this.#state = nextState;\n this.#currentTransitions = this.#transitions[nextState];\n\n // Machine bookkeeping first, effects second: the action reads ONLY its\n // payload, so \"does the action see pre- or post-update context\" never\n // becomes a question on the hot path (RFC-10a §6.2).\n update?.(this.#context, payload as never);\n\n if (this.#actions !== null) {\n const action = this.#actions.get(from)?.get(event);\n\n if (action !== undefined) {\n action(payload);\n }\n }\n\n // Stryker disable next-line ConditionalExpression: equivalent — count>0 is a perf gate to skip the dispatch loop; `true` always enters it, but with no live listener `#listeners` holds only null slots and the loop body guards `listener !== null`, so dispatch is a no-op either way. The EqualityOperator `<=0` sibling on this line stays killed (not silenced here).\n if (this.#listenerCount > 0) {\n const info = {\n from,\n to: nextState,\n event,\n payload,\n } as unknown as TransitionInfo<TStates, TEvents, TPayloadMap>;\n\n for (const listener of this.#listeners) {\n if (listener !== null) {\n listener(info);\n }\n }\n }\n\n return this.#state;\n }\n\n /**\n * `canSend(e, p)` is true ⟺ `send(e, p)` from the same state/context would\n * fire the transition — the mechanical-completeness invariant (RFC-10a §5).\n * The payload is optional: a payload-independent condition answers honestly\n * without one, while a payload-dependent one answers whatever it returns for\n * `undefined` — a refusal for some predicates, an accept for others\n * (INVARIANT 4). `send` stays the authority; pass the payload you intend to\n * send when the correlation has to hold.\n */\n canSend<E extends TEvents>(\n event: E,\n ...args: E extends keyof TPayloadMap ? [TPayloadMap[E]?] : [undefined?]\n ): boolean;\n canSend(event: TEvents, askedPayload?: unknown): boolean {\n const declaration = this.#currentTransitions[event];\n\n if (declaration === undefined) {\n return false;\n }\n\n return (\n declaration.when === undefined ||\n declaration.when(this.#context, askedPayload as never)\n );\n }\n\n getState(): TStates {\n return this.#state;\n }\n\n getContext(): TContext {\n return this.#context;\n }\n\n on<E extends TEvents>(\n from: TStates,\n event: E,\n action: E extends keyof TPayloadMap\n ? (payload: TPayloadMap[E]) => void\n : () => void,\n ): () => void {\n const edges = this.#transitions[from];\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime guard for JS / cast / string-typed callers\n if (edges === undefined) {\n throw new Error(\n `[FSM.on] state \"${from}\" is not declared in config.transitions`,\n );\n }\n\n // #1682 — the state check above is ONE AXIS SHORT of what #885 claimed. An\n // action registered on a `(from, event)` pair with no edge can never fire,\n // which is the \"silently dead-registering an action\" this guard's own\n // docblock says it prevents. Measured across the suite: exactly one\n // registration is affected, and it exists to pin the permissiveness itself\n // — `fsm-edge-reachability` owns that count.\n //\n // One check is TOTAL over both dead shapes: `normalizeTable` drops an\n // explicit `undefined` target (the declared \"no transition\" no-op), so an\n // absent pair and a declared no-op are indistinguishable here — both simply\n // have no key.\n // `Object.hasOwn`, not `event in edges`: `in` walks the prototype chain, so\n // it would admit `toString` / `constructor` / `__proto__` as \"declared\n // edges\" the moment the normalised row stopped being a null-prototype\n // object. That is true today (`Object.create(null)` below) — which is\n // exactly why the `in` form passes its test — but it makes this guard depend\n // on a distant detail of `normalizeTable` instead of on itself.\n if (!hasOwn(edges, event)) {\n throw new Error(\n `[FSM.on] event \"${event}\" has no edge from state \"${from}\"`,\n );\n }\n\n this.#actions ??= new Map();\n\n let stateActions = this.#actions.get(from);\n\n if (!stateActions) {\n stateActions = new Map();\n this.#actions.set(from, stateActions);\n }\n\n const capturedAction = action as (payload: unknown) => void;\n\n stateActions.set(event, capturedAction);\n\n return () => {\n // Stryker disable next-line OptionalChaining: equivalent — `#actions` is assigned (`??= new Map()` above) before this unsubscribe closure is created and returned, so it is never null when the closure runs; `?.` can't short-circuit and behaves identically to `.get`.\n const stateMap = this.#actions?.get(from);\n\n if (stateMap?.get(event) === capturedAction) {\n stateMap.delete(event);\n }\n };\n }\n\n onTransition(\n listener: (info: TransitionInfo<TStates, TEvents, TPayloadMap>) => void,\n ): () => void {\n const nullIndex = this.#listeners.indexOf(null);\n let index: number;\n\n if (nullIndex === -1) {\n index = this.#listeners.length;\n this.#listeners.push(listener);\n } else {\n this.#listeners[nullIndex] = listener;\n index = nullIndex;\n }\n\n this.#listenerCount++;\n let subscribed = true;\n\n return () => {\n if (!subscribed) {\n return;\n }\n\n subscribed = false;\n this.#listeners[index] = null;\n // Stryker disable next-line UpdateOperator: equivalent — #listenerCount feeds only the `> 0` loop gate; `++` inflates it but the loop then iterates already-nulled slots (no-op), and no public reader exposes the count, so the miscount is unobservable.\n this.#listenerCount--;\n };\n }\n}\n","// packages/core/src/routerFSM.ts\n\nimport { freezeStateShell } from \"./helpers\";\nimport { FSM } from \"./utils/fsm\";\n\nimport type { NavigationOptions, State } from \"./types\";\nimport type { TransitionTable } from \"./utils/fsm\";\n\n/**\n * Router FSM states.\n *\n * - IDLE: Router not started or stopped\n * - STARTING: Router is initializing\n * - READY: Router is ready for navigation\n * - TRANSITION_STARTED: Navigation in progress (before deactivation guards)\n * - LEAVE_APPROVED: Deactivation guards passed, activation guards pending\n * - DISPOSED: Router has been disposed (R2+)\n */\nexport const routerStates = {\n IDLE: \"IDLE\",\n STARTING: \"STARTING\",\n READY: \"READY\",\n TRANSITION_STARTED: \"TRANSITION_STARTED\",\n LEAVE_APPROVED: \"LEAVE_APPROVED\",\n DISPOSED: \"DISPOSED\",\n} as const;\n\nexport type RouterState = (typeof routerStates)[keyof typeof routerStates];\n\n/**\n * Router FSM events. The const below is the inventory; documented here are only\n * the two whose name does not carry them — `LEAVE_APPROVE`, which splits the\n * guard walk (deactivate → approve → activate), and `SYSTEM_COMMIT`, at its own\n * key.\n */\nexport const routerEvents = {\n START: \"START\",\n STARTED: \"STARTED\",\n NAVIGATE: \"NAVIGATE\",\n LEAVE_APPROVE: \"LEAVE_APPROVE\",\n COMPLETE: \"COMPLETE\",\n FAIL: \"FAIL\",\n CANCEL: \"CANCEL\",\n STOP: \"STOP\",\n DISPOSE: \"DISPOSE\",\n /**\n * A commit that is NOT a navigation: the 404 bypass and `replace()`'s\n * revalidation. Routing them through the table is what makes \"every channel\n * that changes committed state goes through the table\" hold without\n * exception (plan §6.1, §6.2).\n */\n SYSTEM_COMMIT: \"SYSTEM_COMMIT\",\n} as const;\n\nexport type RouterEvent = (typeof routerEvents)[keyof typeof routerEvents];\n\n/**\n * **The cancellability scope, as the table sees it (#1716).**\n *\n * One field: how to CLOSE the scope. Opening it is not an operation at all —\n * the scope is born with the plan (`plan-born-in-final-shape` pins the slot in\n * the literal) and the machine ADOPTS it on the `NAVIGATE` edge, which is what\n * `ctx.inflight = payload` already does. Closing is the ACTION of whichever\n * terminal edge the navigation leaves the band through, rather than the\n * pipeline's business spread over four settle sites (#1688) — `CANCEL`,\n * `FAIL` or `COMPLETE`. The closure is self-clearing, so calling it twice is a\n * no-op and the two edges that share one action need no coordination.\n *\n * The two payloads that carry it are exactly the two that carry the PLAN:\n * `NAVIGATE`, which the `CANCEL` / `FAIL` actions reach through\n * {@link RouterFSMContext.inflight} (neither edge has an `update`, so the field\n * is still there when they run), and `COMPLETE`, whose action must read its own\n * payload because its `update` — `commitNavigation` — clears `inflight` first.\n *\n * ⚑ **OPENING it is the `NAVIGATE` action's job since #1724, so the field is\n * written from inside the machine at both ends of the lifetime.** Opening it\n * from the pipeline leaves one site the machine cannot own: a navigation whose\n * `NAVIGATE` the table REFUSES would have a bridge standing and no edge to\n * close it. A refused edge runs no action, so from inside the machine such a\n * navigation opens nothing at all.\n *\n * ⚑ **`DISPOSE` is deliberately NOT in that set, and that is measured rather\n * than assumed.** An action there could not reach the scope anyway — the edge's\n * `update` (`resetState`) zeroes `inflight` BEFORE the action runs, and\n * `DISPOSE` carries no payload — but it would also have nothing to close:\n * instrumented over the whole functional tier, every `DISPOSE` traversal comes\n * from `IDLE` or `STARTING`, never from inside the band, and not one carries a\n * live bridge.\n *\n * ⚠ The census COUNTS are deliberately not restated here: they scale with the\n * tier while the SHAPE — only `IDLE` and `STARTING`, never in-band — is the half\n * the claim rests on. Re-run the census rather than trust a number beside the\n * code.\n *\n * Eight deliberate attempts to reach an in-band\n * `DISPOSE` (from a guard, a `subscribeLeave` listener,\n * `onTransitionLeaveApprove`, an async guard's continuation, a parked\n * navigation, a `TRANSITION_CANCEL` listener, a Proxy `opts` getter and\n * `onTransitionStart`) all landed on the `IDLE` edge. The reason is structural:\n * `Router.dispose()` and `Router.stop()` both send `sendCancelIfPossible` FIRST,\n * and `CANCEL` is declared unconditionally on both in-band states, so the band\n * is always left through an edge that DOES close.\n */\ninterface CancellabilityScope {\n detachExternalBridge?: (() => void) | undefined;\n}\n\n/**\n * Per-event payloads for the router FSM (#1169 commit-gate). The three hot\n * navigation transitions carry their transition states so the FSM action\n * dispatched by `send()` emits the matching transition event — i.e. events are\n * literal consequences of FSM transitions (no `forceState` + manual emit). See\n * `EventBusNamespace.#setupFSMActions`.\n *\n * ⚑ **None of them carries an identity FIELD, and that is the design (#1648).**\n * Handing the table a number read back from the machine (`nav.myEpoch`) lets\n * it check only what the caller chose to stamp — the honesty of the stamp\n * being a convention. The identity is instead the payload OBJECT itself: `NavigationPlan` is what `navigate()` builds, it IS the\n * payload for NAVIGATE / LEAVE_APPROVE / COMPLETE, and `beginNavigation`\n * remembers it in {@link RouterFSMContext.inflight}. So \"is this send stale?\" is\n * `payload === ctx.inflight` — a question no caller can answer dishonestly,\n * because presenting the live navigation means presenting the live object.\n * There is no epoch to read, to pass, or to get wrong.\n */\nexport interface RouterPayloads {\n NAVIGATE: {\n toState: State;\n fromState?: State | undefined;\n /**\n * The navigation's own `AbortController`, when it has one (#1684). Read by\n * ONE reader, the `CANCEL` action — never by the table: the `NAVIGATE`\n * `update` only remembers the payload, and `mayCommit` asks the caller's\n * signal through `payload.externalSignal`, not this one. So the field\n * lives on the layer where effects already live (bookkeeping in `update`,\n * effects in the action — RFC-10a §6.2), and it is the same slot the\n * pipeline reads through `NavigationContext.controller`.\n */\n controller?: AbortController | undefined;\n /**\n * Where the `CANCEL` action RECORDS the cancellation, written by that action\n * and by nothing else (#1706).\n *\n * The controller above is allocated lazily, under conditions the pipeline\n * owns (`NavigationContext.controller` lists all three) — so a `CANCEL`\n * that lands before the first consumer opened one had nowhere to go: the\n * `?.` dropped it, and the controller opened moments later was born\n * UNABORTED, which satisfied the liveness fence and let the guards of an\n * already-cancelled navigation run. Recording the reason as DATA costs no\n * allocation, so the born-dead arcs still allocate zero\n * (`born-dead-navigation-1648.test.ts` counts them), and the pipeline's\n * `openController` aborts on birth when this is set.\n */\n cancelReason?: unknown;\n /**\n * The caller's `opts.signal` as the navigation snapshotted it at its entry\n * (`NavigationContext.externalSignal`, #1690) — read by the `NAVIGATE`\n * action, which OPENS the scope onto it (#1724).\n *\n * ⚠ Not `payload.opts.signal`: `NavigationOptions` is accessor- and\n * Proxy-backed by contract, so a second read is a call into application code\n * that may hand back another object — and the bridge would then be detached\n * from something it was never attached to. Same reason the COMPLETE payload\n * carries its own copy (#1717).\n */\n externalSignal?: AbortSignal | undefined;\n } & CancellabilityScope;\n LEAVE_APPROVE: { toState: State; fromState?: State | undefined };\n COMPLETE: {\n toState: State;\n fromState?: State | undefined;\n /**\n * The caller's options as the ENTRY copied them: `adoptNavigationOptions`\n * drops `signal` before the plan exists, so what the announcement hands\n * every plugin's `onTransitionSuccess` carries the caller's other keys and\n * not that one. The machine asks `externalSignal` below instead — the\n * snapshot it may rely on (#1690 / #1717).\n *\n * Required, because `completeTransition` is the ONE sender — which `tsc`\n * proves — and it hands over the plan, whose `opts` is required already.\n */\n opts: NavigationOptions;\n /**\n * The caller's `opts.signal` as the navigation snapshotted it at its entry\n * (`NavigationContext.externalSignal`, #1690), which is the ONLY form of\n * that signal the table may ask about (#1717).\n *\n * ⚠ **Not a convenience copy — `payload.opts.signal` is a DIFFERENT\n * question.** `NavigationOptions` is accessor- and Proxy-backed by contract,\n * so reading it is a call into application code and a later read may hand\n * back another object entirely. `mayCommit` runs inside `FSM.send`, and\n * inside `canSend` a second time, with the destructive post-leave cleanup\n * between them — so a re-read let the two evaluations of one `when`\n * disagree, refusing a healthy commit at the ask (band stuck in\n * `LEAVE_APPROVED`, nothing emitted) or at the send (`completeTransition`\n * returning a state the table never committed). The snapshot cannot\n * disagree with itself.\n */\n externalSignal?: AbortSignal | undefined;\n } & CancellabilityScope;\n /**\n * RFC-10a §7.2 — FAIL and CANCEL carry their own data now. This is what kills\n * the `#pending*` side channel (satellite S2): the action reads a parameter\n * instead of instance fields written \"just before send()\", so the #949\n * \"valid only in this window\" contract has no field left to attach to.\n */\n FAIL: {\n /**\n * The navigation this failure belongs to — the plan object itself, compared\n * by reference against {@link RouterFSMContext.inflight}. `undefined` is\n * legal and means \"not a navigation failure at all\": the one no-navigation\n * sender (`Router.#unwindFailedStart`) takes `STARTING --FAIL--> IDLE`,\n * which carries no `when`. Two others were retired rather than moved — the\n * report in `RouterLifecycleNamespace` (a duplicate of that unwind) and the\n * early validation errors, which emit `TRANSITION_ERROR` directly since the\n * `READY→FAIL` edge went (RFC-10a §16.5).\n *\n * Typed `object` rather than `NavigationPlan` deliberately: the table needs\n * IDENTITY, not structure, and the machine must not learn the pipeline's\n * types to compare two references.\n */\n nav?: object | undefined;\n fromState?: State | undefined;\n error?: unknown;\n };\n /**\n * No `toState`: since #1671 the ACTION reads it off the context instead. The\n * two CANCEL edges are declared inside the transition band only, and\n * `inflight` is no longer cleared on the way out, so the value the action\n * needs is still there when it runs.\n */\n CANCEL: { fromState?: State | undefined; reason?: unknown };\n SYSTEM_COMMIT: {\n toState: State;\n fromState?: State | undefined;\n opts: NavigationOptions;\n };\n}\n\n/**\n * The machine's own memory (RFC-10a §7.1). Three fields, all mutated ONLY by\n * table updates — never from outside.\n *\n * There is deliberately no `inflightFromState`: every cancel source already\n * carries `fromState` (it reads the committed state), and after the ownership\n * move it would duplicate the context's own `current` — RFC-10a §16.6.\n */\nexport interface RouterFSMContext {\n /**\n * **The navigation the machine is currently carrying — the object, not a\n * number (#1648).** This one field is the navigation's IDENTITY and its\n * TARGET at once: it holds the `NAVIGATE` payload, which is the\n * `NavigationPlan` itself, so `payload === ctx.inflight` answers \"is this send\n * still the live navigation's?\" and `ctx.inflight.toState` answers \"where was\n * it going?\".\n *\n * Identity by reference is what makes a stamp unnecessary: a counter would\n * exist so a sender could mark its sends and the table compare marks, and a\n * caller cannot present a live identity it does not hold.\n *\n * ⚠ INTERNAL, and never to be exposed as a public snapshot version — that is\n * exactly what the counter was refused for (plan §11.C2). A reference is\n * additionally unserialisable, which keeps that door shut structurally.\n *\n * ⚑ **Meaningful INSIDE the transition band only, and deliberately allowed to\n * be stale outside it (#1671).** Written by `beginNavigation` on the three\n * edges that enter the band; cleared by `commitNavigation` (COMPLETE) and\n * `resetState` (DISPOSE) — but NOT on the way out through CANCEL or FAIL,\n * because those edges' ACTIONS are the readers and an `update` runs first.\n * So after a cancelled or failed navigation the last plan lingers here until\n * the next `beginNavigation` overwrites it: ONE slot, never a growing set.\n * That slot now holds a plan rather than a `State`, i.e. it also keeps the\n * caller's `opts` (with any external `AbortSignal`), the guard maps and —\n * since #1684 — the navigation's OWN `AbortController` reachable until the\n * next navigation. Bounded and accepted on every count: a navigation that got\n * a controller at all has had it aborted by the time either of these two edges\n * is taken (a cancellation from inside the announce lands here with none —\n * allocation happens later), and it carries no listener (the bridge onto the\n * caller's signal is closed by the ACTION of whichever terminal edge was\n * taken, i.e. before this field is read — see\n * {@link RouterPayloads.NAVIGATE.detachExternalBridge}); the guard maps are owned by\n * `RouteLifecycleNamespace` regardless.\n *\n * The readers, and the gate each is under — check this list before adding one:\n * - the CANCEL action, declared on `TRANSITION_STARTED` / `LEAVE_APPROVED`\n * only, so it runs in-band by construction;\n * - the two IN-BAND FAIL actions, same argument. The third FAIL edge,\n * `STARTING --FAIL--> IDLE`, has its OWN action precisely because it is\n * outside the band: a failed `start()` is not a navigation failure and has\n * no target to name, so it emits `undefined` rather than reading here;\n * - `mayCommit` / `mayFail`, which compare against it by reference.\n *\n * ⚠ Declaring `CANCEL` or `FAIL` from a state outside the band would make\n * the staleness reachable. That is the one edit this field cannot survive\n * silently.\n */\n inflight: RouterPayloads[\"NAVIGATE\"] | undefined;\n /**\n * The committed state, and the one it displaced. Formerly\n * `StateNamespace.#frozenState` / `#previousState`.\n *\n * The pair moves TOGETHER and never separately: `set()` shifts one into the\n * other, so splitting them between the machine and a store would smear that\n * shift across two owners — the exact defect the move exists to remove\n * (plan §11.A2).\n *\n * ⚑ `readonly` is the class guard for \"the table owns the committed pair\"\n * (#1749), and it is a COMPILE-TIME one: every holder of a\n * `RouterFSMContext` outside this module gets `TS2540` on a write, at the\n * moment of the edit rather than on the next tier run. It replaced the\n * second writer — `StateNamespace.clearCommitted`, reachable unguarded from\n * the published `./validation` subpath — with nothing.\n *\n * The three `update`s below take {@link MutableRouterFSMContext} instead.\n * That is not a hole: TypeScript does not track `readonly` across\n * assignment, so the engine still hands them this very object — the modifier\n * constrains the DECLARED type a foreign module holds, which is exactly the\n * surface an authority check cares about.\n */\n readonly current: State | undefined;\n readonly previous: State | undefined;\n}\n\n/**\n * The module-private mutable view of the context. Only the `update` functions\n * in this file take it, which is what makes the `readonly` above meaningful.\n */\ntype MutableRouterFSMContext = {\n -readonly [K in keyof RouterFSMContext]: RouterFSMContext[K];\n};\n\nexport function createInitialRouterFSMContext(): RouterFSMContext {\n return {\n inflight: undefined,\n current: undefined,\n previous: undefined,\n };\n}\n\n/**\n * A FAIL naming no navigation is legal (start-unwind); one naming a navigation\n * that is no longer in flight is stale — it belongs to a navigation that has\n * already been superseded, and letting it through would move the machine out\n * from under the live one, turning its COMPLETE into a table no-op (#1609).\n *\n * ⚑ **The question is IDENTITY, asked by reference (#1648):** the payload names\n * its navigation by handing back the plan object, and `ctx.inflight` is the plan\n * the machine is carrying. Before that this compared two numbers, and the number\n * was one the sender had read out of the machine and stamped by hand — so the\n * table could only check what it was given honestly.\n *\n * ⚠ **This predicate cannot currently refuse, and that is measured, not\n * assumed (#1646).** Instrumented over the whole functional tier it returns\n * `false` ZERO times — never once for a dead navigation. Re-measured 2026-09-08:\n * still zero. The ask COUNT is deliberately not quoted; it tracks the size of\n * the tier and says nothing the refusal count does not.\n * The reason is structural rather than lucky: only the NAVIGATE update ever\n * puts a navigation THERE (the other two writes clear the field), so the\n * navigation a sender can name is by construction the one the machine adopted;\n * and both navigation-naming FAIL senders are already gated on\n * `deps.isTransitioning()` — the machine has to still be in the band — with\n * `asCancellation` restating a lost-liveness failure as `TRANSITION_CANCELLED`\n * — which `routeTransitionError` filters out before any send. Three adversarial\n * arcs (guard-redirect, a guard rejecting after a supersede landed, the\n * ROUTE_NOT_FOUND arc) were driven deliberately: all three end in `CANCELLED`\n * with no `TRANSITION_ERROR` at all. The `nav === undefined` half is dead for\n * a second reason — the one navigation-less sender (`Router.#unwindFailedStart`)\n * takes `STARTING --FAIL--> IDLE`, which carries no `when`.\n *\n * ⚑ **What it DOES hold is measured too (#1672), and that is new.** Two-sided\n * mutation: removing `asCancellation` alone fails 5 tests, removing it AND this\n * predicate fails **6**. The sixth is `superseded-guard-rejection-1609.test.ts`\n * \"outcome 2: the superseding navigation still emits TRANSITION_SUCCESS and\n * notifies subscribers\" — exactly the silent-commit shape. So the division of\n * labour is known rather than assumed: `asCancellation` holds the half facing\n * the CALLER (which code `navigate()` rejects with), this predicate holds the\n * half facing SUBSCRIBERS. \"Defence-in-depth\" below names a second half, it is\n * not a hedge.\n *\n * The dead disjunct is dead by mutation as well as by argument: `mayFail →\n * true` kills nothing (#1648 §5.6), so the no-navigation branch is never TAKEN,\n * not merely never decisive.\n *\n * So it is a PROVEN EQUIVALENT in the mutation-testing sense, kept as\n * defence-in-depth for the day the liveness gates above it change: the table is\n * the last thing between a stale terminal report and the live navigation, and\n * two comparisons is what that costs. Do NOT read its survival as a coverage\n * gap to be closed with a test — no test can reach it without changing\n * production code first (core CLAUDE.md, \"Mutation testing\": prove equivalence,\n * then document rather than chase).\n *\n * ⚠ **The first disjunct ADMITS rather than refuses, and it rests on an\n * INVENTORY, not on an argument: every navigation-less FAIL must reach the\n * table only from `STARTING`.** One sender satisfies it — `#unwindFailedStart`\n * — gated on `isStarting()` and declared on that edge alone. A second one is\n * the edit this predicate cannot\n * survive: admitted from inside the band, its FAIL takes\n * `TRANSITION_STARTED/LEAVE_APPROVED --FAIL--> READY`, whose action names the\n * LIVE navigation as the failure and moves the machine out from under it —\n * committed, silently, with no `TRANSITION_SUCCESS` for anyone. That is not\n * hypothetical: `RouterLifecycleNamespace.start` reported its `ROUTE_NOT_FOUND`\n * here until it turned out to be a duplicate of the unwind, and was removed.\n * Tightening this disjunct is NOT the fix if it happens again — a\n * `nav !== undefined &&` form can only be covered while such a sender exists,\n * so the guard would live off the very defect it guards against. Check the\n * inventory when adding a `sendFail`; report early refusals through\n * `emitTransitionError` (channel (b) in `wireNamespaces`), which does not move\n * the machine.\n */\nconst mayFail = (\n ctx: RouterFSMContext,\n payload: RouterPayloads[\"FAIL\"] | undefined,\n): boolean => payload?.nav === undefined || payload.nav === ctx.inflight;\n\n/**\n * The commit gate, as a condition on the edge the commit takes. Both halves of\n * the interim check it replaces are here: a superseded navigation is no longer\n * the object in `ctx.inflight` (the nested NAVIGATE replaced it), and a\n * terminated router has no COMPLETE edge at all — COMPLETE is declared from\n * LEAVE_APPROVED only.\n *\n * ⚑ The `payload !== undefined` term is not defensive noise: `canSend(COMPLETE)`\n * may be asked WITHOUT a payload (engine INVARIANT \"Totality over absence\"), and\n * `undefined === ctx.inflight` would be TRUE the moment nothing is in flight.\n * Spelling it out keeps the ask conservative, which is the direction this gate\n * must fail in.\n *\n * ⚑ **The IDENTITY term has no killing test, and since #1719 it cannot have one\n * — the same status `mayFail` above carries, reached the same way.** Its only\n * killer was `commit-ask-snapshot-1649 › refuses the commit when an opts\n * getter supersedes the navigation`, which reached the cell through a read of\n * the caller's `opts` INSIDE the commit: a getter firing there could start a\n * second navigation after the outer one had passed every liveness check. No\n * such read remains, and with it no window in which a payload that is not\n * `ctx.inflight` can arrive at this edge. Measured both ways: before #1719\n * dropping this term reds exactly that one test; after it, nothing. Re-measured\n * 2026-09-08 — dropping it still reds nothing. The tier's SIZE is deliberately\n * not quoted: it is not what the statement rests on, and it goes stale weekly.\n *\n * ⛔ **Not a coverage gap to close with a test — no test can reach it without\n * changing production code first.** Reaching it needs a second navigation parked\n * in `LEAVE_APPROVED` between the outer one's last liveness check and its ask,\n * and that window is empty by TWO independent constructions: above the ask stand\n * `hasRoute`, a `buildTransitionMeta` that reads the plan and `Object.freeze`,\n * none of which run application code; between the ask and the send stands only\n * `clearCanDeactivate`, which reads stored compiled forms rather than invoking\n * factories (#1649). The legal carriers were enumerated and each falls earlier:\n * an activation guard is caught by the liveness fence, `subscribeLeave` and\n * `onTransitionLeaveApprove` by the reentrancy ban, codecs and option callbacks\n * by the pre-start ban (#1610/#1665).\n *\n * It stays for the day either of those two constructions changes — the table is\n * the last thing between a superseded navigation and a commit, and one reference\n * comparison is what that costs. The failure mode without it is SILENT and not\n * small: the superseded navigation's `COMPLETE` would fire, `commitNavigation`\n * would write ITS state over the live one and clear `inflight`, the announcement\n * would report that as a success, and the live navigation would then find no\n * edge and resolve a state nobody committed.\n *\n * ⚠ **The third term asks the SNAPSHOT, and re-reading the caller's object\n * instead is the one edit it cannot survive (#1717).** This predicate runs\n * twice per commit — once for `canSend`'s ask, once inside the `send` it\n * permits — with `completeTransition`'s destructive post-leave cleanup between\n * them. `opts` is accessor- and Proxy-backed by contract, so `opts.signal`\n * there is a call into application code that may answer differently each time:\n * a stranger at the ask refused a healthy commit without moving the machine\n * (band stuck in `LEAVE_APPROVED`, no `TRANSITION_CANCEL` for anyone), a\n * stranger at the send made `completeTransition` return a state the table never\n * committed. {@link RouterPayloads.COMPLETE.externalSignal} is the one object\n * the navigation was actually set up with, and it cannot disagree with itself.\n */\nconst mayCommit = (\n ctx: RouterFSMContext,\n payload: RouterPayloads[\"COMPLETE\"] | undefined,\n): boolean =>\n payload !== undefined &&\n payload === ctx.inflight &&\n payload.externalSignal?.aborted !== true;\n\n/**\n * The pair shift, and the ONLY place it happens for a navigation commit. It\n * runs as an `update`, i.e. after the machine has already decided the\n * transition fires — so \"committed\" and \"announced\" cannot come apart.\n */\nconst commitState = (ctx: MutableRouterFSMContext, state: State): void => {\n ctx.previous = ctx.current;\n ctx.current = freezeStateShell(state);\n};\n\nconst commitNavigation = (\n ctx: RouterFSMContext,\n payload: RouterPayloads[\"COMPLETE\"],\n): void => {\n commitState(ctx, payload.toState);\n ctx.inflight = undefined;\n};\n\nconst commitSystemState = (\n ctx: RouterFSMContext,\n payload: RouterPayloads[\"SYSTEM_COMMIT\"],\n): void => {\n commitState(ctx, payload.toState);\n};\n\n/**\n * `stop()` — SHIFTS the pair, so `getPreviousState()` still answers with the\n * state the router was stopped from. Deliberately not the same primitive as\n * DISPOSE below (plan §11.A2 measured the difference: `stop()` leaves\n * `undefined / b`, `dispose()` leaves `undefined / undefined`).\n */\nconst clearCurrent = (ctx: MutableRouterFSMContext): void => {\n ctx.previous = ctx.current;\n ctx.current = undefined;\n};\n\n/** `dispose()` — zeroes BOTH cells at once, no shift. */\nconst resetState = (ctx: MutableRouterFSMContext): void => {\n ctx.current = undefined;\n ctx.previous = undefined;\n ctx.inflight = undefined;\n};\n\n/**\n * Entering the transition band: the machine adopts the navigation as the one it\n * is carrying. This IS the moment identity is issued (#1648) — there is nothing\n * to stamp, because the identity is the object being adopted.\n */\nconst beginNavigation = (\n ctx: RouterFSMContext,\n payload: RouterPayloads[\"NAVIGATE\"],\n): void => {\n ctx.inflight = payload;\n};\n\n/**\n * The band's `CANCEL` edges are unconditional BY TYPE, not by discipline\n * (#1681).\n *\n * Sharpening the table's own declaration type to the STRING form for this one\n * event in these two states means the object form — the only way to spell a\n * `when` — does not compile there. `TS2322`, at the edge itself, instead of a\n * comment two files away from what it protects.\n *\n * What it protects is a NEIGHBOURING edge's unreachability.\n * `abortPreviousNavigation` leaves the band through `canCancel()` =\n * `canSend(CANCEL)`, so while these edges refuse nothing, `sendNavigate` is only\n * ever reached from `READY` — measured, 0 of 3593 sends came from inside the\n * band — and the two `NAVIGATE` self-loops stay untraversed. A `when` here makes\n * the self-loop reachable (measured: a send from `LEAVE_APPROVED`), which is\n * condition 3 of the false-green documented on the `READY` `NAVIGATE` edge.\n *\n * ⚠ It is keyed off `routerStates` / `routerEvents` rather than off string\n * literals, so renaming a state or the event moves the constraint with them\n * instead of silently detaching it — the failure mode a hand-written `\"CANCEL\"`\n * would have.\n */\ntype UnconditionalBandCancel = Readonly<\n Record<\n typeof routerStates.TRANSITION_STARTED | typeof routerStates.LEAVE_APPROVED,\n Readonly<Partial<Record<typeof routerEvents.CANCEL, RouterState>>>\n >\n>;\n\n/**\n * The edges that are ABSENT on purpose — declared as `never` so adding one back\n * is a compile error rather than a silent behaviour change.\n *\n * ⚑ **An absence is the hardest thing in this table to protect, and that is\n * structural rather than accidental.** A test exercises what happens; there is\n * no arc to exercise for an edge that does not exist, so every one of these\n * lived in a comment saying \"and its absence is the answer, not an omission\".\n * Adding the edge back makes the comment false and nothing else — no test walks\n * the arc it opens, because until that moment the arc was unreachable. Spelling\n * the absence in the type is the only mechanism that speaks at the moment of\n * the edit.\n *\n * Each entry is a decision with its own reason, kept at the state it belongs to:\n *\n * - **`STARTING` has no `NAVIGATE` and no `SYSTEM_COMMIT`.** Together they ARE\n * the pre-boot window (#1647): a navigation or a 404 commit attempted from a\n * start interceptor is refused by the table itself, so the window needs no\n * facade predicate of its own.\n * - **`READY` has no `FAIL`.** Its absence is the answer to RFC-10a §16.5: the\n * two senders it existed for are REPORTS to observers, not failures of a\n * transition, so a stale `FAIL` there is a table no-op structurally — stronger\n * than the `when` predicate that was drafted for it.\n * - **The band has no `STOP`.** `stop()` from inside a transition is routed\n * through `CANCEL` first; leaving `STOP` undeclared is what makes the\n * terminate path go through the cancellation machinery instead of around it.\n * - **`DISPOSED` has nothing at all.** The machine cannot be resurrected — the\n * sole authority over state is the table, and this is where that ends\n * (#1169 D-full).\n *\n * ⚠ Only absences that were already DOCUMENTED as load-bearing are listed. An\n * edge nobody has needed yet is not the same as an edge nobody may add, and\n * freezing the second kind would turn this from a lock into a cage.\n */\ntype DeclaredAbsences = Readonly<{\n [routerStates.STARTING]: Readonly<\n Partial<\n Record<\n typeof routerEvents.NAVIGATE | typeof routerEvents.SYSTEM_COMMIT,\n never\n >\n >\n >;\n [routerStates.READY]: Readonly<\n Partial<Record<typeof routerEvents.FAIL, never>>\n >;\n [routerStates.TRANSITION_STARTED]: Readonly<\n Partial<Record<typeof routerEvents.STOP, never>>\n >;\n [routerStates.LEAVE_APPROVED]: Readonly<\n Partial<Record<typeof routerEvents.STOP, never>>\n >;\n [routerStates.DISPOSED]: Readonly<Partial<Record<RouterEvent, never>>>;\n}>;\n\n/**\n * Router FSM configuration.\n *\n * Transitions:\n * - IDLE → STARTING (START), DISPOSED (DISPOSE)\n * - STARTING → READY (STARTED), IDLE (FAIL, STOP), DISPOSED (DISPOSE)\n * - READY → TRANSITION_STARTED (NAVIGATE), READY (SYSTEM_COMMIT, self-loop for the two commits that are not transitions), IDLE (STOP), DISPOSED (DISPOSE)\n * - TRANSITION_STARTED → LEAVE_APPROVED (LEAVE_APPROVE), TRANSITION_STARTED (NAVIGATE, self-loop), READY (CANCEL, FAIL), DISPOSED (DISPOSE)\n * - LEAVE_APPROVED → READY (COMPLETE, CANCEL, FAIL), TRANSITION_STARTED (NAVIGATE), DISPOSED (DISPOSE)\n * - DISPOSED → (no transitions)\n *\n * DISPOSE is wired from every non-DISPOSED state so `router.dispose()` always\n * settles the FSM at DISPOSED. The facade orchestrates cleanup through IDLE\n * for healthy flows; the direct transitions guarantee the FSM is not left\n * stuck if cleanup is skipped (e.g. dispose mid-STARTING when the start\n * pipeline threw before STARTED/FAIL).\n *\n * ⛔ **THIS GRAPH MAY NOT BE CLEANED BY TRACE COVERAGE.** Read this before\n * deleting an edge that \"nothing ever takes\". It was established by\n * measurement, not caution: an `onTransition` recorder over all 4469 tests of\n * the three tiers traversed **15 of 20** edges, and every one of the other five\n * was then mutated away individually. Not one was dead. An edge belongs to\n * exactly one of three categories, and only the first is removable:\n *\n * 1. **No sender** — nothing left in core can send the event from this state.\n * Removable, and the inventory of senders is the proof. The `READY→FAIL`\n * edge was the one instance: it had two, both re-routed off the machine as\n * reports, and the edge went with them (§16.5). Note that it was TRAVERSED\n * while it lived — traversal did not make it necessary, and non-traversal\n * does not make the others removable. The two facts are independent.\n * 2. **Permission bit** — never traversed, load-bearing anyway, because it is\n * read through `canSend()` rather than taken. The two `NAVIGATE` self-loops\n * are these: `abortPreviousNavigation` walks the machine back to READY\n * before `sendNavigate`, so the loop never fires, but its DECLARATION is\n * what makes `canSend(NAVIGATE)` true mid-navigation, i.e. what makes\n * supersede legal. Removing them fails 10 and 30 tests respectively (9 and\n * 29 of those are supersede BEHAVIOUR; the remaining one each is the\n * closure assertion in `fsm-edge-reachability.test.ts`, which notices the\n * edge is gone) — with supersede dying SILENTLY at the predicate, not at\n * the send. `canSend` is read FIVE times in core: NAVIGATE / START / CANCEL\n * as bare permission bits, plus COMPLETE (with payload) and SYSTEM_COMMIT,\n * which the ask-protocol added in #1641 / #1644 and which are each followed\n * by a send. An edge for one of the first three is a candidate for this\n * category by construction. ⚠ FIVE is a count of call sites, so it moves\n * with them (#1672): re-read it from the code rather than quoting this line,\n * which is how a stale figure reaches an analysis that cites the docblock.\n * 3. **Fail-safe** — dead on every healthy flow and there precisely for the\n * unhealthy one. The three direct `DISPOSE` edges (#660) are these: 3881\n * tests pass without them because no test reaches the state they exist for.\n *\n * The corollary for the two `NAVIGATE` self-loops specifically: their `update`\n * is dead code (the machine adopts the navigation on the READY edge, the only\n * one that fires), and it is kept anyway so the three declarations stay identical —\n * a self-loop that silently differed from its sibling is a worse failure than\n * an unreachable line, and coverage does not see either.\n */\nconst routerTransitions: TransitionTable<\n RouterState,\n RouterEvent,\n RouterFSMContext,\n RouterPayloads\n> &\n UnconditionalBandCancel &\n DeclaredAbsences = {\n [routerStates.IDLE]: {\n [routerEvents.START]: routerStates.STARTING,\n [routerEvents.DISPOSE]: {\n target: routerStates.DISPOSED,\n update: resetState,\n },\n },\n [routerStates.STARTING]: {\n // ⚑ Neither `NAVIGATE` nor `SYSTEM_COMMIT` is declared here — see\n // `DeclaredAbsences` above the table, which refuses to compile them back.\n // WHY they are absent is measured, and that part belongs at the edge: the\n // two arcs a `STARTING` `SYSTEM_COMMIT` would exist for do not reach it.\n // \"`start()` with `allowNotFound` commits its 404 while still STARTING\" is\n // false against this code — `RouterLifecycleNamespace.start` calls\n // `completeStart()`, which sends STARTED and leaves STARTING, BEFORE\n // `navigateToNotFound`, an order standing since #123 (2026-02-20). \"A\n // `replace()` inside an async start interceptor commits\" is false too — the\n // revalidation commits only when a state IS committed, which means start\n // finished. Both arcs trace through `READY --SYSTEM_COMMIT--> READY`, no\n // test traverses a STARTING edge, and an edge here would be dead.\n //\n // Consequence worth knowing: a system commit attempted from STARTING is\n // now LOUD. `systemCommit()` asks `canSend` first and THROWS, so an arc\n // nobody has named surfaces instead of silently not committing.\n //\n // ⚠ The code is `ROUTER_NOT_STARTED`, not `ROUTER_DISPOSED`. The gate is\n // `canSend(SYSTEM_COMMIT)` (#1644), which refuses a LIVE router that is\n // merely starting or mid-transition as well as a disposed one, and the\n // codes are split accordingly (`EventBusNamespace.#refuseSystemCommit`):\n // four arms, one code apart — disposed → `ROUTER_DISPOSED`; mid-transition,\n // STARTING and \"not started at all\" → `ROUTER_NOT_STARTED`, each with its\n // own message (the boot window's is #1647).\n [routerEvents.STARTED]: routerStates.READY,\n [routerEvents.FAIL]: routerStates.IDLE,\n [routerEvents.STOP]: { target: routerStates.IDLE, update: clearCurrent },\n [routerEvents.DISPOSE]: {\n target: routerStates.DISPOSED,\n update: resetState,\n },\n },\n [routerStates.READY]: {\n // The one NAVIGATE edge that is ever TRAVERSED, and therefore the one that\n // adopts the navigation: `abortPreviousNavigation` drives the machine back\n // to READY before `sendNavigate` runs, so a supersede arrives here too.\n //\n // ⚠ **`sendNavigate` reads this edge's OUTCOME, and that is load-bearing\n // (#1648).** `send()` returns the resulting state, so\n // `send(NAVIGATE, plan) === TRANSITION_STARTED` stands in for \"the edge\n // fired\" — inexactly, see below — and `beginTransition` refuses a\n // navigation for which it did NOT: user\n // code (a `stop()` from a `forwardState` interceptor) can drive the machine\n // out of the band between `canNavigate()` and the send, and such a\n // navigation is born dead: never announced, with nothing to carry it.\n // ⚠ **The test is inexact in BOTH directions, and one of them is\n // load-bearing (#1681).**\n //\n // FALSE-RED — it reports `false` for an edge that DID fire, whenever\n // something moved the machine off TRANSITION_STARTED before `send`\n // returned: `send` reads `this.#state` after the update, the action AND the\n // listeners, and the `NAVIGATE` action is where `TRANSITION_START` is\n // announced. A `stop()`, a `dispose()` or an aborted `opts.signal` from a\n // plugin's `onTransitionStart` all land there. Measured over the functional\n // tier, and on every one of them the refusal is RIGHT — the count lives with\n // the reader of this outcome (`executeNavigation`'s `startTransition`\n // block), which re-measures it. This is why neither candidate replacement\n // was taken: `ctx.inflight === payload` and `canSend` before the send both\n // report those sends as fired.\n //\n // FALSE-GREEN — a refused `when` would return the same TRANSITION_STARTED\n // the send started from. That needs THREE things at once, not one:\n // 1. the `when` on the SELF-LOOP (source TRANSITION_STARTED) — a refusal\n // on this edge returns READY, which the test reads correctly;\n // 2. it must be PAYLOAD-dependent — `canBeginTransition()` already asks\n // `canSend(NAVIGATE)` upstream, so a context-only condition is refused\n // before `sendNavigate` is reached at all (with `ROUTER_NOT_STARTED`,\n // which is its own problem — #1696);\n // 3. and the machine must still be in the band at send time, which today\n // requires a `when` on `CANCEL` (see that edge).\n // Reproduced under all three; the cost is wasted work and a misleading\n // code, not corruption — the fence, `mayCommit` and `mayFail` all compare\n // identity, so a plan the machine never adopted passes none of them.\n //\n // If that day comes the remedy is `canSend(NAVIGATE, payload)` AND this\n // comparison, not one instead of the other: the ask catches the condition\n // on any edge, the comparison keeps the six announce-window refusals.\n [routerEvents.NAVIGATE]: {\n target: routerStates.TRANSITION_STARTED,\n update: beginNavigation,\n },\n // ⚑ No FAIL edge from READY — the type refuses to compile one back\n // (`DeclaredAbsences`), and RFC-10a §16.5 is answered by WHY: the edge\n // existed for exactly two\n // senders — early validation errors and the plugin-facing report — and both\n // are channel (b): reports to observers, not failures of a transition. Once\n // they emit directly, nothing legal is left to send FAIL from here, and a\n // STALE one (a superseded navigation reporting late) becomes a table no-op\n // structurally, which is stronger than the `mayFail` predicate the sketch\n // proposed for it.\n [routerEvents.SYSTEM_COMMIT]: {\n target: routerStates.READY,\n update: commitSystemState,\n },\n [routerEvents.STOP]: { target: routerStates.IDLE, update: clearCurrent },\n [routerEvents.DISPOSE]: {\n target: routerStates.DISPOSED,\n update: resetState,\n },\n },\n [routerStates.TRANSITION_STARTED]: {\n // ⚠ A PERMISSION BIT read through `canSend`, not a transition. Its presence\n // is what makes `canNavigate()` true while a navigation is in flight, i.e.\n // what makes supersede legal at all; it is never traversed, because the\n // cancel always runs first, so this `update` never fires. Removing the edge\n // kills supersede silently at `canNavigate()` — 9 behaviour tests (10 with\n // the edge-reachability closure assertion).\n [routerEvents.NAVIGATE]: {\n target: routerStates.TRANSITION_STARTED,\n update: beginNavigation,\n },\n // ⚑ No epoch condition here, and its absence is MEASURED rather than an\n // omission (#1670). `when: isOwnEpoch` stood on this edge and refused ZERO\n // times in 3464 asks. It is not inert — with the liveness fence at the head\n // of `runStep` removed it refuses four times — but that is a different\n // codebase, not a runtime scenario, and the fence is pinned by tests of its\n // own — 13 today, four when this was measured with the predicate and\n // without it alike. The unreachability is structural:\n // the asynchronous LEAVE_APPROVE arc is exactly one (through `runStep`,\n // fenced on its first line), and the other two send synchronously right\n // after `beginTransition`, where a reentrant navigate is banned. What the\n // predicate would have held is recorded beside that fence, where the\n // invariant actually lives — not here, where it could never fire.\n [routerEvents.LEAVE_APPROVE]: routerStates.LEAVE_APPROVED,\n // ⚑ No `update` either: not clearing `inflight` on the way out is what lets\n // the ACTION read the target off the context (#1671, RFC-10a §16.6 option\n // (g)). The validity window is expressed by the machine's STATE, not by the\n // field's lifetime — see `RouterFSMContext`.\n //\n // ⚑ No `when` here, and its absence is a TAUTOLOGY retired rather than a\n // guard dropped (#1669). `when: hasInflight` asked \"is anything in flight?\"\n // on the two edges where CANCEL is declared, and both start INSIDE the band:\n // the one way in from outside is `READY --NAVIGATE-->`, which carries\n // `beginNavigation`, the only `update` that puts a VALUE in `inflight`; the\n // two that clear it (`commitNavigation`, `resetState`) sit on edges that\n // LEAVE. So the question is asked only where the answer is already settled.\n //\n // ⚠ Not \"and outside the band it is never defined\" — `CANCEL` carries no\n // `update` precisely so the action can still read the target, which the\n // paragraph above states and `cancellation-stops-the-guard-walk-1687.test.ts`\n // depends on.\n //\n // Measured over the whole functional tier: 0 refusals,\n // and — unlike `isOwnEpoch` — removing its hand-rolled twin in\n // `sendCancelIfPossible` does not even change the NUMBER of asks, so there\n // is no configuration in which it could refuse.\n //\n // ⚠ **And that unconditionality is load-bearing for a NEIGHBOURING edge\n // (#1681) — which is why it is now the TYPE's job**: see\n // `UnconditionalBandCancel` above the table. A `when` here does not compile.\n [routerEvents.CANCEL]: routerStates.READY,\n [routerEvents.FAIL]: { target: routerStates.READY, when: mayFail },\n // `dispose()` from inside a transition takes THIS edge — the band declares\n // no STOP (`DeclaredAbsences`) — so it is the one that has to zero\n // everything.\n [routerEvents.DISPOSE]: {\n target: routerStates.DISPOSED,\n update: resetState,\n },\n },\n [routerStates.LEAVE_APPROVED]: {\n // Same permission bit as above — 29 behaviour tests depend on it being\n // declared (30 with the edge-reachability closure assertion).\n [routerEvents.NAVIGATE]: {\n target: routerStates.TRANSITION_STARTED,\n update: beginNavigation,\n },\n // The commit IS this edge now: `when` decides, `update` writes, the action\n // announces. There is no separate `setState` left to run ahead of the\n // verdict, which is what makes \"committed\" and \"announced\" inseparable —\n // a refused COMPLETE writes nothing at all.\n [routerEvents.COMPLETE]: {\n target: routerStates.READY,\n when: mayCommit,\n update: commitNavigation,\n },\n // Same tautology as the TRANSITION_STARTED edge above (#1669).\n [routerEvents.CANCEL]: routerStates.READY,\n [routerEvents.FAIL]: { target: routerStates.READY, when: mayFail },\n // `dispose()` from inside a transition takes THIS edge — the band declares\n // no STOP (`DeclaredAbsences`) — so it is the one that has to zero\n // everything.\n [routerEvents.DISPOSE]: {\n target: routerStates.DISPOSED,\n update: resetState,\n },\n },\n [routerStates.DISPOSED]: {},\n};\n\n/**\n * Factory function to create a router FSM instance.\n *\n * @returns FSM instance with initial state \"IDLE\"\n */\nexport function createRouterFSM(): FSM<\n RouterState,\n RouterEvent,\n RouterFSMContext,\n RouterPayloads\n> {\n return new FSM<RouterState, RouterEvent, RouterFSMContext, RouterPayloads>({\n initial: routerStates.IDLE,\n // Table shared and immutable; CONTEXT per instance, so an SSR clone starts\n // with nothing in flight (RFC-10a §6.5).\n context: createInitialRouterFSMContext(),\n transitions: routerTransitions,\n });\n}\n","// packages/core/src/namespaces/EventBusNamespace/EventBusNamespace.ts\n\nimport { SCOPE_DECIDED_TOKEN } from \"./types\";\nimport {\n EMPTY_PARAMS,\n EMPTY_SEARCH,\n errorCodes,\n events,\n} from \"../../constants\";\nimport { adoptForeignBag, adoptForeignTransition } from \"../../helpers\";\nimport { RouterError, freezeThrownError } from \"../../RouterError\";\nimport { routerEvents, routerStates } from \"../../routerFSM\";\n\nimport type { EventBusOptions, ScopeDecision } from \"./types\";\nimport type {\n RouterEvent,\n RouterFSMContext,\n RouterPayloads,\n RouterState,\n} from \"../../routerFSM\";\nimport type {\n EventName,\n LeaveFn,\n LeaveState,\n NavigationOptions,\n Plugin,\n State,\n SubscribeFn,\n TreeChangedEvent,\n Unsubscribe,\n EventMethodMap,\n Params,\n SearchParams,\n TransitionMeta,\n} from \"../../types\";\nimport type { RouterEventMap } from \"../../types/internal\";\nimport type { RouterValidator } from \"../../types/RouterValidator\";\nimport type { EventEmitter } from \"../../utils/event-emitter\";\nimport type { FSM } from \"../../utils/fsm\";\n\n/** Captured like the deciding seven, but this one BUILDS the guarantee (#2073). */\nconst freeze = Object.freeze;\n\n/**\n * Internal-only event key for route-tree mutations. Lives on the same\n * `EventEmitter` as the events `EventName` declares but never enters that\n * union — reachable only through `getRoutesApi(router).subscribeChanges()`.\n */\nconst TREE_CHANGED = \"TREE_CHANGED\";\n\nfunction ensureError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value));\n}\n\nfunction settleLeavePromises(\n promises: Promise<void>[],\n firstSyncError: unknown,\n signal: AbortSignal,\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n // #1197 — canonicalize the abort outcome: reject with a\n // `RouterError(TRANSITION_CANCELLED)` carrying the external reason, so the\n // no-guards leave path classifies identically to the guard path (no raw\n // reject, no spurious TRANSITION_ERROR). Internal sources\n // (supersede/stop/dispose) already abort with such a RouterError — thread it\n // through unchanged so the #943 reason is preserved.\n const onAbort = (): void => {\n const reason: unknown = signal.reason;\n\n reject(\n reason instanceof RouterError &&\n reason.code === errorCodes.TRANSITION_CANCELLED\n ? reason\n : new RouterError(errorCodes.TRANSITION_CANCELLED, { reason }),\n );\n };\n\n if (signal.aborted) {\n onAbort();\n\n return;\n }\n\n // Stryker disable next-line ObjectLiteral,BooleanLiteral: equivalent — `{ once: true }` is redundant: onAbort fires at most once (a signal aborts once) and the success path explicitly removeEventListener's it, so dropping `once` is unobservable. StringLiteral sibling stays live (the \"abort\" event name is killed).\n signal.addEventListener(\"abort\", onAbort, { once: true });\n\n void Promise.allSettled(promises).then((results) => {\n // Stryker disable next-line StringLiteral: equivalent — this cleanup name is redundant: onAbort is registered with `{ once: true }` and the per-navigation signal is discarded unaborted on success, so failing to remove the listener leaks nothing observable.\n signal.removeEventListener(\"abort\", onAbort);\n\n // Stryker disable next-line BlockStatement: equivalent — emptying the post-allSettled abort-race early-return falls through to resolve()/reject(), but the abort handler already settled the promise, so the extra settle is a no-op. CE sibling stays live (→true hangs the pipeline = killed via timeout).\n if (signal.aborted) {\n // Race lost to abort — the abort handler already rejected; do nothing\n return;\n }\n\n if (firstSyncError !== undefined) {\n reject(ensureError(firstSyncError));\n\n return;\n }\n\n const rejected = results.find(\n (result): result is PromiseRejectedResult =>\n result.status === \"rejected\",\n );\n\n if (rejected !== undefined) {\n reject(ensureError(rejected.reason));\n\n return;\n }\n\n resolve();\n });\n });\n}\n\n/**\n * Register the bridge onto the caller's signal and hand back the closer that\n * undoes it — the registration is the ONLY way to obtain one.\n *\n * ⚑ **That is the whole point, and it replaces an ordering rule with a shape\n * (#1724).** Left in the caller, the two statements — `addEventListener`\n * first, the closer recorded second — depend on an order that is not obvious\n * — `signal` belongs to the APPLICATION, so registering is a call into\n * code the router does not own, and `FSM.send` runs an edge's action with no\n * `try`/`catch`. A closer recorded FIRST outlives a throwing registration and\n * stands on the plan the edge's `update` has already published as\n * `ctx.inflight`; the next terminal edge calls it, `removeEventListener` fails\n * the same way, and the throw lands inside `handleCancel` above\n * `emitTransitionCancel`. Measured on the `{ signal: controller }` slip (the\n * controller passed where its `.signal` belongs): the FOLLOWING navigation dies\n * with a code-less `TypeError`, no event of any kind is emitted, and the\n * committed state does not move.\n *\n * Written this way there is nothing to order: the closer does not exist until\n * the registration has returned it, so a throw leaves the caller with nothing to\n * record. Getting it wrong is not a matter of care any more — it needs a second\n * closer written by hand, which is a rewrite rather than a swapped pair of\n * lines.\n *\n * `onClosed` keeps the self-clearing half OUT of this function: the field lives\n * on the plan, and taking the plan as a parameter would put the same ordering\n * question back inside here, where no shape guards it. So the caller passes what\n * to forget, and this function decides only WHEN (#1716 — one closing protocol,\n * three terminal edges, no coordination between them).\n */\nfunction bridgeSignal(\n signal: AbortSignal,\n onAbort: () => void,\n onClosed: () => void,\n): () => void {\n // Stryker disable next-line ObjectLiteral: equivalent — `{ once: true }` is redundant, and for a reason that OUTLIVED the router-level slot: `abort` fires at most once per signal (the DOM abort algorithm returns early when `aborted` is already true), and this listener is explicitly removed on all four settle paths. It is NOT equivalent because the signal is discarded — it belongs to the CALLER and is not (#1684).\n signal.addEventListener(\"abort\", onAbort, {\n // Stryker disable next-line BooleanLiteral: equivalent — `once` redundant, same argument as the ObjectLiteral above.\n once: true,\n });\n\n return () => {\n onClosed();\n signal.removeEventListener(\"abort\", onAbort);\n };\n}\n\nexport class EventBusNamespace {\n readonly #fsm: FSM<\n RouterState,\n RouterEvent,\n RouterFSMContext,\n RouterPayloads\n >;\n readonly #emitter: EventEmitter<RouterEventMap>;\n // Lazy accessor for the opt-in RouterValidator (wired by wireNamespaces).\n // Returns `null` until validation-plugin is registered — so the proactive\n // listener-count threshold (#1188) costs the no-plugin path nothing.\n #getValidator: (() => RouterValidator | null) | undefined;\n readonly #leaveListeners: LeaveFn[] = [];\n\n // Depth of the synchronous router-dispatch window — elevated while a\n // transition event is being emitted (`emitTransition*`), while `$start` is\n // being emitted (#1647) or while a `subscribeLeave`\n // listener batch runs. `isProcessing()` reads it so the navigation facade can\n // reject a synchronous reentrant navigate() from inside a transition listener\n // with REENTRANT_NAVIGATION (RFC navigation-cancellation-unification §4). A\n // counter (not a boolean) tolerates legitimately-nested transition emits. The\n // ceiling-bounded \"allow reentrant navigate, throw RecursionDepthError at\n // `maxEventDepth`\" behaviour (#935/#945) is gone: such a navigate now throws\n // REENTRANT_NAVIGATION at depth 1, before it can recurse. (The emitter's old\n // `maxEventDepth` depth-bound is gone too — re-entrant emits are coalesced to a\n // no-op at the emitter, #1033 — so no event can re-enter its own dispatch.)\n #dispatchDepth = 0;\n\n constructor(options: EventBusOptions) {\n this.#fsm = options.routerFSM;\n this.#emitter = options.emitter;\n this.#setupFSMActions();\n }\n\n static validateSubscribeListener(listener: unknown): void {\n if (typeof listener !== \"function\") {\n throw new TypeError(\n \"[router.subscribe] Expected a function. \" +\n \"For Observable pattern use observable(router) from @real-router/rx\",\n );\n }\n }\n\n /**\n * Validates the `subscribeLeave` listener. Unlike\n * {@link validateSubscribeListener}, the error carries **no**\n * `@real-router/rx` hint — rx exposes the Observable pattern for *success*\n * transitions (`observable(router)`, `state$`, `events$`), not for leave\n * events, so steering leave-listener misuse toward rx would mislead. The\n * asymmetry is intentional (mirrored in `core/CLAUDE.md`).\n */\n static validateSubscribeLeaveListener(listener: unknown): void {\n if (typeof listener !== \"function\") {\n throw new TypeError(\"[router.subscribeLeave] Expected a function\");\n }\n }\n\n /**\n * ⚑ Elevated like the five `emitTransition*` below, and NOT like\n * `emitRouterStop` beside it (#1647). `completeStart()` sends STARTED —\n * leaving STARTING for READY — BEFORE the boot navigation commits, so every\n * `onStart` hook runs on a READY machine that still owes a commit, where\n * `NAVIGATE` IS declared: a listener's navigation ran to completion, announced\n * `TRANSITION_SUCCESS`, and was then overwritten by the boot.\n *\n * The boot is unaffected: `completeStart()` returns before `navigateToState`\n * runs, so the counter is back to zero by the time it navigates.\n */\n emitRouterStart(): void {\n this.#dispatchDepth++;\n try {\n this.#emitter.emit(events.ROUTER_START);\n } finally {\n this.#dispatchDepth--;\n }\n }\n\n emitRouterStop(): void {\n this.#emitter.emit(events.ROUTER_STOP);\n }\n\n /**\n * ⚑ **The third parameter is a compile-time obligation, unread at runtime\n * (#1724).** It is the proof that the cancellability scope was decided BEFORE\n * this announce — see `ScopeDecision` in `./types`. The announce is the moment\n * a plugin's `onTransitionStart` runs, so a bridge registered after it misses\n * exactly the aborts it exists for, and misses them SILENTLY. The `const`\n * holding the proof cannot be read above its own declaration, so the wrong\n * order is `TS2448` rather than a test that has to notice a listener that was\n * never called.\n */\n emitTransitionStart(\n toState: State,\n fromState: State | undefined,\n _scope: ScopeDecision,\n ): void {\n this.#dispatchDepth++;\n try {\n this.#emitter.emit(events.TRANSITION_START, toState, fromState);\n } finally {\n this.#dispatchDepth--;\n }\n }\n\n emitTransitionSuccess(\n toState: State,\n fromState?: State,\n opts?: NavigationOptions,\n ): void {\n this.#dispatchDepth++;\n try {\n this.#emitter.emit(events.TRANSITION_SUCCESS, toState, fromState, opts);\n } finally {\n this.#dispatchDepth--;\n }\n }\n\n emitTransitionError(\n toState?: State,\n fromState?: State,\n error?: RouterError,\n ): void {\n this.#dispatchDepth++;\n try {\n this.#emitter.emit(events.TRANSITION_ERROR, toState, fromState, error);\n } finally {\n this.#dispatchDepth--;\n }\n }\n\n emitTransitionCancel(toState: State, fromState?: State): void {\n this.#dispatchDepth++;\n try {\n this.#emitter.emit(events.TRANSITION_CANCEL, toState, fromState);\n } finally {\n this.#dispatchDepth--;\n }\n }\n\n emitTransitionLeaveApprove(toState: State, fromState?: State): void {\n this.#dispatchDepth++;\n try {\n this.#emitter.emit(events.TRANSITION_LEAVE_APPROVE, toState, fromState);\n } finally {\n this.#dispatchDepth--;\n }\n }\n\n /**\n * True while a router event is being dispatched synchronously — an\n * `emitTransition*` call, the `$start` emit (#1647), or a `subscribeLeave`\n * listener batch is on the stack. The navigation facade reads this to reject a\n * synchronous reentrant navigate() from inside such a listener (RFC §4).\n *\n * `$stop` is deliberately NOT in that set: its action runs after the swap, so\n * the machine is already IDLE and the doors refuse `ROUTER_NOT_STARTED` from\n * the table anyway — a second mechanism there would buy nothing (#1647 §5.6a).\n */\n isProcessing(): boolean {\n return this.#dispatchDepth > 0;\n }\n\n /**\n * Emits the internal `TREE_CHANGED` event after a structural route-tree\n * mutation. Reuses the shared `EventEmitter` — so re-entrancy coalescing\n * (#1033) and per-listener error isolation (`onListenerError`) apply\n * automatically.\n */\n emitTreeChanged(event: TreeChangedEvent): void {\n this.#emitter.emit(TREE_CHANGED, event);\n }\n\n /**\n * True while a `TREE_CHANGED` event is being dispatched synchronously.\n * Delegates to the emitter's own in-flight tracking (#1034) — `getRoutesApi`\n * reads this to reject reentrant route-CRUD from a `subscribeChanges` handler\n * (#1032).\n */\n isEmittingTreeChanged(): boolean {\n return this.#emitter.isDispatching(TREE_CHANGED);\n }\n\n /**\n * Subscribes to `TREE_CHANGED`. **Lenient** duplicate semantics (mirrors\n * {@link subscribe}): each call wraps the handler in a fresh closure, so N\n * registrations of the same reference produce N independent subscriptions.\n */\n subscribeTreeChanged(\n handler: (event: TreeChangedEvent) => void,\n ): Unsubscribe {\n // Same disposed-state enforcement as subscribe()/subscribeLeave() (#946),\n // completing the guard across all three subscription primitives — extended\n // here to the internal route-tree channel (#982). A `subscribeChanges`\n // reference bound before dispose() (`const s = routes.subscribeChanges\n // .bind(routes)`) reaches this method via the getRoutesApi delegate, which\n // — unlike its add/remove/update siblings — does not itself check\n // isDisposed(). Without this guard, `emitter.on` would re-register a\n // TREE_CHANGED listener that can never fire (clearAll already ran, the FSM\n // is DISPOSED, the route tree is torn down, no future emit) — a silent\n // no-op, the internal-channel counterpart of the #946 hazard.\n if (this.isDisposed()) {\n throw freezeThrownError(new RouterError(errorCodes.ROUTER_DISPOSED));\n }\n\n return this.#emitter.on(TREE_CHANGED, (event: TreeChangedEvent) => {\n handler(event);\n });\n }\n\n /** Number of active `TREE_CHANGED` listeners (drives conditional emit). */\n treeChangedListenerCount(): number {\n return this.#emitter.listenerCount(TREE_CHANGED);\n }\n\n sendStart(): void {\n this.#fsm.send(routerEvents.START);\n }\n\n sendStop(): void {\n this.#fsm.send(routerEvents.STOP);\n }\n\n sendDispose(): void {\n this.#fsm.send(routerEvents.DISPOSE);\n }\n\n sendStarted(): void {\n this.#fsm.send(routerEvents.STARTED);\n }\n\n /**\n * Announce a navigation to the table, handing it the PLAN as the payload so\n * the machine adopts it as the navigation it is carrying (#1648).\n *\n * @returns whether the NAVIGATE edge actually fired. The edge is declared on\n * READY / TRANSITION_STARTED / LEAVE_APPROVED only, so a `false` here means\n * the machine had already left the band — see `beginTransition`, the one\n * caller that acts on it.\n */\n sendNavigate(payload: RouterPayloads[\"NAVIGATE\"]): boolean {\n // Table-driven: the FSM action emits TRANSITION_START (#1169 D-full). A\n // NAVIGATE that the table rejects is a no-op — the FSM never leaves an\n // invalid state and no event fires.\n return (\n this.#fsm.send(routerEvents.NAVIGATE, payload) ===\n routerStates.TRANSITION_STARTED\n );\n }\n\n /**\n * ask-half of the commit protocol (RFC-10a §7.4). Reads the SAME table row\n * `sendComplete` fires, in the same synchronous window, with only\n * `clearCanDeactivate` between them — which runs no application code (#1649).\n *\n * ⚠ Both calls evaluate the edge's `when`, so that window is a claim about\n * the PREDICATE too: see `mayCommit` in `routerFSM`, which owns the rule and\n * the measurement.\n */\n canCommitTransition(payload: RouterPayloads[\"COMPLETE\"]): boolean {\n return this.#fsm.canSend(routerEvents.COMPLETE, payload);\n }\n\n sendComplete(payload: RouterPayloads[\"COMPLETE\"]): void {\n // Table-driven: the FSM action emits TRANSITION_SUCCESS (#1169 D-full).\n // COMPLETE from IDLE/DISPOSED (a listener stopped/disposed mid-transition)\n // is a table no-op — no resurrection, no phantom success emit.\n this.#fsm.send(routerEvents.COMPLETE, payload);\n }\n\n sendLeaveApprove(payload: RouterPayloads[\"LEAVE_APPROVE\"]): void {\n // Table-driven: the FSM action emits TRANSITION_LEAVE_APPROVE (#1169 D-full).\n // LEAVE_APPROVE from IDLE/DISPOSED is a table no-op — no resurrection.\n // Carries no epoch (#1670): the edge is unconditional, so nothing reads\n // one.\n this.#fsm.send(routerEvents.LEAVE_APPROVE, payload);\n }\n\n sendFail(fromState?: State, error?: unknown, nav?: object): void {\n this.#fsm.send(routerEvents.FAIL, { nav, fromState, error });\n }\n\n sendCancel(fromState?: State, reason?: unknown): void {\n this.#fsm.send(routerEvents.CANCEL, { fromState, reason });\n }\n\n /**\n * Commit a state that is NOT the product of a navigation — the 404 bypass and\n * `replace()`'s revalidation. The write and the announce both happen inside\n * the FSM `SYSTEM_COMMIT` action, so neither escapes the table.\n *\n * ask and fire live HERE, one above the other, deliberately: the table\n * refuses SILENTLY (a `send` from a state without an edge is a no-op), so a\n * caller that only fired would skip the commit and nobody would hear about\n * it. Asking first turns that into the `ROUTER_DISPOSED` these callers were\n * already promised (#1186) — the guard did not disappear when it became\n * structural, it moved to where it cannot be forgotten.\n */\n systemCommit(payload: RouterPayloads[\"SYSTEM_COMMIT\"]): State {\n // ⚑ The commit door that COPIES, because nothing below it does (#1792).\n // `getInternals` is a published export, and four first-party packages reach\n // it: `ssr-utils` and `validation-plugin` directly, `ssr-data-plugin` and\n // `rsc-server-plugin` through `shared/ssr`, which is symlinked into both. So\n // `toState` can be a State someone else BUILT, while the FSM commits by\n // freezing the SHELL only, which leaves both channels as the\n // caller's own writable objects, reachable through the handle it kept.\n //\n // Copied HERE rather than in the wiring that calls this, because a state\n // literal built in the wiring layer is a door in disguise and would inherit\n // the plumbing exclusion `commit-door-authority-1753` grants — its own\n // comment says so, and it reds when that line is crossed. Not in\n // `commitState` either: the ordinary transition lands there too and must\n // stay allocation-free.\n const { toState } = payload;\n // ⚑ RETURNED, not just sent (#1792). The copy above means the caller's\n // argument stops being the object the router holds, and `navigateToNotFound`\n // hands its own argument back to application code — so without this it\n // returns a state the router never committed, value-equal and not `===`\n // `getState()`. The FSM freezes this object in place, so what comes back\n // here is the committed one, identity and all.\n // `as State` for the same reason `materialize` needs it: the conditional\n // spread below makes `transition` optional to the compiler, while `State`\n // declares it required. A foreign State that arrives without one is already\n // outside the type — this preserves that shape rather than inventing a value\n // for it.\n // ⚑ ONE read of the caller's slot, hoisted above the literal (#2008). The\n // conditional below asks whether the field is there and then asks again for\n // the value, and both are calls into application code — so a slot answering\n // differently the second time commits the very thing that conditional's own\n // ⚠ says it exists to prevent. Both halves decide from this one answer.\n const foreignTransition = (toState as { transition?: TransitionMeta })\n .transition;\n\n const committed = {\n // ⚑ Field by field, NOT `{ ...toState }` (#1792). A spread DEFINES, which\n // is the whole reason a spread is dangerous for this one name: a foreign\n // State carrying an own `__proto__` handed it straight onto the committed\n // shell, where `Object.assign(x, getState())` swapped `x`'s prototype and\n // `JSON.stringify` carried the key into the SSR payload. The channels were\n // clean the whole time — the SHELL was not, and it is the same object.\n // `navigateToState` has always built its shell this way; this door now\n // agrees with it, and a foreign state's extra fields stop riding along.\n // ⚑ Field order matches every other producer's (`makeState`, `matchPath`,\n // the pipeline). Not cosmetic: `Object.keys(getState())` is observable,\n // and a differently-ordered literal gives the committed state a second\n // hidden class — the shape #1684's regression took.\n name: toState.name,\n params: adoptForeignBag(toState.params, EMPTY_PARAMS) as Params,\n search: adoptForeignBag(toState.search, EMPTY_SEARCH) as SearchParams,\n path: toState.path,\n // ⚑ THREE channels, not two — the same set `navigateToState` copies. The\n // spread this literal replaced carried `context` by reference, which left the committed\n // `state.context` writable through the handle the caller kept: exactly the\n // defect named for the other two, surviving in the third because a spread\n // looks like a copy. `context` is the documented mutable carve-out\n // (INVARIANTS \"State immutability\" row 2) — so what is fixed here is\n // OWNERSHIP, not mutability: the committed context is core's object, and\n // plugins keep writing to it through `claim.write(getState(), …)` exactly\n // as before. A spread DEFINES, so a namespace claimed under the name\n // `__proto__` survives the copy (#1191 / #1788), which is the contract\n // `context` has and the state channels deliberately do not.\n context: { ...toState.context },\n // ⚑ The FOURTH field that needed it, for the same reason as the other\n // three (#1792). Carried by reference it stayed the caller's object and\n // unfrozen: `getState().transition.phase` could be rewritten after the\n // commit, `Object.assign(x, getState())` swapped `x`'s prototype through\n // it, and `JSON.stringify` carried an own `__proto__` on it.\n //\n // ⚠ EVERY level the shape declares, not just the meta (#2140).\n // `Object.freeze` is shallow, so adopting only the meta hands back the\n // caller's `segments` container and the two arrays inside it, live — and\n // `getState().transition.segments.activated.push(…)` then rewrites\n // published state after the commit. `adoptForeignTransition` owns the\n // depth and states where it stops; on the pipeline's own arc\n // `buildTransitionMeta` seals the same three containers itself.\n //\n // ⚠ NOT a spread. A spread DEFINES, so `{ ...transition }` re-creates an\n // own `__proto__` on the copy — the very idiom the shell three lines up\n // avoids, for the same reason. The guarded copier is the same one the\n // channels use.\n //\n // ⚠ And it is SPREAD IN, not written unconditionally. Written flat, a\n // foreign State with no `transition` committed the adoption's empty\n // answer — the shared `EMPTY_PARAMS` singleton, cast to a type that\n // declares `phase`, `reason` and `segments` as required. The committed\n // state then lied about its own shape and `getState().transition` was the\n // same object as some other state's `getState().params`. Absence stays\n // absence.\n //\n // ⚠ `!= null`, so BOTH spellings of absence are one. Tested for\n // `undefined` alone, `transition: null` reaches the adoption and takes\n // the empty answer the paragraph above is about — the rule\n // `adoptForeignBag` states for its own guard, applied to the test that\n // decides whether this slot is written at all.\n // ⚠ `materialize` is NOT a precedent for this shape: since #1976 the\n // pipeline attaches `transition` at construction on both its terminals.\n // This door is the ONE core State\n // constructor that still spreads the field conditionally, and it is the\n // only one that should — `getInternals` is published, so `toState` here\n // is a State someone ELSE built, and the runtime is the only witness of\n // what it actually contains. Three of the other five ATTACH a value core\n // owns — `pipeline/materialize`'s shared builder, `#copyChannels`,\n // `navigateToNotFound`; the last two (`getRoutesApi`'s revalidation pair)\n // COPY whatever the committed state had, absence included, so they carry\n // this door's answer forward rather than making one.\n // `state-freeze-authority` exempts this SITE, and reds on a second.\n // ⚠ The cast is the point, not noise: `State.transition` is declared\n // REQUIRED, so by the TYPE this test is dead — and `getInternals` is\n // published, so `toState` may be an object some caller hand-built to that\n // type and did not fill. The door trusts the runtime, not the declaration.\n ...(foreignTransition != null && {\n transition: adoptForeignTransition(\n foreignTransition as unknown as Record<string, unknown>,\n ) as unknown as TransitionMeta,\n }),\n } as State;\n\n // ⚑ ASKED HERE, below the copy, so that nothing runs between the ask and\n // the send. Above it the copy sat in the gap — and the copy READS every\n // value of four caller-supplied slots, which is a call into application\n // code. An accessor that called `stop()` or `dispose()` from there left the\n // ask already answered and the `send` a silent no-op, so this method\n // returned a fully-formed State that was never committed, with no throw.\n // That is exactly the outcome `internals.ts` says this throw exists to\n // prevent (#1186): \"a refusal there is silent … the contract these callers\n // already had promises an error, not a quietly skipped commit.\"\n if (!this.#fsm.canSend(routerEvents.SYSTEM_COMMIT)) {\n throw this.#refuseSystemCommit();\n }\n\n this.#fsm.send(routerEvents.SYSTEM_COMMIT, {\n ...payload,\n toState: committed,\n });\n\n return committed;\n }\n\n /**\n * ⚑ **What this ask is, and what it is NOT (#1696).** It answers one\n * question — *is `NAVIGATE` declared from where the machine is standing?* — so\n * its refusal states the router's STATE, which the caller turns into\n * `ROUTER_NOT_STARTED` (or the boot-window sentence, #1647); the sibling asks\n * (`canStart`, `systemCommit`, `canCancel`) work the same way.\n *\n * A table CONDITION refuses on that same wire and inherits whichever of those\n * sentences the call site throws — and it is asked here with **no payload**,\n * so what a `when` answers depends on the predicate (engine INVARIANT 4).\n * Adding one to an asked edge therefore means choosing what its refusal\n * reports: `refusal-code-authority-1696.test.ts` holds that closed set and\n * carries the measurements for both shapes.\n */\n canBeginTransition(): boolean {\n return this.#fsm.canSend(routerEvents.NAVIGATE);\n }\n\n canStart(): boolean {\n return this.#fsm.canSend(routerEvents.START);\n }\n\n canCancel(): boolean {\n return this.#fsm.canSend(routerEvents.CANCEL);\n }\n\n isActive(): boolean {\n const fsmState = this.#fsm.getState();\n\n return fsmState !== routerStates.IDLE && fsmState !== routerStates.DISPOSED;\n }\n\n isDisposed(): boolean {\n return this.#fsm.getState() === routerStates.DISPOSED;\n }\n\n isTransitioning(): boolean {\n const state = this.#fsm.getState();\n\n return (\n state === routerStates.TRANSITION_STARTED ||\n state === routerStates.LEAVE_APPROVED\n );\n }\n\n isLeaveApproved(): boolean {\n return this.#fsm.getState() === routerStates.LEAVE_APPROVED;\n }\n\n isReady(): boolean {\n return this.#fsm.getState() === routerStates.READY;\n }\n\n isStarting(): boolean {\n return this.#fsm.getState() === routerStates.STARTING;\n }\n\n isIdle(): boolean {\n return this.#fsm.getState() === routerStates.IDLE;\n }\n\n /**\n * Plugin-author API for subscribing to internal router events.\n *\n * @remarks\n *\n * **Duplicate-registration semantics — strict (throws).** Passing the same\n * callback reference twice for the same event throws\n * `Error(\"Duplicate listener for ...\")` from the underlying `EventEmitter`.\n * This is loud-on-misuse by design: plugin code is expected to register\n * each callback once. The contract differs from {@link subscribe} /\n * {@link subscribeLeave}, which are end-user surfaces and silently accept\n * duplicates.\n */\n addEventListener<E extends EventName>(\n eventName: E,\n cb: Plugin[EventMethodMap[E]],\n ): Unsubscribe {\n this.#checkListenerThreshold(eventName, \"addEventListener\");\n\n return this.#emitter.on(\n eventName,\n cb as (...args: RouterEventMap[typeof eventName]) => void,\n );\n }\n\n /**\n * End-user / UI-binding API for subscribing to successful transitions.\n *\n * @remarks\n *\n * **Duplicate-registration semantics — independent.** Each call wraps\n * `listener` in a fresh closure and registers it as a distinct internal\n * slot. `router.subscribe(fn)` twice produces **two** active subscriptions;\n * `fn` fires twice per `TRANSITION_SUCCESS`. The returned `Unsubscribe` is\n * paired with its specific call — invoking it removes exactly that\n * registration.\n *\n * This contract differs from {@link addEventListener} (plugin API, throws\n * on duplicate). End-user code that wants idempotent registration must\n * gate itself, e.g. `if (!unsub) unsub = router.subscribe(fn);`.\n */\n subscribe(listener: SubscribeFn): Unsubscribe {\n // Enforce the disposed state HERE, not only on the facade. A reference\n // bound before dispose() (`const s = router.subscribe.bind(router)`)\n // bypasses the facade's #markDisposed swap and reaches this method\n // directly. Without this guard, `emitter.on` would silently re-register a\n // listener that can never fire (clearAll already ran, FSM is DISPOSED, no\n // future emit) — a silent no-op / stuck-UI hazard (#946).\n if (this.isDisposed()) {\n throw freezeThrownError(new RouterError(errorCodes.ROUTER_DISPOSED));\n }\n\n this.#checkListenerThreshold(events.TRANSITION_SUCCESS, \"subscribe\");\n\n // `subscribe` is fire-and-forget; the listener's return value is ignored at\n // the type level (`SubscribeFn` is `=> void`). A void-typed async listener\n // still returns a rejecting Promise at runtime, so return it to the emitter,\n // whose central isolation routes the rejection to the same `onListenerError`\n // sink a sync throw flows through — instead of leaking an `unhandledRejection`\n // (fatal under `--unhandled-rejections=strict`, the Node 22+ default). This\n // is #944, now folded into the emitter's central #1412 async isolation — the\n // former per-site `.catch` here is redundant (symmetric with `subscribeLeave`,\n // which isolates via `Promise.allSettled`).\n return this.#emitter.on(\n events.TRANSITION_SUCCESS,\n (toState: State, fromState?: State) => {\n // Return the listener's runtime value to the emitter so its central\n // isolation catches an async listener's rejection (#944/#1412). Read the\n // void-typed result into `unknown` first — a returned `void` expression\n // is forbidden, but returning the read `unknown` is fine.\n // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -- read the void-typed listener's runtime value so it can be returned for central async isolation (#944/#1412)\n const result: unknown = listener({\n route: toState,\n previousRoute: fromState,\n });\n\n return result;\n },\n );\n }\n\n /**\n * End-user / UI-binding API for subscribing to **approved** route departures\n * (`LEAVE_APPROVED` phase): all `canDeactivate` guards have passed, but the\n * departure is **tentative, not committed** — an activation (`canActivate`)\n * guard can still reject (or the target route be removed mid-transition),\n * leaving the user on the current route (#932). Treat the leave as tentative\n * for non-idempotent side-effects and use the payload `signal` (which aborts\n * with the failure reason, #943) to roll back when the navigation does not\n * commit. Async listeners block the activation phase.\n *\n * @remarks\n *\n * **Duplicate-registration semantics — independent.** Each call pushes\n * `listener` onto the internal array; `router.subscribeLeave(fn)` twice\n * produces two entries and `fn` fires twice per leave. Each returned\n * `Unsubscribe` is **idempotent** (a `removed` flag, #1349) and removes\n * exactly ONE entry — the first still matching the reference (`indexOf`\n * semantic). So a repeated call of one unsubscribe is a true no-op and does\n * **not** touch the other registration; N subscribes + M *distinct*\n * unsubscribes leave N − M entries. Which physical entry survives is\n * irrelevant — the reference is the same.\n *\n * Contract differs from {@link addEventListener} (throws on duplicate).\n * For idempotent *registration* (one active subscription), gate at the call site.\n */\n subscribeLeave(listener: LeaveFn): Unsubscribe {\n // Same disposed-state enforcement as subscribe() (#946): a pre-bound\n // reference would otherwise push onto #leaveListeners after dispose() and\n // silently never fire (FSM is DISPOSED, no LEAVE_APPROVE emit).\n if (this.isDisposed()) {\n throw freezeThrownError(new RouterError(errorCodes.ROUTER_DISPOSED));\n }\n\n this.#leaveListeners.push(listener);\n\n // Idempotency flag (#1349), mirroring extendRouter / addInterceptor (#1198).\n // Without it, a double call would `indexOf(listener)` again and splice a\n // DUPLICATE registration of the same fn — silently deactivating another\n // subscriber whose own unsubscribe was never called. The `Unsubscribe`\n // contract names subscribeLeave as idempotent. (Unlike addInterceptor, the\n // `idx !== -1` guard stays: `dispose()` empties `#leaveListeners` via\n // `clearAll`, so an unsubscribe called after dispose reaches this with\n // idx === -1.)\n let removed = false;\n\n return () => {\n if (removed) {\n return;\n }\n\n removed = true;\n\n const idx = this.#leaveListeners.indexOf(listener);\n\n if (idx !== -1) {\n this.#leaveListeners.splice(idx, 1);\n }\n };\n }\n\n hasLeaveListeners(): boolean {\n return this.#leaveListeners.length > 0;\n }\n\n /**\n * True when a plugin listener runs in a PRE-COMMIT transition window where a\n * synchronous `stop()`/`dispose()` can supersede the in-flight navigation\n * before it commits — i.e. `onTransitionStart` (TRANSITION_START) or\n * `onTransitionLeaveApprove` (TRANSITION_LEAVE_APPROVE). (`subscribeLeave` is\n * tracked separately via `hasLeaveListeners`; `onTransitionSuccess` fires\n * post-commit and cannot cancel.) Read into the pre-`startTransition` liveness\n * snapshot so the commit-gate (#1169) fires for these windows too.\n */\n hasPreCommitListeners(): boolean {\n return (\n this.#emitter.listenerCount(events.TRANSITION_START) > 0 ||\n this.#emitter.listenerCount(events.TRANSITION_LEAVE_APPROVE) > 0\n );\n }\n\n awaitLeaveListeners(\n toState: State,\n fromState: State | undefined,\n signal: AbortSignal,\n ): Promise<void> | undefined {\n if (fromState === undefined) {\n return undefined;\n }\n\n // Freeze the payload wrapper so listeners cannot mutate it (`payload.route`\n // is already deep-frozen via the State immutability invariant; this closes\n // the wrapper-mutation gap surfaced by audit `probe-05-payload-frozen`).\n const leaveState: LeaveState = freeze({\n route: fromState,\n nextRoute: toState,\n signal,\n });\n\n let promises: Promise<void>[] | undefined;\n let firstSyncError: unknown;\n\n // Snapshot before iteration — a listener that reentrantly calls\n // `subscribeLeave(newFn)` or its own `unsubscribe()` must not affect the\n // current emit cycle. Symmetric with the EventEmitter snapshot invariant\n // (PR #666 / #659).\n const snapshot = [...this.#leaveListeners];\n\n // Elevated across the SYNC leave-listener dispatch: `isProcessing()` reads\n // it, so a sync subscribeLeave listener that calls navigate() is rejected\n // with REENTRANT_NAVIGATION at the facade (RFC §4). The `finally` restores it\n // before any async tail (returned promise) runs, so a DEFERRED navigate from\n // an async listener (after its first `await`) sees depth 0 and is allowed.\n this.#dispatchDepth++;\n\n try {\n for (const listener of snapshot) {\n try {\n const result = listener(leaveState);\n\n if (result !== undefined && typeof result.then === \"function\") {\n promises ??= [];\n promises.push(result);\n }\n } catch (error: unknown) {\n if (firstSyncError === undefined) {\n firstSyncError = error;\n }\n }\n }\n } finally {\n this.#dispatchDepth--;\n }\n\n if (promises === undefined) {\n if (firstSyncError !== undefined) {\n throw ensureError(firstSyncError);\n }\n\n return undefined;\n }\n\n return settleLeavePromises(promises, firstSyncError, signal);\n }\n\n clearAll(): void {\n this.#emitter.clearAll();\n this.#leaveListeners.length = 0;\n }\n\n setLimits(limits: { maxListeners: number; warnListeners: number }): void {\n this.#emitter.setLimits(limits);\n }\n\n /**\n * Injects the lazy validator accessor (wireNamespaces), mirroring\n * `PluginsNamespace` / `RouteLifecycleNamespace`. The closure reads the live\n * `RouterInternals.validator`, so a validation-plugin registered AFTER wiring\n * is still observed on the next `subscribe` / `addEventListener`.\n */\n setValidatorAccessor(getValidator: () => RouterValidator | null): void {\n this.#getValidator = getValidator;\n }\n\n // Single entry point for routing a cancel into the FSM `CANCEL` action — every\n // source comes through here: stop/dispose pass no reason, supersede and the\n // external `opts.signal` pass the abort reason (#943).\n //\n // The `canCancel()` ask is not a safety net — a `send` with no edge is a\n // silent no-op, and removing the ask reds nothing at 100 % coverage. It states\n // the intent at the one place every source passes, and it skips the payload\n // literal for the cancels that arrive out of band.\n //\n // ⚑ No TARGET is passed (#1671): `sendCancel` takes `fromState` for the event\n // it announces, and the navigation being cancelled is read by the action off\n // `ctx.inflight`, on an edge that only exists in-band — the function takes no\n // target, and the predicate carries only `canCancel()`.\n sendCancelIfPossible(fromState: State | undefined, reason?: unknown): void {\n if (!this.canCancel()) {\n return;\n }\n\n this.sendCancel(fromState, reason);\n }\n\n /**\n * Route an external `opts.signal` abort onto FSM `CANCEL`, for the WHOLE life\n * of the navigation (#1684) — the ONE implementation, called from two moments\n * (#1724).\n *\n * Registered for the WHOLE life of the navigation because a bridge that only\n * covers the parked arc leaves every synchronous abort unheard by the machine:\n * the navigation rejects correctly and the band stays in `LEAVE_APPROVED`,\n * with `isLeaveApproved()` lying and route-CRUD silently blocked (#1684).\n *\n * ⚑ **It lives HERE, and not in the pipeline, because the SCOPE belongs to the\n * band (#1716 / #1724).** The machine already owns closing it — the `CANCEL` /\n * `FAIL` / `COMPLETE` actions — so opening it from the `NAVIGATE` action is\n * what makes the lifetime symmetric, and it is what removes the pipeline's\n * last say in that lifetime. Registering a listener is an EFFECT, so the\n * action is its layer (RFC-10a §6.2: bookkeeping in `update`, effects in the\n * action), and writing `payload.detachExternalBridge` from here is the same\n * class as `handleCancel` writing `inflight.cancelReason`.\n *\n * ⚑ **Two callers, one owner of \"is a bridge already standing?\" — and the\n * single owner is load-bearing rather than tidy.** The two moments exist\n * because `hasGuards` is not knowable at the announce (`planPhases` runs after\n * `startTransition`, since a `TRANSITION_START` listener may still register a\n * guard), so the pipeline still asks for the late one through\n * `NavigationDependencies.bridgeExternalSignal`. While BOTH the caller and\n * this function tested the flag, the early-return below was structurally\n * unreachable and coverage fell to 99.95 % pointing straight at it. With one\n * owner the same branch is taken by the ordinary arc that reaches both moments\n * — a pre-commit listener AND a guard — and a second registration would ORPHAN\n * the first, leaking a listener on the caller's own controller.\n *\n * `fromState` comes off the machine's own context rather than from the\n * wiring.\n */\n bridgeExternalSignal(payload: RouterPayloads[\"NAVIGATE\"]): ScopeDecision {\n const signal = payload.externalSignal;\n\n if (signal === undefined || payload.detachExternalBridge !== undefined) {\n return SCOPE_DECIDED_TOKEN;\n }\n\n const onExternalAbort = (): void => {\n // No direct `controller.abort()` here — \"FSM CANCEL ⟹ controller aborted\"\n // lives in one place (`handleCancel`), which also returns the machine to\n // READY and emits `TRANSITION_CANCEL`, atomically (#1030). `reason`\n // surfaces via the leave signal (#943).\n this.sendCancelIfPossible(this.#fsm.getContext().current, signal.reason);\n };\n\n // ⚑ ONE expression, and that is what holds \"register, THEN record\": the\n // closer is the registration's RETURN VALUE, so no moment exists at which a\n // closer stands and the listener does not (`bridgeSignal` above has what\n // the two-statement form cost). The `onClosed` argument is the self-clearing\n // half, written here because this caller owns the field. Pinned by\n // `bridge-registration-order-1724.test.ts`, which COUNTS\n // `removeEventListener` — the balance discriminates, the outcome does not.\n payload.detachExternalBridge = bridgeSignal(signal, onExternalAbort, () => {\n payload.detachExternalBridge = undefined;\n });\n\n return SCOPE_DECIDED_TOKEN;\n }\n\n /**\n * Proactive listener-count threshold (#1188) — mirrors the plugins /\n * lifecycle / dependencies counters. Opt-in: the emitter's per-event count is\n * read ONLY when the validator is installed, so the bare-core hot path pays\n * nothing. `count` is the POST-add size (`listenerCount + 1`), matching\n * `RouteLifecycleNamespace`'s `count + 1`, so warn/error fire exactly when the\n * new listener reaches the threshold. Core keeps the emitter's bare-`Error`\n * hard cap; this only surfaces an actionable signal well before it.\n */\n #checkListenerThreshold(eventName: EventName, methodName: string): void {\n const validator = this.#getValidator?.();\n\n if (validator) {\n validator.eventBus.validateCountThresholds(\n this.#emitter.listenerCount(eventName) + 1,\n eventName,\n methodName,\n );\n }\n }\n\n /**\n * Why the table refused, said out loud (#1644).\n *\n * The gate this ask replaced (#1186) was `!isActive()`, i.e. IDLE or DISPOSED\n * — so `ROUTER_DISPOSED` was very nearly true wherever it fired. The table's\n * `canSend(SYSTEM_COMMIT)` is a different question: the edge is declared on\n * `READY` alone, so it also refuses while starting and mid-transition, on a\n * router that `isActive()` reports as live. Keeping the old code there told\n * callers the router was terminated when it was merely busy.\n *\n * Two codes, split by what the caller can DO about it: `ROUTER_DISPOSED` is\n * terminal, everything else is transient. No code in the registry says\n * \"mid-transition\", so the phase rides the message rather than growing the\n * public `errorCodes` surface for one internal refusal.\n *\n * ⚑ The BOOT window is a third phase (#1647), and `isStarting()` is the whole\n * distinction — an ordinary never-started router keeps the plain sentence.\n * Named here, where the phase is already known, rather than by a predicate one\n * layer up that would repeat this ask's own refusal with a worse message.\n */\n #refuseSystemCommit(): RouterError {\n if (this.isDisposed()) {\n return new RouterError(errorCodes.ROUTER_DISPOSED);\n }\n\n let phase: string;\n\n if (this.isTransitioning()) {\n phase =\n \"[router] cannot commit a state while a transition is in flight — the navigation in progress commits its own\";\n } else if (this.isStarting()) {\n phase =\n \"[router] cannot commit before the start navigation does — the boot would overwrite it; await start() first\";\n } else {\n phase = \"[router] cannot commit a state before the router has started\";\n }\n\n return new RouterError(errorCodes.ROUTER_NOT_STARTED, { message: phase });\n }\n\n #setupFSMActions(): void {\n const fsm = this.#fsm;\n\n fsm.on(routerStates.STARTING, routerEvents.STARTED, () => {\n this.emitRouterStart();\n });\n\n fsm.on(routerStates.READY, routerEvents.STOP, () => {\n this.emitRouterStop();\n });\n\n // NAVIGATE / LEAVE_APPROVE / COMPLETE emit their transition event as the FSM\n // action (payload = the transition states), so `send()` from an invalid\n // state (e.g. COMPLETE from IDLE after a listener's stop()/dispose()) is a\n // table no-op that emits nothing — the FSM table is the sole authority over\n // state, no `forceState` resurrection (#1169 D-full). NAVIGATE fires from\n // READY plus the TRANSITION_STARTED / LEAVE_APPROVED self-loops (supersede).\n const emitNavigate = (payload: RouterPayloads[\"NAVIGATE\"]): void => {\n // ⚑ **OPENING the cancellability scope is this edge's job (#1724).** The\n // action runs after the edge's `update` and before `emitTransitionStart`,\n // so a plugin's `onTransitionStart` is still covered — measured, the\n // bridge fires from inside the announce 4 times across the tier. And a\n // `NAVIGATE` the table REFUSES runs no action, so a born-dead navigation\n // registers nothing and has nothing to close, which is what retired the\n // pipeline's last closing site (#1688). ⚠ Note that \"the edge fired\" is a\n // WIDER set than \"`sendNavigate` returned true\": `FSM.send` reports the\n // state after the action AND the listeners, so 9 navigations of the tier\n // register here and still see `false` — for those the `CANCEL` that moved\n // the machine closes the scope on its way out.\n //\n // ⚑ Conditional (#1690): registering unconditionally measures\n // **+23…30 %** on the guard-free, listener-free\n // arc, and `bridge-only-when-the-band-can-abort-1690` plus two siblings\n // red without the condition. `externalSignal` is tested first so cut A\n // short-circuits before two `listenerCount` reads.\n //\n // ⚑ **The order below is held by the TYPE (#1724).** `emitTransitionStart`\n // demands a `ScopeDecision`, which exists only as the result of deciding\n // the scope's fate, so announcing first is `TS2448` — a compile-time lock\n // for the same reason `CommitPermit` is one: the failure it prevents is\n // SILENT (a bridge registered below the announce misses an abort raised\n // inside it, and `addEventListener` never fires retroactively).\n // ⚠ \"Decided\" is not \"a bridge stands\": the `SCOPE_DECIDED_TOKEN` arm is\n // the decision that this navigation needs none, and cut A takes it.\n const scope: ScopeDecision =\n payload.externalSignal !== undefined &&\n (this.hasLeaveListeners() || this.hasPreCommitListeners())\n ? this.bridgeExternalSignal(payload)\n : SCOPE_DECIDED_TOKEN;\n\n this.emitTransitionStart(payload.toState, payload.fromState, scope);\n };\n\n fsm.on(routerStates.READY, routerEvents.NAVIGATE, emitNavigate);\n fsm.on(\n routerStates.TRANSITION_STARTED,\n routerEvents.NAVIGATE,\n emitNavigate,\n );\n fsm.on(routerStates.LEAVE_APPROVED, routerEvents.NAVIGATE, emitNavigate);\n\n fsm.on(\n routerStates.TRANSITION_STARTED,\n routerEvents.LEAVE_APPROVE,\n (payload) => {\n this.emitTransitionLeaveApprove(payload.toState, payload.fromState);\n },\n );\n\n fsm.on(routerStates.LEAVE_APPROVED, routerEvents.COMPLETE, (payload) => {\n // ⚑ Close the scope (#1716). Read off the PAYLOAD and not the context:\n // this edge's `update` (`commitNavigation`) clears `inflight` before the\n // action runs, which is exactly why `CANCEL` / `FAIL` — this row's edges\n // with no `update` — read the context instead. (Row-scoped, not\n // tree-wide: the table carries other update-less edges elsewhere.)\n payload.detachExternalBridge?.();\n\n // Subscribers never see the caller's `AbortSignal`: it is an input to the\n // navigation, not part of what was committed. The TABLE does see it —\n // `mayCommit` refuses a commit whose signal was aborted — through\n // `payload.externalSignal`, the snapshot taken at the entry.\n //\n // ⚑ The strip lives at the ENTRY door, not here (#1962). Here it would\n // sit in the LAST reader of `opts` and decide — with a ternary on\n // `externalSignal` — whether plugins get the application's own object or\n // a copy of it, discriminated by a signal the plugin never sees.\n // `payload.opts` is core's own frozen record on every arc, made by one\n // walk above every other read, so there is nothing to decide.\n this.emitTransitionSuccess(\n payload.toState,\n payload.fromState,\n payload.opts,\n );\n });\n\n const handleCancel = (payload: RouterPayloads[\"CANCEL\"]) => {\n const { fromState, reason } = payload;\n // In-band by construction: CANCEL is declared on TRANSITION_STARTED /\n // LEAVE_APPROVED only, and `inflight` is written on entry to the\n // band and no longer cleared on the way out (#1671), so the target is\n // always here. Same shape as the wiring-guaranteed `lifecycleNamespace!`\n // in `api/` — an invariant the type system cannot carry.\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- in-band by construction: CANCEL is declared on TRANSITION_STARTED / LEAVE_APPROVED only, and `inflight` is written on entry to the band and no longer cleared on the way out (#1671); widening `emitTransitionCancel` instead would push `undefined` into the public `onTransitionCancel` hook for a case that cannot occur\n const inflight = this.#fsm.getContext().inflight!;\n\n // (RFC navigation-cancellation-unification §5): the FSM CANCEL\n // action OWNS the abort. Aborting the in-flight controller wakes the parked\n // async pipeline (#1018) and rejects navigate() — the post-race local\n // isActive() sees signal.aborted; `reason` surfaces as the leave signal's\n // reason (#943). Order: (i) abort (wake) then (ii) emit TRANSITION_CANCEL.\n // No cycle: onInternalAbort is wake-only, it does not re-enter cancel.\n //\n // ⚑ Read straight off the navigation the machine is carrying (#1684).\n // Going out through an injected `abortController` effect to a\n // router-level slot in `InFlightNavigation` puts it out of reach: the\n // pipeline nulls that slot BEFORE the commit on every synchronous arc, so\n // this line finds nothing and the abort arrives AFTER the emit below,\n // inverting the order stated above. The controller is a field of\n // `ctx.inflight`, so there is no second slot to fall out of step with.\n // `?.` because allocating one is conditional (cut A allocates none).\n const cancelReason =\n reason ?? new RouterError(errorCodes.TRANSITION_CANCELLED);\n\n // ⚑ RECORD first, abort second (#1706). The controller is allocated\n // lazily by whichever consumer needs a signal, so `?.` here is not \"no\n // controller, nothing to do\" — it is \"the consumer has not opened one\n // YET\", and the one it opens moments later would be born unaborted. The\n // record costs no allocation, which is what keeps cut A and the\n // born-dead arcs at zero controllers; `openController` replays it.\n inflight.cancelReason = cancelReason;\n inflight.controller?.abort(cancelReason);\n\n // ⚑ Closing the cancellability scope is this edge's job (#1716). BEFORE\n // the emit deliberately: no observer of\n // `TRANSITION_CANCEL` may find a live bridge on a navigation the machine\n // has already declared over.\n inflight.detachExternalBridge?.();\n\n this.emitTransitionCancel(inflight.toState, fromState);\n };\n\n fsm.on(routerStates.TRANSITION_STARTED, routerEvents.CANCEL, handleCancel);\n fsm.on(routerStates.LEAVE_APPROVED, routerEvents.CANCEL, handleCancel);\n\n // The SYSTEM_COMMIT action does BOTH halves — the write and the announce —\n // so neither happens outside the table. It sits on ONE edge since the\n // `STARTING` one was removed; kept as a named function rather than inlined\n // so the registration reads like its `emitNavigate` / `handleCancel`\n // siblings.\n const handleSystemCommit = (\n payload: RouterPayloads[\"SYSTEM_COMMIT\"],\n ): void => {\n // The WRITE is the edge's `update`; the action only announces. Same\n // layering as every other transition — bookkeeping in `update`, effects in\n // the action (RFC-10a §6.2).\n this.emitTransitionSuccess(\n payload.toState,\n payload.fromState,\n payload.opts,\n );\n };\n\n fsm.on(routerStates.READY, routerEvents.SYSTEM_COMMIT, handleSystemCommit);\n\n // ⚑ The FAIL action is SPLIT BY EDGE (#1671), and that split is what let\n // `toState` leave the payload. The two in-band edges report a navigation's\n // failure, so the target is the machine's own `inflight` — measured\n // identical to what the payload carried on every one of 206 in-band FAILs\n // across the functional tier. `STARTING --FAIL--> IDLE` is not a navigation\n // failure at all: it is how a thrown `start()` unwinds, both of its senders\n // pass `undefined` today, and reading the context there would name whatever\n // a previously CANCELLED navigation left behind. The table now carries that\n // distinction instead of the caller.\n //\n // ⚠ Reading the context here is only safe while EVERY sender that reaches\n // an in-band edge names its navigation — the split is by EDGE, and the edge\n // cannot tell a navigation's failure from a report that merely happened\n // during one. A `ROUTE_NOT_FOUND` sent from `RouterLifecycleNamespace.start`\n // through the table would be exactly that second thing: a `start()` resuming\n // inside the band would report the LIVE navigation's target as the thing\n // that failed, and take the band away from it. No such sender exists —\n // `#unwindFailedStart` reports instead — which keeps this line honest. See `mayFail` on what a new one would cost.\n const emitNavigationFail = (payload: RouterPayloads[\"FAIL\"]): void => {\n const inflight = this.#fsm.getContext().inflight;\n\n // ⚑ Close the scope (#1716) — same position and same reasoning as the\n // `CANCEL` action above. Registered on the two IN-BAND edges only, which\n // is what makes the read safe; `STARTING --FAIL--> IDLE` has its own\n // action precisely because it is not a navigation's failure.\n inflight?.detachExternalBridge?.();\n\n this.emitTransitionError(\n inflight?.toState,\n payload.fromState,\n payload.error as RouterError | undefined,\n );\n };\n\n fsm.on(routerStates.LEAVE_APPROVED, routerEvents.FAIL, emitNavigationFail);\n fsm.on(\n routerStates.TRANSITION_STARTED,\n routerEvents.FAIL,\n emitNavigationFail,\n );\n\n fsm.on(routerStates.STARTING, routerEvents.FAIL, (payload) => {\n this.emitTransitionError(\n undefined,\n payload.fromState,\n payload.error as RouterError | undefined,\n );\n });\n }\n}\n","// packages/core/src/namespaces/RouterLifecycleNamespace/constants.ts\n\nimport { errorCodes } from \"../../constants\";\nimport { RouterError } from \"../../RouterError\";\n\n// =============================================================================\n// Cached Errors (Performance Optimization)\n// =============================================================================\n// Pre-create error instances to avoid object allocation on hot paths.\n// Error creation involves: new object, stack trace capture (~500ns-2μs).\n// Cached errors skip this overhead entirely.\n//\n// Trade-off: All error instances share the same stack trace (points here).\n// This is acceptable because:\n// 1. These errors indicate user misconfiguration, not internal bugs\n// 2. Error code and message are sufficient for debugging\n// 3. Performance gain (~80% for error paths) outweighs stack trace loss\n// =============================================================================\n\n/**\n * Cached error for start() called when router is already started/starting.\n */\nexport const CACHED_ALREADY_STARTED_ERROR = new RouterError(\n errorCodes.ROUTER_ALREADY_STARTED,\n);\n// #1606's backstop, applied to the fifth cached error — the one its sweep could\n// not see, because that sweep was scoped to `NavigationNamespace/constants.ts`\n// and this instance lives here. Same reasoning verbatim: it is handed to\n// arbitrary consumer code process-wide (every `.catch()` on `start()`), so an\n// in-place write rewrites the error every OTHER consumer sees, across routers\n// and — under SSR — across requests (#1960).\nObject.freeze(CACHED_ALREADY_STARTED_ERROR);\n","import type {\n EventEmitterLimits,\n EventEmitterOptions,\n Unsubscribe,\n} from \"./types\";\n\nconst DEFAULT_LIMITS: EventEmitterLimits = {\n maxListeners: 0,\n warnListeners: 0,\n};\n\n// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\ntype AnyCallback = Function;\n\n/**\n * Generic typed event emitter with listener limits, duplicate detection,\n * re-entrancy coalescing, and per-listener error isolation.\n *\n * All limits are opt-in via constructor options.\n */\nexport class EventEmitter<TEventMap extends Record<string, unknown[]>> {\n readonly #callbacks = new Map<string, Set<AnyCallback>>();\n // Names currently being dispatched. A re-entrant `emit` of an event already\n // on this set is coalesced to a no-op (see `emit`), so an event can never\n // re-enter its own dispatch — recursion is structurally impossible (depth ≤ 1)\n // with no depth bound and no stack-overflow path (#1033).\n readonly #dispatching = new Set<string>();\n #warnedEvents: Set<string> | null = null;\n #limits: EventEmitterLimits = DEFAULT_LIMITS;\n readonly #onListenerError:\n ((eventName: string, error: unknown) => void) | null;\n readonly #onListenerWarn: ((eventName: string, count: number) => void) | null;\n\n constructor(options?: EventEmitterOptions) {\n if (options?.limits) {\n this.#limits = options.limits;\n }\n\n this.#onListenerError = options?.onListenerError ?? null;\n this.#onListenerWarn = options?.onListenerWarn ?? null;\n }\n\n /**\n * Validates that a callback is a function.\n */\n static validateCallback(\n cb: unknown,\n eventName: string,\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n ): asserts cb is Function {\n if (typeof cb !== \"function\") {\n throw new TypeError(\n `Expected callback to be a function for event ${eventName}`,\n );\n }\n }\n\n /**\n * Replaces current limits with the provided limits.\n */\n setLimits(limits: EventEmitterLimits): void {\n this.#limits = limits;\n }\n\n /**\n * Adds an event listener and returns an unsubscribe function.\n * Throws on duplicate listeners or when maxListeners is reached.\n *\n * Registration is atomic (validate-before-mutate, #1358): every rejection\n * check runs against the CURRENT record (read once, never created early), the\n * advisory warn hook runs before any mutation, and the record is created +\n * the listener added only after all checks pass. So a throw — a rejected\n * limit, or a throwing `onListenerWarn` — leaves NO side-effect behind: no\n * orphaned empty record (#1167) and no burnt warn latch (#1168).\n */\n on<E extends keyof TEventMap & string>(\n eventName: E,\n cb: (...args: TEventMap[E]) => void,\n ): Unsubscribe {\n const existing = this.#callbacks.get(eventName);\n const size = existing?.size ?? 0;\n\n if (existing?.has(cb)) {\n throw new Error(`Duplicate listener for \"${eventName}\"`);\n }\n\n const { maxListeners, warnListeners } = this.#limits;\n\n // Enforce the hard limit before warning, so onListenerWarn never fires for\n // a registration that then throws (the warnListeners === maxListeners case).\n if (maxListeners !== 0 && size >= maxListeners) {\n throw new Error(\n `Listener limit (${maxListeners}) reached for \"${eventName}\"`,\n );\n }\n\n // Warn at most once per emitter+event, using the PRE-add size. The hook is\n // invoked first and the latch set only after it returns without throwing, so\n // a throwing hook fails the registration atomically and leaves the latch\n // unspent — the next (W+1)th registration warns as documented (#1168). The\n // latch keeps the advisory hint \"exactly once\" across off/on churn around\n // the threshold; reset by clearAll() or by removing the last listener.\n if (\n warnListeners !== 0 &&\n size === warnListeners &&\n this.#onListenerWarn !== null\n ) {\n this.#warnedEvents ??= new Set();\n\n if (!this.#warnedEvents.has(eventName)) {\n this.#onListenerWarn(eventName, warnListeners);\n this.#warnedEvents.add(eventName);\n }\n }\n\n // Mutate last — create the record only now, so a rejected registration\n // above never strands an empty record (#1167).\n let set = existing;\n\n if (set === undefined) {\n set = new Set();\n this.#callbacks.set(eventName, set);\n }\n\n set.add(cb);\n\n return () => {\n this.off(eventName, cb);\n };\n }\n\n /**\n * Removes an event listener.\n */\n off<E extends keyof TEventMap & string>(\n eventName: E,\n cb: (...args: TEventMap[E]) => void,\n ): void {\n const set = this.#callbacks.get(eventName);\n\n if (!set) {\n return;\n }\n\n set.delete(cb);\n\n if (set.size === 0) {\n // Release per-event records once the last listener is gone, so consumers\n // with dynamic event names don't accumulate empty Sets unbounded\n // (listenerCount stays 0 either way, masking the growth). See #750.\n this.#callbacks.delete(eventName);\n this.#warnedEvents?.delete(eventName);\n }\n }\n\n /**\n * Emits an event, calling all registered listeners with the provided args.\n *\n * Uses snapshot iteration — listeners added/removed during emit don't affect\n * the current invocation. Per-listener errors are caught and reported via the\n * `onListenerError` callback; other listeners still run.\n *\n * Re-entrant emit is coalesced: emitting an event that is already being\n * dispatched (a listener that synchronously re-emits the same event) is a\n * no-op, so dispatch never recurses into itself (#1033).\n *\n * Uses explicit params instead of rest params to avoid V8 array materialization.\n * Extra undefined args are harmless — JS functions ignore extra arguments.\n */\n emit(\n eventName: keyof TEventMap & string,\n arg1?: unknown,\n arg2?: unknown,\n arg3?: unknown,\n arg4?: unknown,\n ): void {\n const set = this.#callbacks.get(eventName);\n\n if (!set || set.size === 0) {\n return;\n }\n\n // Coalesce a re-entrant emit of an in-flight event (depth ≤ 1, #1033).\n if (this.#dispatching.has(eventName)) {\n return;\n }\n\n // arguments.length is O(1) in V8 strict mode — no deopt\n const argc = arguments.length - 1;\n\n this.#dispatching.add(eventName);\n\n try {\n // Single-listener fast path — skip the [...set] snapshot allocation.\n if (set.size === 1) {\n const [cb] = set;\n\n this.#invokeIsolated(eventName, cb, argc, arg1, arg2, arg3, arg4);\n } else {\n const listeners = [...set];\n\n for (const cb of listeners) {\n this.#invokeIsolated(eventName, cb, argc, arg1, arg2, arg3, arg4);\n }\n }\n } finally {\n this.#dispatching.delete(eventName);\n }\n }\n\n /**\n * Removes all listeners and resets the warn latch.\n *\n * Does NOT touch `#dispatching`: the in-flight coalesce guard is owned by the\n * active `emit` frame (added when dispatch starts, self-released in that\n * frame's `finally`). Clearing it here would lift the guard for a live frame\n * when `clearAll()` runs from inside a listener, so a re-entrant same-event\n * emit would no longer coalesce and would re-enter — violating the depth-≤-1\n * contract (#1164). The guard self-releases; `clearAll()` has no business\n * sweeping state owned by active emit frames.\n */\n clearAll(): void {\n this.#callbacks.clear();\n this.#warnedEvents = null;\n }\n\n /**\n * Returns the number of listeners for the given event.\n */\n listenerCount(eventName: keyof TEventMap & string): number {\n return this.#callbacks.get(eventName)?.size ?? 0;\n }\n\n /**\n * Returns whether the given event is currently being dispatched (an `emit`\n * for it is on the stack). Single source of truth for \"is this event\n * in-flight\" — consumers read it to reject re-entrant operations that would\n * trigger such an emit (the emit itself would be coalesced regardless).\n */\n isDispatching(eventName: keyof TEventMap & string): boolean {\n return this.#dispatching.has(eventName);\n }\n\n // ===========================================================================\n // Private methods\n // ===========================================================================\n\n /**\n * Calls a listener with the correct number of arguments.\n * Dispatches by argc to preserve exact call semantics.\n */\n #invokeIsolated(\n eventName: keyof TEventMap & string,\n cb: AnyCallback,\n argc: number,\n arg1: unknown,\n arg2: unknown,\n arg3: unknown,\n arg4: unknown,\n ): void {\n try {\n const result = this.#callListener(cb, argc, arg1, arg2, arg3, arg4);\n\n // A listener typed `=> void` may still return a Promise at runtime (an\n // async hook or any-cast misuse). The sync `catch` below cannot see its\n // rejection, so route it to the same `#onListenerError` sink — otherwise\n // it escapes as a Node `unhandledRejection` (fatal under\n // `--unhandled-rejections=strict`, the Node 22+ default). Centralised here\n // so every listener kind (plugin hooks, `subscribe`, …) is isolated\n // symmetrically (#1412; `subscribe`'s per-site #944 wrapper folds in).\n //\n // ⚑ ONE read of `.then`, and the function the check judged is the function\n // that runs (#2136). `Promise.resolve` re-asks the slot to adopt a\n // thenable, so an object whose `then` answered differently per read was\n // adopted on one value and invoked on another — and a later non-function\n // read made it a plain value, with its rejection reaching nobody.\n //\n // ⚠ The captured `then` goes to a PROMISE, not to the sink directly: the\n // wrapper is what keeps `#onListenerError` outside this `try`, and it\n // costs one extra promise for a listener that returns a thenable. Calling\n // it directly cost three properties of the async arm instead, all pinned\n // by `a thenable's rejection reaches the sink OUTSIDE the protected\n // region (#2136)`.\n const then: unknown = (result as { then?: unknown } | null | undefined)\n ?.then;\n\n if (typeof then === \"function\") {\n new Promise<unknown>((resolve, reject) => {\n (\n then as (\n onFulfilled: (value: unknown) => void,\n onRejected: (error: unknown) => void,\n ) => unknown\n ).call(result, resolve, reject);\n }).catch((error: unknown) => {\n this.#onListenerError?.(eventName, error);\n });\n }\n } catch (error) {\n this.#onListenerError?.(eventName, error);\n }\n }\n\n #callListener(\n cb: AnyCallback,\n argc: number,\n arg1: unknown,\n arg2: unknown,\n arg3: unknown,\n arg4: unknown,\n ): unknown {\n switch (argc) {\n case 0: {\n return (cb as () => unknown)();\n }\n case 1: {\n return (cb as (a: unknown) => unknown)(arg1);\n }\n case 2: {\n return (cb as (a: unknown, b: unknown) => unknown)(arg1, arg2);\n }\n case 3: {\n return (cb as (a: unknown, b: unknown, c: unknown) => unknown)(\n arg1,\n arg2,\n arg3,\n );\n }\n default: {\n return (\n cb as (a: unknown, b: unknown, c: unknown, d: unknown) => unknown\n )(arg1, arg2, arg3, arg4);\n }\n }\n }\n\n // (record creation is inlined into `on()` so a rejected registration never\n // creates one — see the atomicity note there, #1167/#1358.)\n}\n","import type { LogLevel, LogLevelConfig } from \"../../types\";\n\n/**\n * Numeric mapping for log message severity levels.\n *\n * Maps each severity level to a numeric value for threshold comparison.\n * Lower values = lower severity, higher values = higher severity.\n *\n * Used internally to determine if a message should be filtered based on\n * the configured threshold level.\n *\n * Mapping:\n * - `log`: 0 (lowest severity - informational)\n * - `warn`: 1 (medium severity - warnings)\n * - `error`: 2 (highest severity - critical errors)\n *\n * @example\n * ```ts\n * const messageLevel = LOG_LEVELS['warn']; // 1\n * const threshold = 2; // error-only\n * const shouldFilter = messageLevel < threshold; // true (warn is filtered)\n * ```\n *\n * @internal This is used for internal filtering logic\n */\nexport const LOG_LEVELS: Record<LogLevel, number> = Object.freeze({\n log: 0,\n warn: 1,\n error: 2,\n});\n\n/**\n * Numeric thresholds for logger configuration levels.\n *\n * Maps each configuration level to a minimum threshold value.\n * Messages with a severity level below this threshold are filtered out.\n *\n * Threshold logic:\n * - A message is shown if: `LOG_LEVELS[messageLevel] >= LEVEL_CONFIGS[configLevel]`\n * - Higher threshold value = stricter filtering = fewer messages shown\n *\n * Mapping:\n * - `all`: 0 (no filtering - show everything)\n * - Shows: log (0), warn (1), error (2) ✓\n * - `warn-error`: 1 (filter log messages)\n * - Shows: warn (1), error (2) ✓\n * - Filters: log (0) ✗\n * - `error-only`: 2 (filter log and warn messages)\n * - Shows: error (2) ✓\n * - Filters: log (0), warn (1) ✗\n * - `none`: 3 (filter all messages - complete silence)\n * - Filters: log (0), warn (1), error (2) ✗\n *\n * @example\n * ```ts\n * // Configuration: warn-error\n * const threshold = LEVEL_CONFIGS['warn-error']; // 1\n *\n * // Check if 'log' message should be shown\n * LOG_LEVELS['log'] >= threshold // 0 >= 1 = false (filtered)\n *\n * // Check if 'warn' message should be shown\n * LOG_LEVELS['warn'] >= threshold // 1 >= 1 = true (shown)\n *\n * // Check if 'error' message should be shown\n * LOG_LEVELS['error'] >= threshold // 2 >= 1 = true (shown)\n * ```\n *\n * @internal This is used for internal threshold comparison\n */\nexport const LEVEL_CONFIGS: Record<LogLevelConfig, number> = Object.freeze({\n all: 0,\n \"warn-error\": 1,\n \"error-only\": 2,\n none: 3,\n});\n","// packages/core/src/utils/logger/RouterLogger.ts\n\nimport { LOG_LEVELS, LEVEL_CONFIGS } from \"./constants\";\nimport { assertLoggerConfig } from \"../../guards\";\n\nimport type {\n LogLevel,\n LoggerConfig,\n LogLevelConfig,\n LogCallback,\n} from \"../../types\";\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 hasOwn = Object.hasOwn;\n\n/**\n * Internal config type with required callbackIgnoresLevel\n * (always initialized to false)\n */\ninterface InternalLoggerConfig {\n level: LogLevelConfig;\n callback?: LogCallback | undefined;\n callbackIgnoresLevel: boolean;\n}\n\n/**\n * Logger class for centralized logging with configurable levels and callbacks.\n *\n * Features:\n * - Three log levels: log, warn, error\n * - Configurable threshold filtering (all, warn-error, error-only, none)\n * - Optional callback for custom log processing\n * - Callback can optionally ignore level threshold\n * - Context-based message formatting\n *\n * @example\n * ```ts\n * import { logger } from './Logger';\n *\n * // Configure logger\n * logger.configure({ level: 'warn-error' });\n *\n * // Use logger\n * logger.log('Router', 'Navigation started'); // Won't show (below threshold)\n * logger.warn('Router', 'Deprecated API used'); // Will show\n * ```\n */\nexport class RouterLogger {\n /** Internal configuration storage using private field */\n readonly #config: InternalLoggerConfig = {\n level: \"all\",\n // ⚑ Declared here even though the type makes it optional (#2138). `callback`\n // is both WRITTEN and READ on this record — `configure` assigns it and\n // `getConfig` returns it — so an absent own key sends both through the\n // prototype: an ambient `Object.prototype.callback` setter swallows the\n // sink, and a getter-only one makes an ordinary `configure` call THROW.\n // Present as an own key, neither can be reached.\n callback: undefined,\n callbackIgnoresLevel: false,\n };\n\n /** Cached numeric threshold value for performance (avoids repeated lookups) */\n #currentThreshold = 0;\n\n /**\n * Re-entrancy guard: true while a user callback is executing. Prevents a\n * callback that itself calls `logger.*` from recursing back through\n * `#invokeCallback` (which would otherwise spin ~5.9k deep until a swallowed\n * RangeError, see #791). Console output is unaffected.\n */\n #inCallback = false;\n\n /**\n * @param config - Optional initial configuration (level / callback /\n * callbackIgnoresLevel), applied once at construction.\n *\n * Each router owns its own `RouterLogger` instance, built from\n * `options.logger` in the `Router` constructor. This replaces the former\n * process-global singleton whose `configure()` leaked across every router in\n * the process — the last `createRouter` won (#724).\n */\n constructor(config?: Partial<LoggerConfig>) {\n if (config) {\n this.configure(config);\n }\n }\n\n /**\n * Configures the logger with new settings.\n *\n * @param config - Partial configuration to merge with existing config\n * @param config.level - Minimum log level to output ('all' | 'warn-error' | 'error-only' | 'none')\n * @param config.callback - Optional callback function to receive log messages\n * @param config.callbackIgnoresLevel - If true, callback receives all messages regardless of level\n *\n * @example\n * ```ts\n * // Set minimum level to warnings\n * logger.configure({ level: 'warn-error' });\n *\n * // Add custom callback that ignores level\n * logger.configure({\n * callback: (level, context, message) => {\n * sendToAnalytics({ level, context, message });\n * },\n * callbackIgnoresLevel: true\n * });\n * ```\n */\n configure(config: Partial<LoggerConfig>): void {\n // ⚑ Validated HERE, through the same door the constructor uses (#1814 /\n // #1842). This method is PUBLIC, so it cannot assume its input was screened\n // — and reading the caller's object independently of `assertLoggerConfig`\n // lets a field answer one thing to the type check and another to the store.\n // The guard reads each field once and hands back core's own record;\n // everything below reads THAT.\n //\n // ⚠ \"Read each field ONCE into a local — an unstable getter must not be\n // re-read between validation and storage (#1162)\" is NOT the mechanism:\n // reading once into a local pins the REFERENCE, not the checked value. The\n // `typeof` gate lives in `guards.ts` and does not reach here, and the pair\n // below would coerce `level` twice MORE on its own —\n // `hasOwn(LEVEL_CONFIGS, level)` computes a property key and\n // `LEVEL_CONFIGS[level]` computes it again. Measured: a bag whose `toString`\n // answers a valid level and then a bogus one passes the membership test and\n // indexes `undefined`, so `#currentThreshold` is never set and\n // `level: \"none\"` — the setting that suppresses everything — lets warnings\n // through, with no error anywhere.\n const validated = assertLoggerConfig(config);\n const level = validated.level;\n\n if (level !== undefined) {\n this.#config.level = level;\n this.#currentThreshold = LEVEL_CONFIGS[level];\n }\n\n // `hasOwn`, not `!== undefined`: an explicit `callback: undefined` CLEARS\n // the sink, and the normalised record carries presence for exactly that.\n if (hasOwn(validated, \"callback\")) {\n this.#config.callback = validated.callback;\n }\n\n if (validated.callbackIgnoresLevel !== undefined) {\n this.#config.callbackIgnoresLevel = validated.callbackIgnoresLevel;\n }\n }\n\n /**\n * Returns the current logger configuration.\n *\n * @returns Current configuration object with level, callback, and callbackIgnoresLevel\n *\n * @example\n * ```ts\n * const config = logger.getConfig();\n * console.log(config.level); // 'warn'\n * console.log(config.callbackIgnoresLevel); // false\n * ```\n */\n getConfig(): LoggerConfig {\n return {\n level: this.#config.level,\n callback: this.#config.callback,\n callbackIgnoresLevel: this.#config.callbackIgnoresLevel,\n };\n }\n\n /**\n * Logs an informational message at 'log' level.\n *\n * This is the lowest severity level. Messages are shown when level is 'all'.\n *\n * @param context - Context identifier (e.g., 'Router', 'Plugin')\n * @param message - Main log message\n * @param args - Additional arguments to log (objects, arrays, etc.)\n *\n * @example\n * ```ts\n * logger.log('Router', 'Navigation started', { from: '/home', to: '/about' });\n * // Output: [Router] Navigation started { from: '/home', to: '/about' }\n * ```\n */\n log(context: string, message: string, ...args: unknown[]): void {\n this.#writeLog(\"log\", context, message, args);\n }\n\n /**\n * Logs a warning message at 'warn' level.\n *\n * Use for deprecation notices, non-critical issues, or potential problems.\n * Messages are shown when level is 'all' or 'warn-error'.\n *\n * @param context - Context identifier (e.g., 'Router', 'Plugin')\n * @param message - Warning message\n * @param args - Additional arguments to log\n *\n * @example\n * ```ts\n * logger.warn('Router', 'Using deprecated API', { method: 'oldNavigate' });\n * // Output: [Router] Using deprecated API { method: 'oldNavigate' }\n * ```\n */\n warn(context: string, message: string, ...args: unknown[]): void {\n this.#writeLog(\"warn\", context, message, args);\n }\n\n /**\n * Logs an error message at 'error' level.\n *\n * Use for critical errors, exceptions, or failures that require attention.\n * Messages are shown when level is 'all', 'warn-error', or 'error-only'.\n *\n * @param context - Context identifier (e.g., 'Router', 'Plugin')\n * @param message - Error message\n * @param args - Additional arguments to log (often error objects)\n *\n * @example\n * ```ts\n * logger.error('Router', 'Navigation failed', new Error('Route not found'));\n * // Output: [Router] Navigation failed Error: Route not found\n * ```\n */\n error(context: string, message: string, ...args: unknown[]): void {\n this.#writeLog(\"error\", context, message, args);\n }\n\n /**\n * Central logging method that coordinates console output and callback invocation.\n *\n * This method implements the core logging logic:\n * 1. Early exit optimization for 'none' level (unless callback ignores level)\n * 2. Level threshold comparison for console output filtering\n * 3. Delegates to #writeToConsole and #invokeCallback\n *\n * @param level - Log level ('log' | 'warn' | 'error')\n * @param context - Context identifier\n * @param message - Log message\n * @param args - Additional arguments\n *\n * @private\n */\n #writeLog(\n level: LogLevel,\n context: string,\n message: string,\n args: unknown[],\n ): void {\n // Early exit optimization: if level is 'none' and callback doesn't ignore level,\n // skip all processing (both console and callback)\n // Stryker disable next-line BlockStatement: equivalent — emptying this early-exit block falls through, but at level \"none\" the downstream guards already yield no output: #writeToConsole skips (threshold 3 > every message level) and #invokeCallback returns (this branch runs only when callbackIgnoresLevel is false). Pure perf shortcut; the ConditionalExpression →true sibling on this line stays killed (not silenced here).\n if (this.#config.level === \"none\" && !this.#config.callbackIgnoresLevel) {\n return;\n }\n\n // Convert message level to numeric value for threshold comparison\n // LOG_LEVELS: { log: 0, warn: 1, error: 2 }\n const messageLevelValue = LOG_LEVELS[level];\n\n // Determine if this message should skip console output\n // Example: if threshold is 'warn' (1), then 'log' messages (0) are skipped\n const shouldSkipConsole = messageLevelValue < this.#currentThreshold;\n\n // Console output (respects level threshold)\n if (!shouldSkipConsole) {\n this.#writeToConsole(level, context, message, args);\n }\n\n // Callback handling (may ignore level threshold based on config)\n this.#invokeCallback(level, context, message, shouldSkipConsole, args);\n }\n\n /**\n * Writes a formatted log message to the console.\n *\n * Features:\n * - Formats message with context: \"[Context] message\"\n * - Uses appropriate console method (log/warn/error)\n * - Safe: checks for console existence (for non-browser environments)\n *\n * @param level - Console method to use ('log' | 'warn' | 'error')\n * @param context - Context identifier (prepended to message if present)\n * @param message - Log message\n * @param args - Additional arguments to pass to console\n *\n * @private\n */\n #writeToConsole(\n level: LogLevel,\n context: string,\n message: string,\n args: unknown[],\n ): void {\n // Safety check: ensure console exists and has the required method\n // This is important for environments like Node.js tests or edge cases\n if (\n typeof console !== \"undefined\" &&\n typeof console[level] === \"function\"\n ) {\n // Format message with context bracket notation for visual clarity\n // Note: formatting is done inside the check to avoid unnecessary string allocation\n // when console is not available\n const formattedMessage = context ? `[${context}] ${message}` : message;\n\n console[level](formattedMessage, ...args);\n }\n }\n\n /**\n * Invokes the configured callback with log data, respecting level settings.\n *\n * Complex logic handling:\n * 1. Skip if no callback configured\n * 2. Skip if callback respects level AND message is below threshold\n * 3. Call callback with error handling (prevents callback errors from breaking logger)\n *\n * The callbackIgnoresLevel flag enables two modes:\n * - false (default): callback only receives messages that pass threshold (same as console)\n * - true: callback receives ALL messages regardless of threshold (useful for analytics)\n *\n * @param level - Log level\n * @param context - Context identifier\n * @param message - Log message\n * @param shouldSkipConsole - Whether console output was skipped (used for level logic)\n * @param args - Additional arguments\n *\n * @private\n */\n #invokeCallback(\n level: LogLevel,\n context: string,\n message: string,\n shouldSkipConsole: boolean,\n args: unknown[],\n ): void {\n // Early exit: no callback configured, or callback respects level and message is filtered\n if (\n !this.#config.callback ||\n (!this.#config.callbackIgnoresLevel && shouldSkipConsole)\n ) {\n return;\n }\n\n // Re-entrancy guard: a callback calling logger.* re-enters here via\n // #writeLog → #invokeCallback. Skip the nested invocation so the pattern is\n // a safe no-op (console output already happened in #writeLog) instead of\n // recursing to a swallowed RangeError (#791).\n if (this.#inCallback) {\n return;\n }\n\n // Wrap callback invocation in try-catch to prevent user code errors\n // from breaking the logger or causing cascading failures\n this.#inCallback = true;\n try {\n // An async callback (`(...) => Promise<void>` is assignable to the\n // void-typed LogCallback) returns a Promise whose rejection would otherwise\n // leak as a Node `unhandledRejection` — process-fatal under\n // `--unhandled-rejections=strict` (Node 22+ default). Read the runtime\n // return and isolate it like core's subscribe (#944): duck-check the\n // thenable + `.catch` into the same console.error sink a sync throw uses\n // (#1161).\n // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -- read the runtime Promise of a void-typed async callback (#1161)\n const result: unknown = this.#config.callback(\n level,\n context,\n message,\n ...args,\n );\n\n if (\n result !== null &&\n result !== undefined &&\n typeof (result as PromiseLike<unknown>).then === \"function\"\n ) {\n Promise.resolve(result as PromiseLike<unknown>).catch(\n (error: unknown) => {\n this.#reportError(\"[Logger] Error in async callback:\", error);\n },\n );\n }\n } catch (error) {\n // Fallback error reporting if the callback throws synchronously\n this.#reportError(\"[Logger] Error in callback:\", error);\n } finally {\n this.#inCallback = false;\n }\n }\n\n // Report a callback error via console.error directly — never call the logger\n // (would recurse). Shared by the sync-throw catch and the async-rejection\n // `.catch` (#1161). Console-safety guard mirrors #writeToConsole.\n #reportError(message: string, error: unknown): void {\n if (typeof console !== \"undefined\" && typeof console.error === \"function\") {\n console.error(message, error);\n }\n }\n}\n","// packages/core/src/wiring/wireNamespaces.ts\n\nimport { assertShippedChannelCorrect } from \"../channels\";\nimport { getInternals } from \"../internals\";\nimport { COMMIT_PERMIT_TOKEN } from \"../namespaces/NavigationNamespace\";\nimport { resolveOption } from \"../namespaces/OptionsNamespace\";\nimport { buildURL, canonicalize, materialize } from \"../pipeline\";\n\nimport type { RouterError } from \"../RouterError\";\nimport type { NamespaceBag } from \"./types\";\nimport type { NavigationDependencies } from \"../namespaces/NavigationNamespace\";\nimport type { PluginsDependencies } from \"../namespaces/PluginsNamespace\";\nimport type { RouteLifecycleDependencies } from \"../namespaces/RouteLifecycleNamespace\";\nimport type { RouterLifecycleDependencies } from \"../namespaces/RouterLifecycleNamespace\";\nimport type { RoutesDependencies } from \"../namespaces/RoutesNamespace\";\nimport type { RouteResolver } from \"../pipeline\";\nimport type { Router } from \"../Router\";\nimport type { DefaultDependencies } from \"../types\";\nimport type { RouterValidator } from \"../types/RouterValidator\";\n\n/**\n * Compiles a guard/plugin factory against the router + a cached `getDependency`\n * accessor. One generic function serves both RouteLifecycle (`GuardFnFactory →\n * GuardFn`) and Plugins (`PluginFactory → Plugin`).\n */\ntype CompileFactory<Dependencies extends DefaultDependencies> = <T>(\n factory: (\n router: Router<Dependencies>,\n getDependency: <K extends keyof Dependencies>(name: K) => Dependencies[K],\n ) => T,\n) => T;\n\n/**\n * Wires the inter-namespace dependencies of a freshly-constructed router.\n *\n * Replaces the former `RouterWiringBuilder` class + `wireRouter` director\n * (#1334): a single call-site with a fixed sequence and nothing to build is a\n * pack of procedures over a shared bag, not a builder — plain functions over a\n * `NamespaceBag` drop the triple-repeated field list and the builder instance.\n *\n * Call order is arbitrary (#1331): no `wire*` function runs user code or\n * eagerly reads another namespace's deps. (`wireLimits` is the one eager\n * *write* — it hands the frozen limits object to dependenciesStore/eventBus;\n * the rest only store deps-closures.) Initial-route guard factories are\n * flushed afterwards, from the constructor's `flushPendingGuards()` call.\n */\nexport function wireNamespaces<Dependencies extends DefaultDependencies>(\n ns: NamespaceBag<Dependencies>,\n): void {\n // One shared factory for both guard and plugin compilation (#1334); the\n // `getDependency` closure is allocated once here, not per compile call.\n const compileFactory = createCompileFactory(ns);\n\n // One port per router instance — allocated at wiring time, not per call.\n // Hoisted OUT of `wireNavigation` (Phase 2, step 2-2): `navigate` is no longer\n // its only consumer — the entry points migrating onto the pipeline live in\n // RoutesNamespace, which is wired first. Creation order is safe because the\n // resolver only captures `ns.router`'s internals (registered before wiring,\n // #1331) and the routes store (assigned in the namespace's constructor); every\n // member reads through `ns.*` at CALL time, so no wiring order is baked in.\n const port = createRouteResolver(ns);\n\n // Shared by RouteLifecycle and Plugins — one allocation. Internals are\n // registered before wiring (#1331), so this never throws; returns null until\n // validation-plugin installs the validator.\n const getValidator = (): RouterValidator | null =>\n getInternals(ns.router).validator;\n\n wireLimits(ns);\n wireEventBus(ns, getValidator);\n wireRouteLifecycle(ns, compileFactory, getValidator);\n wireRoutes(ns, port, getValidator);\n wirePlugins(ns, compileFactory, getValidator);\n wireNavigation(ns, port);\n wireRouterLifecycle(ns);\n wireState(ns, port);\n}\n\nfunction createCompileFactory<Dependencies extends DefaultDependencies>(\n ns: NamespaceBag<Dependencies>,\n): CompileFactory<Dependencies> {\n const { router, dependenciesStore } = ns;\n\n const getDependency = <K extends keyof Dependencies>(\n name: K,\n ): Dependencies[K] => dependenciesStore.dependencies[name] as Dependencies[K];\n\n return <T>(\n factory: (\n router: Router<Dependencies>,\n getDependency: <K extends keyof Dependencies>(name: K) => Dependencies[K],\n ) => T,\n ): T => factory(router, getDependency);\n}\n\nfunction wireLimits<Dependencies extends DefaultDependencies>(\n ns: NamespaceBag<Dependencies>,\n): void {\n ns.dependenciesStore.limits = ns.limits;\n ns.eventBus.setLimits({\n maxListeners: ns.limits.maxListeners,\n warnListeners: ns.limits.warnListeners,\n });\n}\n\n/**\n * Hands EventBusNamespace the shared lazy validator accessor so `subscribe` /\n * `addEventListener` can run the opt-in listener-count threshold (#1188) — the\n * emitter-side parallel to the plugins / lifecycle / dependencies counters.\n */\nfunction wireEventBus<Dependencies extends DefaultDependencies>(\n ns: NamespaceBag<Dependencies>,\n getValidator: () => RouterValidator | null,\n): void {\n ns.eventBus.setValidatorAccessor(getValidator);\n}\n\nfunction wireRouteLifecycle<Dependencies extends DefaultDependencies>(\n ns: NamespaceBag<Dependencies>,\n compileFactory: CompileFactory<Dependencies>,\n getValidator: () => RouterValidator | null,\n): void {\n const deps: RouteLifecycleDependencies<Dependencies> = {\n logger: getInternals(ns.router).logger,\n compileFactory,\n getValidator,\n };\n\n ns.routeLifecycle.setDependencies(deps);\n}\n\nfunction wireRoutes<Dependencies extends DefaultDependencies>(\n ns: NamespaceBag<Dependencies>,\n port: RouteResolver,\n getValidator: () => RouterValidator | null,\n): void {\n const deps: RoutesDependencies<Dependencies> = {\n logger: getInternals(ns.router).logger,\n getValidator,\n port,\n addActivateGuard: (name, handler, precompiledFn) => {\n ns.routeLifecycle.addCanActivate(name, handler, true, precompiledFn);\n },\n addDeactivateGuard: (name, handler, precompiledFn) => {\n ns.routeLifecycle.addCanDeactivate(name, handler, true, precompiledFn);\n },\n compileGuard: (handler, methodName) =>\n ns.routeLifecycle.compileGuardFactory(handler, methodName),\n getState: () => ns.state.get(),\n areStatesEqual: (state1, state2, ignoreQueryParams) =>\n ns.state.areStatesEqual(state1, state2, ignoreQueryParams),\n getDependency: (name) =>\n ns.dependenciesStore.dependencies[name] as Dependencies[typeof name],\n };\n\n ns.routes.setDependencies(deps);\n ns.routes.setLifecycleNamespace(ns.routeLifecycle);\n}\n\nfunction wirePlugins<Dependencies extends DefaultDependencies>(\n ns: NamespaceBag<Dependencies>,\n compileFactory: CompileFactory<Dependencies>,\n getValidator: () => RouterValidator | null,\n): void {\n const deps: PluginsDependencies<Dependencies> = {\n logger: getInternals(ns.router).logger,\n addEventListener: (eventName, cb) =>\n ns.eventBus.addEventListener(eventName, cb),\n canNavigate: () => ns.eventBus.canBeginTransition(),\n compileFactory,\n getValidator,\n };\n\n ns.plugins.setDependencies(deps);\n}\n\n/**\n * The router's implementation of the pipeline's read-model (`RouteResolver`).\n *\n * ⚠ Both ends are wired to the INTERCEPTABLE primitives on purpose — see the\n * port's own docs. `resolveForward` is the `forwardState` seam (interceptors +\n * the centralized channel CHECK that replaced stage ②'s repair), so the seam\n * lives here, in the port implementation, and never inside the pipeline module.\n * `buildPath` is the namespace primitive, with nothing interceptable between it\n * and the engine: what a plugin injects belongs above the route-default merge,\n * and `forwardState` is the seam that sits there — on both doors (#2087).\n */\nfunction createRouteResolver<Dependencies extends DefaultDependencies>(\n ns: NamespaceBag<Dependencies>,\n): RouteResolver {\n // Hoisted once per router, NOT per call: `navigate` is the hot path, and\n // `getInternals` is a WeakMap lookup. Safe because both references are stable\n // for the router's lifetime — internals are registered before wiring (#1331)\n // and the object is mutated in place, never swapped; `store` is `readonly` on\n // RoutesNamespace, assigned once in its constructor. ⚠ Only `store` itself is\n // stable: `add`/`replace`/`clear` DO swap `store.config`'s sub-maps wholesale\n // (`Object.assign(store.config, artifacts.config)`), so the accessors below\n // must keep re-reading `store.config.defaultParams` per call — hoisting the\n // sub-map would freeze the pre-mutation defaults. The interceptable methods\n // are stable closures that read the live interceptor map on every call, so\n // hoisting them keeps plugin registration fully dynamic.\n const ctx = getInternals(ns.router);\n const store = ns.routes.getStore();\n\n const reportUndeclaredParamKey = (routeName: string, key: string): void => {\n ctx.validator?.state.reportUndeclaredParamKey(routeName, key);\n };\n\n const reportDroppedQueryKey = (routeName: string, key: string): void => {\n ctx.validator?.state.reportDroppedQueryKey(routeName, key);\n };\n\n return {\n resolveForward: (name, params, search) =>\n ctx.forwardState(name, params, search),\n // `config.*` maps are null-prototype, so a missing entry reads as\n // `undefined` (never a proto value). O(1) lookup, no ancestor walk.\n defaultParams: (name) => store.config.defaultParams[name],\n defaultSearch: (name) => store.config.defaultSearch[name],\n // ⑤a prints through the namespace primitive directly. There is no\n // interceptable between the port and the engine: the door above this one\n // runs the `forwardState` seam (#2087), so a plugin injecting there is seen\n // by BOTH `navigate` and `router.buildPath`, above the route-default merge\n // rather than below it.\n buildPath: (name, params, search) =>\n ns.routes.buildPath(name, params, search, ns.options.get()),\n queryNames: (name) => ns.routes.getQueryParams(name),\n // `undefined` for a route that does not exist (#1584) — `getUrlParams`\n // answers `[]` for that case and for a real route with no path slots alike,\n // and the diagnostic downstream cannot tell those apart. `hasRoute` is the\n // matcher's own predicate, so no second derivation of existence appears.\n pathNames: (name) =>\n ns.routes.hasRoute(name) ? ns.routes.getUrlParams(name) : undefined,\n // A closure because the port TYPES this member as `() => boolean`, not\n // because anything can change the answer: options are immutable after\n // construction (#63 removed `setOption`), `ns.options.get()` returns a\n // deep-frozen bag, and `tsc` refuses a captured boolean here outright.\n admitsUndeclaredQuery: () => ns.options.get().queryParamsMode === \"loose\",\n // A GETTER for the same reason as its sibling below — a plain closure is\n // always truthy, so the pipeline's `?.` never gated anything and bare core\n // paid #1584's `pathNames` existence lookup once per dropped key with no\n // sink behind it. Both sinks now report their absence honestly.\n get reportDroppedQueryKey() {\n return ctx.validator ? reportDroppedQueryKey : undefined;\n },\n // A GETTER, not a closure — the absence is the gate (#1579). The pipeline\n // reads `port.reportUndeclaredParamKey` and skips the whole caller-bag walk\n // when it is `undefined`; a plain closure is always truthy, so bare core\n // (`validator === null`, the repo default) walked the bag on every commit\n // anyway and the \"opt-in sink\" the design bought was never wired. The\n // validator is installed AFTER wiring, so this cannot be decided once at\n // construction — it has to be resolved per ACCESS. `admitsUndeclaredQuery`\n // above is a closure for an unrelated reason (the port's type), so it is\n // not the precedent this stands on.\n get reportUndeclaredParamKey() {\n return ctx.validator ? reportUndeclaredParamKey : undefined;\n },\n };\n}\n\nfunction wireNavigation<Dependencies extends DefaultDependencies>(\n ns: NamespaceBag<Dependencies>,\n port: RouteResolver,\n): void {\n const deps: NavigationDependencies = {\n logger: getInternals(ns.router).logger,\n getOptions: () => ns.options.get(),\n hasRoute: (name) => ns.routes.hasRoute(name),\n getQueryParams: (name) => ns.routes.getQueryParams(name),\n getMetaForState: (name) => ns.routes.getMetaForState(name),\n getState: () => ns.state.get(),\n buildNavigateState: (routeName, routeParams, routeSearch) => {\n const ctx = getInternals(ns.router);\n\n ctx.validator?.routes.validateStateBuilderArgs(\n routeName,\n routeParams,\n \"navigate\",\n );\n\n // The pipeline (RFC nav-pipeline, milestone 1): `canonicalize` is the sole\n // producer of the canonical intent (① forwardTo resolution through the\n // interceptor zone + ③ route defaults under the caller's value), and both\n // `buildURL` and `materialize` physically accept nothing else — so the URL\n // and the State can never derive from differently-merged channels.\n //\n // Channels stay correct at the seam (#1548/#1549): `routeSearch` (the\n // positional / descriptor form), a declared `?key` riding in the caller's\n // `params` bag, and persistent params injected by a plugin all land in the\n // query channel inside `port.resolveForward`, so one path serves both the\n // positional and the v1 single-bag forms. A colliding name\n // (`/items/:id?id`) keeps its path slot and query twin independent.\n const canonical = canonicalize(\n port,\n routeName,\n routeParams,\n routeSearch,\n {\n diagnoseUndeclared: true,\n },\n );\n const meta = ns.routes.getMetaForState(canonical.name);\n\n if (meta === undefined) {\n return;\n }\n\n // ⑤a then ⑤b: the URL is built from the merged channels (not the raw\n // args), so `state.path` stays in step with `state.search`. The pending\n // form defers the freeze of the state SHELL for the transition pipeline —\n // never a channel: `query` arrives frozen from the merge, and `params` is\n // frozen inside the state builder BOTH terminals go through, so a guard\n // sees immutable bags either way (INVARIANTS canonicalize #4).\n assertShippedChannelCorrect(\n \"navigate\",\n canonical.name,\n canonical.path,\n port.queryNames(canonical.name),\n );\n\n return materialize(canonical, buildURL(canonical, port));\n },\n resolveDefault: () => {\n const options = ns.options.get();\n const ctx = getInternals(ns.router);\n\n const route = resolveOption(\n options.defaultRoute,\n (name: string) =>\n ns.dependenciesStore.dependencies[name as keyof Dependencies],\n );\n const params = resolveOption(\n options.defaultParams,\n /* v8 ignore next -- @preserve: unreachable unless defaultParams is a callback that calls getDependency */\n (name: string) =>\n ns.dependenciesStore.dependencies[name as keyof Dependencies],\n );\n const search = resolveOption(\n options.defaultSearch,\n /* v8 ignore next -- @preserve: unreachable unless defaultSearch is a callback that calls getDependency */\n (name: string) =>\n ns.dependenciesStore.dependencies[name as keyof Dependencies],\n );\n\n if (typeof options.defaultRoute === \"function\") {\n ctx.validator?.options.validateResolvedDefaultRoute(\n route,\n ctx.routeGetStore(),\n );\n }\n\n return { route, params, search };\n },\n startTransition: (plan) => ns.eventBus.sendNavigate(plan),\n\n systemCommit: (toState, fromState, opts) =>\n ns.eventBus.systemCommit({ toState, fromState, opts }),\n cancelNavigation: (reason) => {\n ns.eventBus.sendCancelIfPossible(ns.state.get(), reason);\n },\n // The bridge's LATE moment (#1690). The EARLY one needs no wire at all — it\n // is the `NAVIGATE` action, inside the same namespace (#1724).\n bridgeExternalSignal: (plan) => {\n ns.eventBus.bridgeExternalSignal(plan);\n },\n canCommitTransition: (payload) =>\n ns.eventBus.canCommitTransition(payload)\n ? COMMIT_PERMIT_TOKEN\n : undefined,\n sendTransitionDone: (payload) => {\n ns.eventBus.sendComplete(payload);\n },\n sendTransitionFail: (fromState, error, nav) => {\n ns.eventBus.sendFail(fromState, error, nav);\n },\n // Channel (b): early refusals (ROUTE_NOT_FOUND, the P3 channel guard,\n // same-state) report to observers without moving the machine — there is no\n // transition of theirs to fail.\n emitTransitionError: (toState, fromState, error) => {\n ns.eventBus.emitTransitionError(toState, fromState, error as RouterError);\n },\n sendLeaveApprove: (plan) => {\n ns.eventBus.sendLeaveApprove(plan);\n },\n canNavigate: () => ns.eventBus.canBeginTransition(),\n isStarting: () => ns.eventBus.isStarting(),\n getLifecycleFunctions: () => ns.routeLifecycle.getFunctions(),\n // Deactivation half only — the 404 has no route to activate (#1643).\n canDeactivateCurrent: (deactivated, toState, fromState) =>\n ns.routeLifecycle.canNavigateTo(deactivated, [], toState, fromState),\n isTransitioning: () => ns.eventBus.isTransitioning(),\n // Post-leave auto-cleanup unregisters only the EXTERNAL (component-managed)\n // guard; a route-config (definition) guard survives for re-entry (#1171).\n // `_permit` is a compile-time obligation (#1649) — unread at runtime.\n clearCanDeactivate: (name: string, _permit) => {\n ns.routeLifecycle.clearCanDeactivate(name, \"external\");\n },\n hasLeaveListeners: () => ns.eventBus.hasLeaveListeners(),\n hasPreCommitListeners: () => ns.eventBus.hasPreCommitListeners(),\n awaitLeaveListeners: (toState, fromState, signal) =>\n ns.eventBus.awaitLeaveListeners(toState, fromState, signal),\n };\n\n ns.navigation.setDependencies(deps);\n}\n\nfunction wireRouterLifecycle<Dependencies extends DefaultDependencies>(\n ns: NamespaceBag<Dependencies>,\n): void {\n const deps: RouterLifecycleDependencies = {\n getOptions: () => ns.options.get(),\n navigateToState: (state, opts) =>\n ns.navigation.navigateToState(state, opts),\n // No opts: `start()` commits before anything is committed, so the\n // deactivation consult (#1643) short-circuits on an absent `fromState`.\n navigateToNotFound: (path) => ns.navigation.navigateToNotFound(path),\n matchPath: (path) => ns.routes.matchPath(path, ns.options.get()),\n completeStart: () => {\n ns.eventBus.sendStarted();\n },\n isIdle: () => ns.eventBus.isIdle(),\n };\n\n ns.lifecycle.setDependencies(deps);\n}\n\nfunction wireState<Dependencies extends DefaultDependencies>(\n ns: NamespaceBag<Dependencies>,\n port: RouteResolver,\n): void {\n ns.state.setDependencies({\n // `makeState` is `canonicalize`'s literal form since Phase 4, so the port is\n // all it needs. Seven further members — both default maps, `getQueryParams`,\n // `hasRoute`, `admitsUndeclaredQuery`, `getDropReporter`, `buildPath` —\n // would exist only to feed a second copy of stage ③ and the mode gate.\n port: () => port,\n getUrlParams: (name) => ns.routes.getUrlParams(name),\n });\n}\n","// packages/core/src/Router.ts\n\n/**\n * Router class - facade with integrated namespaces.\n *\n * All functionality is now provided by namespace classes.\n */\n\nimport { assertChannelCorrect, findMisChanneledKey } from \"./channels\";\nimport { EMPTY_OPTS, EMPTY_PARAMS, errorCodes } from \"./constants\";\nimport {\n assertLoggerConfig,\n guardDependencyShape,\n guardRouteStructure,\n} from \"./guards\";\nimport {\n copyOwnData,\n dropUnsafeKey,\n adoptChannel,\n withoutUnsafeKey,\n} from \"./helpers\";\nimport {\n createInterceptable,\n createTernaryInterceptable,\n getInternals,\n registerInternals,\n SEAM,\n throwOnMisChanneledKey,\n} from \"./internals\";\nimport { createLimits } from \"./limits\";\nimport {\n EventBusNamespace,\n NavigationNamespace,\n OptionsNamespace,\n PluginsNamespace,\n RouteLifecycleNamespace,\n RouterLifecycleNamespace,\n RoutesNamespace,\n StateNamespace,\n createDependenciesStore,\n} from \"./namespaces\";\nimport { isExpectedRejection } from \"./namespaces/NavigationNamespace/constants\";\nimport { defaultOptions } from \"./namespaces/OptionsNamespace/constants\";\nimport { CACHED_ALREADY_STARTED_ERROR } from \"./namespaces/RouterLifecycleNamespace/constants\";\nimport { buildURL, canonicalize, materialize } from \"./pipeline\";\nimport { RouterError, freezeThrownError } from \"./RouterError\";\nimport { createRouterFSM } from \"./routerFSM\";\nimport { getTransitionPath } from \"./transitionPath\";\nimport { EventEmitter } from \"./utils/event-emitter\";\nimport { RouterLogger } from \"./utils/logger\";\nimport { wireNamespaces } from \"./wiring\";\n\nimport type { CreateMatcherOptions, QueryParamsConfig } from \"./engine\";\nimport type { RouterInternals } from \"./internals\";\nimport type { DependenciesStore } from \"./namespaces\";\nimport type {\n DefaultDependencies,\n LeaveFn,\n NavigationOptions,\n NavigationTarget,\n Options,\n Params,\n Router as RouterInterface,\n SearchParams,\n State,\n SubscribeFn,\n Unsubscribe,\n PluginFactory,\n Route,\n} from \"./types\";\nimport type { Limits, RouterEventMap } from \"./types/internal\";\n\n/** Captured like the deciding seven, but this one BUILDS the guarantee (#2072). */\nconst objectCreate = Object.create;\n\n/**\n * Captured at module load, the discipline `cloneRouter`, `helpers` and `guards`\n * already follow. Both DECIDE something a shim could take over: `objectKeys`\n * picks which limits a clone inherits, and `freeze` is what makes that key set\n * safe to hand out by reference — a no-op shim of either leaves the base and its\n * clones free to disagree.\n */\nconst objectKeys = Object.keys;\nconst freeze = Object.freeze;\n\n/**\n * Router class with integrated namespace architecture.\n *\n * All functionality is provided by namespace classes:\n * - OptionsNamespace: getOptions (immutable)\n * - DependenciesStore: get/set/remove dependencies\n * - EventEmitter: subscribe\n * - StateNamespace: state SERVICE (makeState, areStatesEqual); the committed\n * pair itself lives in the FSM context (#1641)\n * - RoutesNamespace: route tree operations\n * - RouteLifecycleNamespace: canActivate/canDeactivate guards\n * - PluginsNamespace: plugin lifecycle\n * - NavigationNamespace: navigate\n * - RouterLifecycleNamespace: start (stop/dispose are FSM edges, not methods)\n *\n * @internal This class implementation is internal. Use createRouter() instead.\n */\nexport class Router<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> implements RouterInterface<Dependencies> {\n [key: string]: unknown;\n\n // ============================================================================\n // Namespaces\n // ============================================================================\n\n readonly #options: OptionsNamespace<Dependencies>;\n readonly #limits: Limits;\n /** The limit names the CALLER passed, snapshotted at construction (#1961). */\n readonly #limitKeys: readonly string[] | undefined;\n\n /**\n * Where the two behaviour-bearing option bags CAME FROM, weakly (#2148).\n *\n * ⚑ **Weak, and that word carries the whole design.** Core does not hold the\n * application's container (#2171), and a strong field here would be holding it.\n * A `WeakRef` is not: it knows where the bag was, if the bag is still alive, and\n * the application can free it at any time.\n *\n * ⚑ **Diagnostic only, and nothing routes through it.** Adoption already took\n * the copy the router runs on; this exists so `@real-router/validation-plugin`\n * can tell an application that mutating its config after `createRouter` no\n * longer reaches the router. Core stays silent — it is the layer that degrades,\n * the plugin is the layer that reports, which is the split\n * `packages/core/CLAUDE.md` › Supported Input Shapes already decides.\n *\n * ⚠ TWO slots, not four. `queryParams` is not adopted at all, and core stops\n * reading `limits` once `createLimits` has taken its numbers — so a late\n * mutation of either changes no behaviour and has nothing to report.\n *\n * {@link isWatchableBag} owns which slots earn an entry, and why.\n */\n readonly #adoptedOrigins: AdoptedOrigins;\n readonly #dependenciesStore: DependenciesStore<Dependencies>;\n readonly #state: StateNamespace;\n readonly #routes: RoutesNamespace<Dependencies>;\n readonly #routeLifecycle: RouteLifecycleNamespace<Dependencies>;\n readonly #plugins: PluginsNamespace<Dependencies>;\n readonly #navigation: NavigationNamespace;\n readonly #lifecycle: RouterLifecycleNamespace;\n\n readonly #eventBus: EventBusNamespace;\n\n /**\n * Per-instance suppressor for fire-and-forget `start()`. It logs through THIS\n * router's logger (built in the constructor), so it cannot be static (#724).\n *\n * Only start, since Step 0: the navigate/navigateToState/navigateToDefault\n * suppressor moved to `NavigationNamespace`, which is where those promises are\n * created and therefore the only layer that knows which of them are already\n * pre-suppressed. Start keeps its own because it suppresses a promise the\n * FACADE builds (`internalStart.catch(#unwindFailedStart)`), and because the\n * #931 category split survives — start failures log under \"router.start\" (a\n * start interceptor throwing a plain Error after next() committed, #763, or a\n * cryptic path TypeError — neither a suppressed RouterError). Both sides still\n * classify through ONE shared policy, `isExpectedRejection`.\n */\n readonly #onSuppressedStartError: (error: unknown) => void;\n\n /**\n * The href door's own run of the `forwardState` chain, above the route-default\n * merge (#2087).\n */\n readonly #buildPathIntent: (\n route: string,\n params: Params,\n search: SearchParams | undefined,\n ) => string;\n\n // ============================================================================\n // Constructor\n // ============================================================================\n\n /**\n * @param routes - Route definitions\n * @param options - Router options\n * @param dependencies - DI dependencies\n */\n constructor(\n routes: Route<Dependencies>[] = [],\n options: Partial<Options<Dependencies>> = {},\n dependencies: Dependencies = {} as Dependencies,\n ) {\n // Extract the logger config WITHOUT mutating the caller's `options` object\n // (#724). `routerOptions` is the logger-stripped view handed to the options\n // pipeline so `logger` never lands in the frozen router options.\n const { logger: loggerConfig, ...routerOptions } = options;\n\n // ⚑ The guard RETURNS core's own copy, and the logger is built from that\n // (#1814 / #1842). One reader: nothing downstream re-reads the caller's bag,\n // so no two readers can disagree about own-ness.\n const normalizedLogger = loggerConfig\n ? assertLoggerConfig(loggerConfig)\n : undefined;\n\n // Per-router logger instance — replaces the former process-global singleton\n // whose configure() leaked across every router in the process, last\n // createRouter winning (#724). Stored on ctx (registerInternals below), so\n // the facade reads getInternals(this).logger; namespaces receive it via\n // their deps at wiring; plugins reach it through getPluginApi(router).logger.\n const logger = new RouterLogger(normalizedLogger);\n\n // Per-instance fire-and-forget suppressor (see the field declaration): it\n // logs through THIS router's logger, so it is built here, not static.\n this.#onSuppressedStartError = (error: unknown): void => {\n if (isExpectedRejection(error)) {\n return;\n }\n\n logger.error(\"router.start\", \"Unexpected start error\", error);\n };\n\n // =========================================================================\n // Validate inputs before creating namespaces\n // =========================================================================\n\n // Always validate the caller's options (catches non-object / array inputs)\n OptionsNamespace.validateOptionsIsObject(options);\n\n // Unconditional guard-level validation before creating namespaces.\n // ⚑ SHAPE only — the getter ban rides the store's copy walk (#1861), so a\n // dependency bag is enumerated ONCE per router instead of twice. This half\n // walks nothing, so it stays here, above `guardRouteStructure`, and keeps\n // \"is this even an object\" as the first thing a caller hears about.\n guardDependencyShape(dependencies);\n\n // ⚑ The walk RETURNS core's batch (#2139), so the constructor door snapshots\n // where it guards instead of handing the caller's array on to be walked a\n // second time inside `createRoutesStore`. No `routes.length` gate any more:\n // an empty list produces an empty batch through the same line, and the gate\n // only existed to skip a no-op.\n const routeBatch = guardRouteStructure(routes);\n\n // =========================================================================\n // Create Namespaces\n // =========================================================================\n\n // ⚑ Adopted BEFORE the namespace, so every reader below — the options\n // freeze, `createLimits`, the `#limitKeys` snapshot, `deriveMatcherOptions`,\n // and `getCloneState().options` after them — sees core's own object rather\n // than the caller's (#2171). One read of each caller bag, at this line.\n const adoptedOptions = adoptOptionBags(routerOptions);\n\n this.#adoptedOrigins = weakOrigins(routerOptions);\n\n this.#options = new OptionsNamespace(adoptedOptions);\n this.#limits = createLimits(adoptedOptions.limits);\n // ⚑ The key set, snapshotted HERE, beside the values (#1961). `createLimits`\n // owns what each limit IS; this owns which ones the caller NAMED, and a\n // clone needs both. Read once, at construction, for the same reason the\n // values are: `routerOptions.limits` is the caller's object and stays\n // theirs.\n //\n // ⚠ `objectKeys`, matching `createLimits`' SPREAD, not `Object.hasOwn` over\n // the five known names. The spread skips a non-enumerable own key, so the\n // base does not see one — and a snapshot that did would make the clone\n // stricter than its base. Pinned by \"a non-enumerable own limit is invisible\n // to the base AND to the clone\".\n //\n // ⚠ FROZEN, for the reason its sibling `#limits` is (#1880): `getCloneState`\n // hands this out BY REFERENCE, so a consumer holding it could move what the\n // clone inherits while the base kept what its emitter was wired with —\n // measured on the unfrozen form, emptying it gave the base cap 50 and the\n // clone none, and pushing a name onto it made the clone report a\n // materialised default the base never had. That is #1961's own divergence,\n // reintroduced through the slot that fixes it.\n this.#limitKeys =\n adoptedOptions.limits == null\n ? undefined\n : freeze(objectKeys(adoptedOptions.limits));\n this.#dependenciesStore =\n createDependenciesStore<Dependencies>(dependencies);\n this.#state = new StateNamespace();\n this.#routes = new RoutesNamespace<Dependencies>(\n routeBatch,\n deriveMatcherOptions(this.#options.get()),\n logger,\n );\n this.#routeLifecycle = new RouteLifecycleNamespace<Dependencies>();\n this.#plugins = new PluginsNamespace<Dependencies>();\n this.#navigation = new NavigationNamespace();\n this.#lifecycle = new RouterLifecycleNamespace();\n\n // =========================================================================\n // Initialize EventBus\n // =========================================================================\n\n const routerFSM = createRouterFSM();\n\n // The state service reads the machine's context from here on — the cells\n // themselves live there (plan §11.A2). Assigned before anything can read\n // state: the namespaces are still being constructed.\n this.#state.setContext(routerFSM.getContext());\n\n const emitter = new EventEmitter<RouterEventMap>({\n // Shared per-listener error sink: EventEmitter reports synchronous listener\n // throws here, and EventBusNamespace.subscribe routes an async listener's\n // rejected Promise through the SAME sink (#944) — both land in one place.\n onListenerError: (eventName, error) => {\n logger.error(\"Router\", `Error in listener for ${eventName}:`, error);\n },\n onListenerWarn: (eventName, count) => {\n logger.warn(\n \"router.addEventListener\",\n `Event \"${eventName}\" has ${count} listeners — possible memory leak`,\n );\n },\n });\n\n // ⚑ No `abortController` effect to inject any more (#1684): the FSM CANCEL\n // action reads the controller off the navigation it is already carrying\n // (`ctx.inflight.controller`), so the wire from here to\n // `NavigationNamespace` — and the router-level slot it fed — are both gone.\n this.#eventBus = new EventBusNamespace({ routerFSM, emitter });\n\n // =========================================================================\n // Register Internals (WeakMap for plugin/infrastructure access)\n // =========================================================================\n // Registered BEFORE wiring (#1331) so every namespace's deps-closure sees a\n // router already present in the internals registry — `getInternals(router)`\n // never throws during wiring, and guard factories flushed at the end of the\n // constructor see a fully-registered instance.\n\n const interceptorsMap: RouterInternals[\"interceptors\"] = new Map();\n\n // THE single forwardState boundary (#1548/#1549). The interceptable resolves\n // the route (forwardTo) and runs the whole interceptor chain — a plugin\n // injecting params, a search-schema validation, etc. The outer layer then\n // CHECKS the channels once, keyed on the RESOLVED route's `?`-declaration.\n //\n // It REFUSES rather than repairs, and three things are why no repair may be\n // re-introduced (#1570). Moving a declared query key out of the params bag\n // leaves the producer believing the bag it wrote is the one that ships. It\n // lets a plugin inject past a validation that has already run — the leak\n // search-schema names. And the caller's own mis-channelled key and a chain\n // default's query half land in DIFFERENT channels, where no merge ranks\n // them, so the default silently wins. Whoever names the route knows its\n // declaration.\n //\n // `as unknown as` is required: the closure is non-generic, but\n // RouterInternals[\"forwardState\"] is declared generic `<P, S>`, which tsc\n // will not infer from a non-generic source (Sonar S4325 misclassifies this\n // as a redundant cast).\n // ⚑ What every `next()` in the chain hands back (#1986). The door's own\n // answer is the exit copy's business, below; this is the boundary BETWEEN\n // links, which nothing else sees — `original` into the first interceptor,\n // and each interceptor into the one outside it. It is what stops a plugin\n // merging `next()`'s result from swapping its own object's prototype, and it\n // is the only cover for a plugin poisoning the one outside it.\n //\n // ⚠ Not on the ARGUMENTS: the chain fold\n // (`RoutesNamespace.#layerChainDefaults`) merges the caller's bag INSIDE the\n // call, and `mergeDefined`'s own `UNSAFE_KEY` skip depends on that. Cleaning\n // the arguments takes that branch's only live input away — measured, the\n // skip fires once per CHANNEL whose caller bag carries the key and whose hop\n // declares a default on that slot, so a chain hostile on both fires it\n // twice and one hostile on neither never reaches it at all. That skip\n // carries its own ⚠ against dropping it on a reachability argument, and\n // sanitising the arguments here is that argument arriving from the other\n // side.\n //\n // ⚠ A SNAPSHOT, never the source object, and that is what makes the closure\n // real rather than defeatable. A hop's result may be accessor-backed — the\n // shape `proto-key-guarantee` builds — so returning it by identity when it\n // reads clean lets the next read answer poisoned. The literal is what the\n // interceptor outside then reads, so there is one read of the hop's object\n // and it is this one.\n //\n // ⚠ Both slots are nullish-guarded because both can arrive empty here:\n // `RoutesNamespace.forwardState` hands back the `params` it was given, so\n // `forwardState(name, undefined)` and a `decodeParams` that fills only the\n // query channel both reach the first hop empty; `search` cannot from THAT\n // source (`search ?? EMPTY_SEARCH` resolves it) but very much can from an\n // inner interceptor spreading a partial result. Left untouched rather than\n // defaulted: the wrapper below is what normalises them.\n const sanitiseForwarded = (result: {\n name: string;\n params: Params;\n search: SearchParams;\n }) => {\n const params = result.params;\n const search = result.search;\n\n return {\n name: result.name,\n params:\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- the declared type cannot model what a codec or a partial return hands back\n params === undefined || params === null\n ? params\n : withoutUnsafeKey(params),\n search:\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- an interceptor spreading a partial result nulls the slot\n search === undefined || search === null\n ? search\n : withoutUnsafeKey(search),\n };\n };\n\n const rawForwardState = createTernaryInterceptable(\n SEAM.forwardState,\n (name: string, params: Params, search?: SearchParams) =>\n this.#routes.forwardState(name, params, search),\n interceptorsMap,\n sanitiseForwarded,\n snapshotForwarded,\n );\n\n const forwardState = ((\n name: string,\n params: Params,\n search?: SearchParams,\n ) => {\n const forwarded = rawForwardState(name, params, search);\n\n // ⚑ `name` and `params` into locals — and only those two (#1792).\n // `rawForwardState` is an interceptable, so `forwarded` may be backed by\n // accessors. Each of the two is read once by the channel check and once by\n // the object this returns, so without a local a chain result can answer\n // differently the second time and the check vouches for a value that never\n // ships. `search` needs no local: `assertChannelCorrect` never receives\n // it, so there is no check to fool, and hoisting it would only guarantee\n // the slot is read on the ERROR path too, handing a hostile interceptor a\n // side effect it does not have today. The same discipline the route\n // `updates` path enforces (#1738, pinned by `read-count-authority`); both\n // slots are pinned in `proto-key-guarantee` under \"the seam reads the\n // slots it checks\".\n const forwardedName = forwarded.name;\n const forwardedParams = forwarded.params;\n\n // The DECLARATION that matters is the RESOLVED route's — it owns the URL\n // that gets printed. When a chain resolved to a different route, say so:\n // a caller who wrote `navigate(\"src\", { lang })` looked at `src`'s config,\n // where `lang` is undeclared and legitimate, and needs to be told that the\n // hop landed somewhere that spells it `?lang`. Naming only the target\n // would read as a message about a route they never mentioned.\n assertChannelCorrect(\n \"forwardState\",\n forwardedName,\n forwardedParams,\n this.#routes.getQueryParams(forwardedName),\n () =>\n forwardedName === name\n ? \"the `params` bag leaving the forwardState chain\"\n : `the \\`params\\` bag leaving the forwardState chain (forwarded here from \"${name}\")`,\n );\n\n // ⚑ Read HERE, below the check, not hoisted with the other two (#1986).\n // The sanitiser needs the VALUE, and hoisting the read above\n // `assertChannelCorrect` would perform it on the ERROR path too — the\n // side effect the note above deliberately denies a hostile interceptor.\n // One read either way, on the success path only, exactly as before.\n // ⚠ Widened, not suppressed. `SimpleState` declares `search` required and\n // the note above says why that is a CONTRACT rather than a guarantee —\n // measured through this seam, an interceptor spreading a partial result\n // leaves `undefined` in the slot, and the sanitiser below would throw on\n // it. The cast states the runtime type the `params` slot handles one line\n // down with `?? EMPTY_PARAMS`.\n const forwardedSearch = forwarded.search as SearchParams | undefined;\n\n const searchAbsent =\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- an interceptor spreading a partial result nulls the slot; the declared type cannot model it\n forwardedSearch === undefined || forwardedSearch === null;\n\n // ⚑ Both channels leave sanitised (#1986). This seam is a PASS-THROUGH —\n // on the no-default fast path it hands back the caller's own bags — so\n // core mints no swap primitive here and the rule that closed #1957's doors\n // does not reach it. It is extended to this one anyway because the door is\n // INTERCEPTABLE: a plugin author arrives through the documented extension\n // seam and merges the result, so an own `__proto__` riding through would\n // be core handing a prototype-swap primitive to someone following the\n // instructions.\n //\n // ⚠ This copy is NOT the one that serves the chain, and the two do not\n // overlap. `sanitiseForwarded` above cleans what `next()` hands an\n // interceptor; this one cleans what the OUTERMOST interceptor hands the\n // caller, which no `next` wraps — and it is the only sanitiser at all when\n // no interceptor is registered, because the chain is skipped entirely\n // then. Removing either reds a cell the other leaves green.\n //\n // ⚠ AFTER the check, and that ordering is load-bearing: the check must\n // vouch for what SHIPS. What ships is now a SUBSET of what was checked,\n // and removing a key cannot introduce a mis-channelled one — while a\n // refusal throws before this line is reached.\n //\n // ⚠ `withoutUnsafeKey` is GATED on `hasOwn`, which is what makes this\n // affordable: a clean bag is returned by identity, so a navigation pays\n // one intrinsic read and no allocation. Pinned as its own cell in\n // `handed-out-containers-1957`, not left as a claim.\n //\n // ⚠ The query slot is guarded because an interceptor spreading a partial\n // result leaves `undefined` there — measured through this seam, not\n // assumed. The path slot has its own `?? EMPTY_PARAMS` for the same\n // reason, pinned in `forwardState.test.ts`.\n return {\n name: forwardedName,\n // The type says `params: P`, and across THIS boundary the type is a\n // contract, not a guarantee: `rawForwardState` is an interceptable, so\n // the value has passed through user code that can spread a partial\n // result. The net is reached only by that contract violation — still\n // worth surviving rather than putting `undefined` into `state.params`.\n // Pinned by \"normalises a params bag an interceptor dropped to\n // `undefined`\" in forwardState.test.ts, which fails if this is removed.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- see above: the declared type cannot model an interceptor's runtime return\n params: withoutUnsafeKey(forwardedParams ?? EMPTY_PARAMS),\n\n search: searchAbsent\n ? forwardedSearch\n : withoutUnsafeKey(forwardedSearch),\n };\n }) as unknown as RouterInternals[\"forwardState\"];\n\n // ⚑ **The SAME chain, one door lower (#2087).** `router.buildPath` runs the\n // `forwardState` seam on the caller's INTENT, so an injected value meets the\n // route's `defaultSearch` from ABOVE — the side `navigate` has always\n // injected from. Both doors then answer one intent with one URL, which is\n // INVARIANTS row 7.\n //\n // ⚠ The terminal is LITERAL — it resolves no `forwardTo`. That is this\n // door's contract (`buildPath(\"src\")` answers about `\"src\"`), and it is the\n // whole difference from the navigate door's terminal, which resolves.\n //\n // ⚠ No channel assert here, deliberately: render-path predicates are not\n // instrumented (#1572 / #1581). The bag still meets `canonicalize`'s\n // always-on mode gate below.\n const literalForwardState = createTernaryInterceptable(\n SEAM.forwardState,\n (name: string, params: Params, search?: SearchParams) => ({\n name,\n params,\n // The slot is optional and the sanitiser handles its absence; the\n // assertion states the shape `sanitiseForwarded` is typed against, the\n // same contract-not-guarantee the navigate seam records over its own.\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- see above\n search: search!,\n }),\n interceptorsMap,\n sanitiseForwarded,\n snapshotForwarded,\n );\n\n this.#buildPathIntent = (route, params, search) => {\n const forwarded = literalForwardState(route, params, search);\n\n // ⚑ Into locals for the reason the navigate door states over its own two\n // (#1792): the chain result may be accessor-backed, and each slot is read\n // once here and once by the call below.\n const forwardedParams = forwarded.params;\n const forwardedSearch = forwarded.search as SearchParams | undefined;\n\n // ⚠ No output sanitiser, and that is a difference from the navigate door\n // rather than an omission: what leaves THAT seam becomes `state.params`\n // directly, while what leaves this one goes through `canonicalize`, whose\n // `normalizeChannel` drops the unsafe key on both channels. Pinned rather\n // than argued — see the `__proto__` cell in the #2087 suite.\n return this.#routes.buildPathFromIntent(\n forwarded.name,\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- the declared type cannot model an interceptor spreading a partial result\n forwardedParams ?? EMPTY_PARAMS,\n forwardedSearch,\n );\n };\n\n registerInternals(this, {\n logger,\n makeState: (name, params, search, path) =>\n this.#state.makeState(name, params, search, path),\n getMetaForState: (name) => this.#routes.getMetaForState(name),\n getQueryParams: (name) => this.#routes.getQueryParams(name),\n forwardState,\n buildStateResolved: (name, params) =>\n this.#routes.buildStateResolved(name, params),\n port: () => this.#routes.getPort(),\n matchPath: (path, matchOptions) =>\n this.#routes.matchPath(path, matchOptions),\n getOptions: () => this.#options.get(),\n getAdoptedOrigins: () => this.#adoptedOrigins,\n addEventListener: (eventName, cb) =>\n this.#eventBus.addEventListener(eventName, cb),\n treeChanged: {\n emit: (event) => {\n this.#eventBus.emitTreeChanged(event);\n },\n subscribe: (handler) => this.#eventBus.subscribeTreeChanged(handler),\n listenerCount: () => this.#eventBus.treeChangedListenerCount(),\n isEmitting: () => this.#eventBus.isEmittingTreeChanged(),\n },\n emitTransitionError: (error) => {\n // Channel (b): a REPORT to observers, not a machine failure. It comes\n // from a plugin, at a moment core does not control, so it must never\n // drive a transition that could collide with one in flight.\n this.#eventBus.emitTransitionError(\n undefined,\n this.#state.get(),\n error as RouterError,\n );\n },\n navigateToNotFound: (path) => this.#navigation.navigateToNotFound(path),\n revalidateToNotFound: (path) =>\n this.#navigation.revalidateToNotFound(path),\n start: createInterceptable(\n SEAM.start,\n (path: string) => {\n return this.#lifecycle.start(path);\n },\n interceptorsMap,\n ),\n navigateToState: (state, navOpts) => {\n // Plugin-only navigation primitive (#525). Fire-and-forget safe like the\n // public facade methods — popstate handlers call it without awaiting —\n // but the safety now belongs to the namespace that creates the promise,\n // so this closure only owes callers the Promise shape.\n this.#assertNotReentrant();\n\n return Router.#asPromise(\n this.#navigation.navigateToState(state, navOpts ?? EMPTY_OPTS),\n );\n },\n interceptors: interceptorsMap,\n setRootPath: (rootPath) => {\n this.#routes.setRootPath(rootPath);\n },\n getRootPath: () => this.#routes.getStore().rootPath,\n getTree: () => this.#routes.getStore().tree,\n isDisposed: () => this.#eventBus.isDisposed(),\n validator: null,\n // Dependencies (issue #172)\n dependenciesGetStore: () => this.#dependenciesStore,\n // Clone support (issue #173)\n getCloneState: () => ({\n options: { ...this.#options.get() },\n // ⚑ The same withholding `getAll` performs one door over (#1823),\n // and for the same reason: this door has the identical spread and no\n // delete (#1957). The store is `Object.create(null)`, so an own\n // `\"__proto__\"` sits there as an ORDINARY key — legitimate, and\n // `get(\"__proto__\")` still answers — but a spread re-defines it on a\n // normal object and makes THIS container a prototype-swap primitive for\n // whoever merges it. `getCloneState` is reachable from the published\n // `@real-router/core/validation` subpath.\n //\n // ⚠ Consequence, and it is the one #1823 already took at `getAll`: a\n // dependency literally named `__proto__` does not reach a clone. The\n // base still holds it; the clone re-ingests this container, and the key\n // is no longer in it.\n dependencies: dropUnsafeKey({\n ...this.#dependenciesStore.dependencies,\n }),\n pluginFactories: this.#plugins.getAll(),\n // `logger` is a const in this constructor's scope (a RouterLogger class\n // instance), so getConfig() yields the resolved config a clone inherits\n // — frozen options don't carry `logger`, so cloneRouter reads it here.\n loggerConfig: logger.getConfig(),\n // Adjacent reason, one field over (#1880): `options.limits` is the\n // caller's bag, and an accessor on it is re-invoked by the clone's own\n // `createLimits`. These are already numbers. NOT the same mechanism as\n // `loggerConfig` above, though: that is a `getConfig()` call returning a\n // FRESH object per call, while this hands out `#limits` itself. What\n // makes handing it out safe is the freeze in `createLimits`.\n limits: this.#limits,\n limitKeys: this.#limitKeys,\n }),\n routeGetStore: () => this.#routes.getStore(),\n // Cross-namespace state (issue #174)\n getStateName: () => this.#state.get()?.name,\n isTransitioning: () => this.#eventBus.isTransitioning(),\n systemCommit: (toState, fromState, opts) =>\n this.#eventBus.systemCommit({ toState, fromState, opts }),\n routerExtensions: [],\n contextClaimRecords: new Map(),\n hydrationState: null,\n });\n\n // =========================================================================\n // Wire Dependencies\n // =========================================================================\n\n wireNamespaces<Dependencies>({\n router: this,\n options: this.#options,\n limits: this.#limits,\n dependenciesStore: this.#dependenciesStore,\n state: this.#state,\n routes: this.#routes,\n routeLifecycle: this.#routeLifecycle,\n plugins: this.#plugins,\n navigation: this.#navigation,\n lifecycle: this.#lifecycle,\n eventBus: this.#eventBus,\n });\n\n // =========================================================================\n // Bind Public Methods\n // =========================================================================\n // All public methods that access private fields must be bound to preserve\n // `this` context when methods are extracted as references.\n // See: https://github.com/tc39/proposal-bind-operator\n // =========================================================================\n\n // Path & State Building\n this.isActiveRoute = this.isActiveRoute.bind(this);\n this.buildPath = this.buildPath.bind(this);\n\n // State Management\n this.getState = this.getState.bind(this);\n this.getPreviousState = this.getPreviousState.bind(this);\n this.areStatesEqual = this.areStatesEqual.bind(this);\n this.shouldUpdateNode = this.shouldUpdateNode.bind(this);\n\n // Router Lifecycle\n this.isActive = this.isActive.bind(this);\n this.start = this.start.bind(this);\n this.stop = this.stop.bind(this);\n this.dispose = this.dispose.bind(this);\n\n // Route Lifecycle (Guards)\n this.canNavigateTo = this.canNavigateTo.bind(this);\n\n // Plugins\n this.usePlugin = this.usePlugin.bind(this);\n\n // Navigation\n this.navigate = this.navigate.bind(this);\n this.navigateToDefault = this.navigateToDefault.bind(this);\n this.navigateToNotFound = this.navigateToNotFound.bind(this);\n\n // Subscription\n this.subscribe = this.subscribe.bind(this);\n this.subscribeLeave = this.subscribeLeave.bind(this);\n this.isLeaveApproved = this.isLeaveApproved.bind(this);\n\n // =========================================================================\n // Flush initial-route guard factories\n // =========================================================================\n // Deferred out of wiring (#1331): the pending canActivate/canDeactivate\n // factories from initial route definitions are compiled and executed HERE,\n // on the fully-built and bound router — a factory calling read-only methods\n // (`buildPath()`, `isActiveRoute()`, `getState()`) does not hit a\n // half-assembled instance. Side-effectful calls (`navigate`, `usePlugin`,\n // route-CRUD) stay OUT OF CONTRACT: factories re-execute outside the\n // constructor (cloneRouter re-compiles definition guards per clone), so any\n // side effect would duplicate per re-execution — see CLAUDE.md. Runtime\n // add()/replace() compile guards in their own PREPARE phase and never touch\n // these pending maps.\n //\n // ⚑ The re-execution set is exactly the REGISTRATION paths (#1649).\n // `#recompileSlot` is a `Map` READ and not a factory invocation, so\n // \"compiled once per registration per router\" is the whole contract, with\n // no exception firing at a moment no caller could predict.\n //\n // Fail-closed on a factory throw: by this point a router reference leaked\n // from an earlier factory is fully operational, while later guards would\n // stay silently unregistered — a fail-open guard bypass. Disposing before\n // the rethrow turns any leaked reference into a ROUTER_DISPOSED-throwing\n // husk (pre-#1331 such a reference was inert because getInternals threw).\n try {\n this.#routes.flushPendingGuards();\n } catch (error) {\n this.dispose();\n\n throw error;\n }\n }\n\n // ============================================================================\n // Path & State Building\n // ============================================================================\n\n isActiveRoute(\n name: string,\n params?: Params,\n search?: SearchParams,\n strictEquality?: boolean,\n ignoreQueryParams?: boolean,\n ): boolean {\n // ⚑ ONE handle for the whole method. `getInternals` is a WeakMap lookup, and\n // this is a render-path predicate — a `<Link>` asks it on every render, so\n // repeating the lookup per validator hop is measurable (#1972).\n const ctx = getInternals(this);\n\n ctx.validator?.routes.validateIsActiveRouteArgs(\n name,\n params,\n strictEquality,\n ignoreQueryParams,\n );\n // ⚑ Beside it, not inside: the path bag is checked by the call above and\n // the query bag by its twin, so both halves of #1972's rule stand at every\n // door that takes both — including the two predicates.\n ctx.validator?.navigation.validateSearch(search, \"isActiveRoute\");\n ctx.validator?.routes.validateRouteName(name, \"isActiveRoute\");\n\n // Empty string is special case - warn and return false (root node is not a parent)\n if (name === \"\") {\n ctx.logger.warn(\n \"real-router\",\n 'isActiveRoute(\"\") called with empty string. Root node is not considered a parent of any route.',\n );\n\n return false;\n }\n\n // Slot-shift (RFC-4 M2 / #1548): `search` is the explicit query channel at\n // position 3; `strictEquality` / `ignoreQueryParams` shift to 4 / 5.\n return this.#routes.isActiveRoute(\n name,\n params,\n search,\n strictEquality,\n ignoreQueryParams,\n );\n }\n\n buildPath(route: string, params?: Params, search?: SearchParams): string {\n const ctx = getInternals(this);\n\n ctx.validator?.routes.validateBuildPathArgs(route);\n ctx.validator?.navigation.validateParamsShape(params, \"buildPath\");\n ctx.validator?.navigation.validateSearch(search, \"buildPath\");\n\n // ⚑ Core's SINGLE read of the caller's bag, and it happens here so that the\n // layer which judges and the layer which ships read the same one (#2134).\n // Everything below — the seam, the validator's value walk, the merge — sees\n // this object, so a key that answers differently per read is admitted on\n // the same value it is printed with.\n const ownParams = adoptChannel(params);\n\n ctx.validator?.navigation.validateParams(ownParams, \"buildPath\");\n\n // `search` (RFC-4 M2 / #1548) is the explicit query channel; the matcher\n // builds the query string from it and the path from `params`, resolving a\n // colliding name (`/items/:id?id`). Omitted → the v1 single-bag path.\n //\n // ⚑ The INTENT form (#1847). The merge belongs to whoever has an intent,\n // and this door is one of the two that do (`buildURL` is the other); the\n // executor below takes already-merged channels, which is what keeps a\n // navigation from running `canonicalize` twice over two independent reads\n // of the route's live default. The chain a plugin registers is not below\n // this line but ABOVE it — `#buildPathIntent` runs the `forwardState` seam\n // on the caller's intent first (#2087).\n // ⚑ CORE's copy, and #2087's rule is what makes that safe rather than what\n // forbids it. The rule is one seam, one input shape: both producers hand\n // their interceptors the same kind of object, and both copy at the same\n // point. The copy is `adoptChannel`, so what the chain sees carries the\n // caller's keys and values exactly — including an `undefined` one, which is\n // a plugin's removal marker and NOT core's to drop this far up.\n //\n // ⚠ The strip below the seam STAYS, on two arcs rather than one:\n // `canonicalize` for a known route, and `buildPathFromIntent`'s\n // `UNKNOWN_ROUTE` branch, which skips the pipeline and spells its own. It is\n // not redundant with the copy above — an interceptor runs BETWEEN them and\n // may inject `undefined` values of its own, which is the case that put the\n // strip there.\n return this.#buildPathIntent(route, ownParams ?? EMPTY_PARAMS, search);\n }\n\n // ============================================================================\n // State Management (delegated to StateNamespace)\n // ============================================================================\n\n getState<P extends Params = Params>(): State<P> | undefined {\n return this.#state.get<P>();\n }\n\n getPreviousState(): State | undefined {\n return this.#state.getPrevious();\n }\n\n areStatesEqual(\n state1: State | undefined,\n state2: State | undefined,\n ignoreQueryParams = true,\n ): boolean {\n getInternals(this).validator?.state.validateAreStatesEqualArgs(\n state1,\n state2,\n ignoreQueryParams,\n );\n\n return this.#state.areStatesEqual(state1, state2, ignoreQueryParams);\n }\n\n shouldUpdateNode(\n nodeName: string,\n ): (toState: State, fromState?: State) => boolean {\n getInternals(this).validator?.routes.validateShouldUpdateNodeArgs(nodeName);\n\n return RoutesNamespace.shouldUpdateNode(nodeName, (name) =>\n this.#routes.getMetaForState(name),\n );\n }\n\n // ============================================================================\n // Router Lifecycle\n // ============================================================================\n\n isActive(): boolean {\n return this.#eventBus.isActive();\n }\n\n /**\n * ONE fire-and-forget checkpoint for `start()`, deliberately — the same shape\n * `NavigationNamespace.#settle` gives the navigate family, and for the same\n * reason: a `.catch()` remembered at each `return` site is a thing that can be\n * forgotten, and a forgotten one is invisible until it leaks.\n *\n * It HAD been forgotten (#1605). The `ALREADY_STARTED` rejection left through\n * an early `return` above the suppressor, so a second, unawaited `start()`\n * raised an `unhandledRejection` — process-fatal under Node 22+'s default\n * `--unhandled-rejections=throw`, with a stack pointing at the cached error's\n * module constant rather than at the caller. Every return site now leaves\n * through `#runStart`, so no future early return can reopen it.\n */\n start(startPath: string): Promise<State> {\n const promiseState = this.#runStart(startPath);\n\n promiseState.catch(this.#onSuppressedStartError);\n\n return promiseState;\n }\n\n stop(): this {\n // SendCancelIfPossible → FSM CANCEL → the CANCEL action\n // aborts the in-flight controller (waking the pipeline). No separate abort.\n this.#eventBus.sendCancelIfPossible(this.#state.get());\n\n // `isStarting()` is included (#1185): a stop() while `start()` is parked in\n // an async start-interceptor (FSM STARTING, before `next()`) must cancel the\n // start, not silently no-op. `sendStop()` takes STARTING → IDLE (FSM table),\n // and `RouterLifecycleNamespace.start` re-checks `isIdle()` after the\n // interceptor chain and rejects with TRANSITION_CANCELLED — mirroring the\n // guard-phase behavior (which already cancels from TRANSITION_STARTED).\n if (\n !this.#eventBus.isReady() &&\n !this.#eventBus.isTransitioning() &&\n !this.#eventBus.isStarting()\n ) {\n return this;\n }\n\n // The STOP edge's `update` shifts the pair — the facade only sends.\n this.#eventBus.sendStop();\n\n return this;\n }\n\n dispose(): void {\n // Stryker disable next-line BlockStatement: equivalent — emptying the early-return re-runs the dispose body on a 2nd call, but it is fully idempotent (FSM `send(DISPOSE)` no-ops from DISPOSED, `disposeAll()` already cleared `#unsubscribes`, every clear is idempotent). (ConditionalExpression stays live: `→true` always-returns and never disposes = killed.)\n if (this.#eventBus.isDisposed()) {\n return;\n }\n\n // the FSM CANCEL action aborts the in-flight controller.\n this.#eventBus.sendCancelIfPossible(this.#state.get());\n\n if (this.#eventBus.isReady() || this.#eventBus.isTransitioning()) {\n this.#eventBus.sendStop();\n }\n\n this.#eventBus.sendDispose();\n this.#eventBus.clearAll();\n\n this.#plugins.disposeAll();\n\n // Safety net: clean up extensions plugins failed to remove in teardown\n const ctx = getInternals(this);\n\n for (const extension of ctx.routerExtensions) {\n for (const key of extension.keys) {\n delete (this as Record<string, unknown>)[key];\n }\n }\n\n ctx.routerExtensions.length = 0;\n\n // Safety net: release context namespace claims plugins failed to release in teardown\n ctx.contextClaimRecords.clear();\n\n // Safety net: drop interceptors plugins failed to remove in teardown (#1199).\n // The third per-plugin registration channel — symmetric with routerExtensions\n // / contextClaimRecords above. `buildPath` is not method-swapped by dispose\n // and reads this Map live, so a leaked interceptor would otherwise still run\n // on the disposed router.\n ctx.interceptors.clear();\n\n this.#routes.clearRoutes();\n this.#routeLifecycle.clearAll();\n this.#dependenciesStore.dependencies = objectCreate(\n null,\n ) as Partial<Dependencies>;\n\n this.#markDisposed();\n }\n\n // ============================================================================\n // Route Lifecycle (Guards)\n // ============================================================================\n\n canNavigateTo(name: string, params?: Params, search?: SearchParams): boolean {\n const ctx = getInternals(this);\n\n ctx.validator?.routes.validateRouteName(name, \"canNavigateTo\");\n ctx.validator?.navigation.validateParamsShape(params, \"canNavigateTo\");\n ctx.validator?.navigation.validateSearch(search, \"canNavigateTo\");\n\n // The same single read as the two producers (#2134) — the predicate must\n // answer about the bag they would ship, not about an earlier read of it.\n //\n // ⚠ Guarded, because the read is APPLICATION code and this door is\n // documented TOTAL (INVARIANTS canNavigateTo #5, #725): a getter that throws\n // is an unreachable route, not an exception into a `<Link>` render. The\n // strip below the seam sits inside the `try` around `canonicalize`, so a\n // read placed ABOVE that one needs a net of its own — this is that net, and\n // the two together mean no read on this door escapes as an exception.\n let ownParams: Params | undefined;\n\n try {\n ownParams = adoptChannel(params);\n } catch (error) {\n ctx.logger.warn(\n \"router.canNavigateTo\",\n `Reading the params bag for route \"${name}\" threw; treating the route as unreachable.`,\n error,\n );\n\n return false;\n }\n\n ctx.validator?.navigation.validateParams(ownParams, \"canNavigateTo\");\n\n if (!this.#routes.hasRoute(name)) {\n return false;\n }\n\n // Mirror EVERY way `navigate` refuses these same arguments, not only the\n // guard verdict (#1576). A declared query key handed in the PATH bag makes\n // `navigate` throw synchronously at the facade (channel guard P1, #1572), so\n // the route is unreachable with this input — exactly the situation invariant\n // canNavigateTo #5 already answers `false` to for an unbuildable path (#725).\n // Answering `true` here promised a navigation that throws on the click.\n //\n // The RAW caller bag, before `forwardState`: the same argument, the same\n // registry and the same name P1 reads, so the predicate cannot be stricter\n // OR laxer than the verb. The `/items/:id?id` collision is absent from\n // `queryNames` by construction (#843 / #1549), so it stays navigable in both.\n //\n // A `false` rather than a rethrow: a capability predicate answers, it never\n // throws (#725), and it runs on every `<Link>` render across six adapters —\n // which is exactly why P1 does not instrument the predicates (#1572).\n if (\n findMisChanneledKey(params, this.#routes.getQueryParams(name)) !==\n undefined\n ) {\n return false;\n }\n\n // Resolution runs USER code and must not escape as an exception (#1577):\n // a dynamic `forwardTo` callback, a plugin's `forwardState` interceptor, and\n // the caller's own bag (the merge walks it key by key, so an accessor-backed\n // key throws here — the channel guard itself does NOT, it catches its own\n // read) all sit on this one call. The\n // predicate is documented TOTAL — it answers, it never throws (INVARIANTS\n // canNavigateTo #5, #725) — and its sibling `isActiveRoute` has wrapped the\n // very same primitive since #1573 (`isActiveRoute`'s own `try` around the\n // NAMESPACE `forwardState`, not the seam). Leaving\n // this one bare made the two render-path predicates disagree about what a\n // throwing resolution means.\n //\n // A separate `try` rather than widening the one below: that one is SILENT by\n // design (an unbuildable path is a normal \"unreachable with this input\"\n // answer, #725), while user code crashing is an operational fault that must\n // never vanish — the same split #959 draws for a throwing guard.\n // Stages ① + ③ + the mode gate, one pass through the pipeline (nav-pipeline\n // Phase 2, step 2-3). `canonicalize` reaches the `forwardState` seam through\n // `port.resolveForward`, which IS `ctx.forwardState` — so the resolution,\n // the interceptor zone and the channel CHECK all happen on that one seam\n // rather than in a composition assembled here.\n // Read ONCE (#1589): a second read for `buildURL` below would cost every\n // `<Link>` render one. The port is one object per router, created at wiring\n // time, so it could only ever return the same reference.\n const port = this.#routes.getPort();\n\n let canonical;\n\n try {\n canonical = canonicalize(\n port,\n name,\n // The door's own copy (#2134), not the caller's bag: reading it a second\n // time here is exactly the divergence the copy exists to remove — the\n // predicate would answer about a later read than the one it validated.\n //\n // The singleton, not a fresh `{}` (#1589): this predicate runs on every\n // `<Link>` render too, and `normalizeChannel` recognises `EMPTY_PARAMS` by\n // identity — a literal makes it walk and re-allocate instead.\n ownParams ?? EMPTY_PARAMS,\n search,\n );\n } catch (error) {\n ctx.logger.warn(\n \"router.canNavigateTo\",\n `Resolving route \"${name}\" threw while answering the predicate; treating the route as unreachable.`,\n error,\n );\n\n return false;\n }\n\n // Build `toState` exactly as `buildNavigateState` does — WITH route-meta and\n // normalized params — so `getTransitionPath` takes its STANDARD PATH and\n // trims the shared ancestor, mirroring navigate's guard set (#970). A\n // meta-less `toState` makes both sides meta-less (the committed `getState()`\n // carries no meta after a path-matched `start()`), so `getTransitionPath`\n // takes FAST PATH 3 and (de)activates the WHOLE chain incl. shared ancestors\n // → false-negative (\"Link disabled though the click would succeed\").\n // `normalizeChannel` also aligns the params guards observe with navigate's.\n // `materializePending` mirrors the navigate guard phase, where guards see\n // an unfrozen `toState` carrying `DEFAULT_TRANSITION` (the freeze, and the\n // real meta, arrive later in `completeTransition`).\n //\n // A capability predicate must answer, not throw: if the target path can't be\n // built from these params (e.g. a required path param is missing), the route\n // is simply unreachable with this input — return `false` rather than letting\n // `buildPath` throw (#725).\n let toState: State;\n\n try {\n // ⑤a then ⑤b, from the SAME canonical intent — so `toState.search` and\n // `toState.path` cannot drift. `materializePending` mirrors the navigate\n // guard phase, where guards see an unfrozen `toState`.\n toState = materialize(canonical, buildURL(canonical, port));\n } catch {\n return false;\n }\n\n const fromState = this.#state.get();\n\n const { toDeactivate, toActivate } = getTransitionPath(\n toState,\n fromState,\n (routeName) => this.#routes.getMetaForState(routeName),\n );\n\n return this.#routeLifecycle.canNavigateTo(\n toDeactivate,\n toActivate,\n toState,\n fromState,\n );\n }\n\n // ============================================================================\n // Plugins\n // ============================================================================\n\n usePlugin(\n ...plugins: (PluginFactory<Dependencies> | false | null | undefined)[]\n ): Unsubscribe {\n // Post-dispose guard, mirroring #946 for subscribe/subscribeLeave. A\n // reference captured before dispose() (`const up = router.usePlugin`)\n // bypasses the #markDisposed method swap, so the swap alone is not enough:\n // without this, the factory would run on a disposed router (real side\n // effects), listeners would land in the cleared emitter, and teardown would\n // never fire — a silent zombie plugin (#1196).\n if (this.#eventBus.isDisposed()) {\n throw freezeThrownError(new RouterError(errorCodes.ROUTER_DISPOSED));\n }\n\n const filtered = plugins.filter(Boolean) as PluginFactory<Dependencies>[];\n\n if (filtered.length === 0) {\n return () => {};\n }\n\n const ctx = getInternals(this);\n\n ctx.validator?.plugins.validatePluginLimit(\n this.#plugins.count(),\n this.#limits,\n );\n for (const plugin of filtered) {\n // `getAll()` sits inside the optional-chain argument on purpose: with no\n // validator installed (production default) the `?.` short-circuits and the\n // array is never allocated. Hoisting it out would either allocate on the\n // no-validator hot path or push the dev-only branch out of coverage.\n ctx.validator?.plugins.validateNoDuplicatePlugins(\n plugin,\n this.#plugins.getAll(),\n );\n }\n\n return this.#plugins.use(...filtered);\n }\n\n // ============================================================================\n // Subscription (backed by EventEmitter)\n // ============================================================================\n\n subscribe(listener: SubscribeFn): Unsubscribe {\n EventBusNamespace.validateSubscribeListener(listener);\n\n return this.#eventBus.subscribe(listener);\n }\n\n subscribeLeave(listener: LeaveFn): Unsubscribe {\n EventBusNamespace.validateSubscribeLeaveListener(listener);\n\n return this.#eventBus.subscribeLeave(listener);\n }\n\n isLeaveApproved(): boolean {\n return this.#eventBus.isLeaveApproved();\n }\n\n // ============================================================================\n // Navigation\n // ============================================================================\n\n navigate(\n target: NavigationTarget,\n options?: NavigationOptions,\n ): Promise<State>;\n navigate(\n routeName: string,\n routeParams?: Params,\n routeSearch?: SearchParams,\n options?: NavigationOptions,\n ): Promise<State>;\n navigate(\n nameOrTarget: string | NavigationTarget,\n paramsOrOptions?: Params | NavigationOptions,\n routeSearch?: SearchParams,\n options?: NavigationOptions,\n ): Promise<State> {\n this.#assertNotReentrant();\n\n const ctx = getInternals(this);\n\n // Two equal-standing forms (RFC-4 M2 / #1548): the descriptor\n // `navigate(target, opts)` (opts at position 2) and the positional\n // `navigate(name, params, search, opts)` (opts at position 4). The v1\n // `navigate(name, params, opts)` form is gone — its position-3 opts is now\n // the `search` slot; unpack whichever form the caller used into one path.\n let routeName: string;\n let routeParams: Params | undefined;\n let search: SearchParams | undefined;\n let opts: NavigationOptions;\n\n // The static type excludes null, but `navigate(null)` is a real runtime\n // misuse that must stay graceful (ROUTE_NOT_FOUND, not a crash on\n // `null.name`) — the null check routes it to the positional branch.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime null guard for navigate(null)\n if (typeof nameOrTarget === \"object\" && nameOrTarget !== null) {\n routeName = nameOrTarget.name;\n routeParams = nameOrTarget.params;\n search = nameOrTarget.search;\n opts = (paramsOrOptions as NavigationOptions | undefined) ?? EMPTY_OPTS;\n } else {\n routeName = nameOrTarget;\n routeParams = paramsOrOptions as Params | undefined;\n search = routeSearch;\n opts = options ?? EMPTY_OPTS;\n }\n\n throwOnMisChanneledKey(ctx, \"navigate\", routeName, routeParams);\n\n ctx.validator?.navigation.validateNavigateArgs(routeName);\n ctx.validator?.navigation.validateParamsShape(routeParams, \"navigate\");\n ctx.validator?.navigation.validateSearch(search, \"navigate\");\n ctx.validator?.navigation.validateNavigationOptions(opts, \"navigate\");\n\n // ⚑ One read for the whole navigation (#2134). `buildNavigateState` runs\n // `validateStateBuilderArgs` further down the pipeline, and it now receives\n // this object rather than the caller's — which is why the door's count\n // falls from four reads to one and not merely to two.\n //\n // ⚠ A REJECTION, not a synchronous throw. The read is application code, and\n // everything this method answers with is a promise — a getter that throws\n // belongs in the caller's `.catch()`, not past it. The facade's own guards\n // above DO throw synchronously: those are programmer error, and #1572 pins\n // that shape deliberately.\n let ownParams: Params | undefined;\n\n try {\n ownParams = adoptChannel(routeParams);\n } catch (error: unknown) {\n // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- preserve the original throw shape from the caller's own accessor\n return Promise.reject(error);\n }\n\n ctx.validator?.navigation.validateParams(ownParams, \"navigate\");\n\n return Router.#asPromise(\n this.#navigation.navigate(\n routeName,\n ownParams ?? EMPTY_PARAMS,\n search,\n opts,\n ),\n );\n }\n\n navigateToDefault(options?: NavigationOptions): Promise<State> {\n this.#assertNotReentrant();\n\n const ctx = getInternals(this);\n\n ctx.validator?.navigation.validateNavigateToDefaultArgs(options);\n\n const opts = options ?? EMPTY_OPTS;\n\n ctx.validator?.navigation.validateNavigationOptions(\n opts,\n \"navigateToDefault\",\n );\n\n return Router.#asPromise(this.#navigation.navigateToDefault(opts));\n }\n\n navigateToNotFound(path?: string): State {\n this.#assertNotReentrant();\n\n if (!this.#eventBus.isActive()) {\n throw freezeThrownError(new RouterError(errorCodes.ROUTER_NOT_STARTED));\n }\n\n if (path !== undefined && typeof path !== \"string\") {\n throw new TypeError(\n `[router.navigateToNotFound] path must be a string, got ${typeof path}`,\n );\n }\n\n if (path !== undefined) {\n // No boot-window predicate here any more (#1647). The window it named is\n // held by two mechanisms that were already load-bearing under it: from an\n // `onStart` hook or a `$start` / transition listener `#assertNotReentrant`\n // above throws first, and from a start INTERCEPTOR the machine is still\n // STARTING, where `SYSTEM_COMMIT` is not declared — so `systemCommit()`\n // refuses and names the phase itself. A guard OF the boot navigation stays\n // legal exactly as before: the primitive aborts that navigation first, so\n // its 404 displaces the boot's commit rather than being overwritten.\n return this.#navigation.navigateToNotFound(path);\n }\n\n const current = this.#state.get();\n\n // #1172: a path-less call derives the default path from the committed state.\n // During the two-phase start window the router is active (`isActive()` true)\n // while `getState()` is still undefined, so throw an actionable RouterError\n // instead of a cryptic `TypeError` from dereferencing the absent state —\n // same class as the #939 always-on invariant guards. Unconditional on the\n // in-flight question above: there is no path to derive either way.\n if (current === undefined) {\n throw freezeThrownError(\n new RouterError(errorCodes.ROUTER_NOT_STARTED, {\n message:\n \"[router.navigateToNotFound] cannot derive the path before the start navigation commits — pass an explicit path\",\n }),\n );\n }\n\n return this.#navigation.navigateToNotFound(current.path);\n }\n\n /**\n * Hands the namespace's result back as the `Promise<State>` the public API\n * owes, and does nothing else.\n *\n * A non-Promise means the navigation already settled synchronously — the\n * return TYPE says so, which is what retired `lastSyncResolved`. Suppression is\n * not the facade's business any more: the namespace attaches it where the\n * promise is created, the only layer that can tell a fresh rejection from one\n * of its own pre-suppressed singletons.\n *\n * The wrap allocates nothing extra: a result that is already a Promise is\n * returned by identity.\n */\n static #asPromise(result: State | Promise<State>): Promise<State> {\n return result instanceof Promise ? result : Promise.resolve(result);\n }\n\n #runStart(startPath: string): Promise<State> {\n if (!this.#eventBus.canStart()) {\n return Promise.reject(CACHED_ALREADY_STARTED_ERROR);\n }\n\n getInternals(this).validator?.navigation.validateStartArgs(startPath);\n\n // FSM bookkeeping is split across the facade and RouterLifecycleNamespace by\n // design, NOT a missed consolidation (#940): `sendStart()` runs HERE, before\n // the interceptor chain, so the STARTING window spans the whole start\n // pipeline. A pre-`next()` interceptor throw then unwinds via STARTING →\n // `sendFail`, which emits TRANSITION_ERROR from STARTING (EventBusNamespace\n // FAIL action) for `onTransitionError` plugins. Moving `sendStart()` into the\n // namespace (the interceptor *target*) would skip STARTING on a pre-`next()`\n // throw — the namespace is never reached — silently dropping that\n // TRANSITION_ERROR: a #668 regression. The commit (`completeStart`) lives in\n // the namespace; recovery needs facade state (`#state`, `#lifecycle`), so it\n // stays here in `#unwindFailedStart`.\n this.#eventBus.sendStart();\n\n // Convert sync interceptor throws to rejections so the recovery path is\n // reachable; otherwise the throw escapes synchronously, the FSM is left in\n // STARTING, and the router is permanently bricked (#668).\n let internalStart: Promise<State>;\n\n try {\n const chainResult: unknown = getInternals(this).start(startPath);\n\n // A `start` interceptor that returns without calling next() yields a\n // non-thenable (typically undefined); the `.catch` below would then throw\n // a cryptic `TypeError: ...reading 'catch'` and leave the FSM stuck in\n // STARTING. Reject with an actionable message so recovery unwinds via\n // #unwindFailedStart — the same deferred-crash class as the #939\n // start-path guard (#1411).\n internalStart =\n typeof (chainResult as { then?: unknown } | null | undefined)?.then ===\n \"function\"\n ? (chainResult as Promise<State>)\n : Promise.reject(\n new TypeError(\n \"[router.start] a `start` interceptor returned without calling next(). Every start interceptor must return `next(path)`.\",\n ),\n );\n } catch (syncError: unknown) {\n // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- preserve original throw shape from user-provided start interceptor\n internalStart = Promise.reject(syncError);\n }\n\n return internalStart.catch((error: unknown) =>\n this.#unwindFailedStart(error),\n );\n }\n\n /**\n * Rejects a synchronous reentrant navigation — `navigate` /\n * `navigateToDefault` / `navigateToState` / `navigateToNotFound` called from\n * inside a navigation core has not finished with. Throws synchronously: inside\n * a listener the emit's `onListenerError` isolation surfaces it (visible,\n * non-fatal); a DEFERRED (async / microtask) navigate runs after the window\n * closes and is allowed. Always-on core invariant guard (not validator-gated).\n *\n * TWO windows, because application code runs in two places core does not\n * control, on opposite sides of the announce:\n *\n * - **Dispatch** (`isProcessing`) — a transition-event listener, mid-emit\n * (RFC navigation-cancellation-unification §4) — and, since #1647, a\n * `$start` listener too: a plugin's `onStart` runs on a READY machine that\n * still owes the boot's commit, so a navigation from there would run to\n * completion and the boot would overwrite it. Counting the `$start` emit\n * puts that window under this rule rather than a predicate of its own.\n * - **Pre-start** (`isPreparing`, #1610) — application code running BEFORE the\n * first emit. `INVARIANTS.md` row 4 owns the site list and the exclusions;\n * what belongs here is why the window needs a predicate of its own: the\n * dispatch depth cannot see it, there has been no emit yet, and without one\n * a nested `navigate()` runs to completion, commits a phantom\n * `TRANSITION_SUCCESS`, and shifts the outer transition's `fromState`.\n *\n * A guard is deliberately NOT either of them: it runs after the announce, so\n * the classic guard-redirect (`navigate(...)` then `return false`) stays a\n * plain supersede.\n *\n * ⚑ The two windows get DIFFERENT messages (#1665), and that is not polish.\n * The code names a rule the caller broke, and unlike a state error\n * (`ROUTER_DISPOSED`, `SAME_STATES`) the remedy does not follow from the name\n * — which is why the bare code produced two docs issues (#1203, #1219) and\n * nothing else. One text cannot serve both halves: \"you are inside a\n * listener\" is false for an interceptor, where no emit is on the stack at\n * all, and a developer told that reads their error as spurious. Splitting the\n * `||` costs the happy path nothing: it already evaluated both predicates in\n * this order.\n */\n #assertNotReentrant(): void {\n if (this.#eventBus.isProcessing()) {\n throw freezeThrownError(\n new RouterError(errorCodes.REENTRANT_NAVIGATION, {\n message:\n \"[router] cannot start a navigation from inside a router event listener — the nested navigation would commit a state the outer one overwrites. Defer it: queueMicrotask(() => router.navigate(...)), await the current transition, or use an async listener.\",\n }),\n );\n }\n\n if (this.#navigation.isPreparing()) {\n throw freezeThrownError(\n new RouterError(errorCodes.REENTRANT_NAVIGATION, {\n message:\n \"[router] cannot start a navigation from inside a forwardState interceptor, a route's encodeParams or dynamic forwardTo callback, or a defaultRoute/defaultParams/defaultSearch option callback — they run while a navigation is being prepared, before it is announced. Defer it: queueMicrotask(() => router.navigate(...)).\",\n }),\n );\n }\n\n // ⚠ The THIRD window, with its own sentence for the reason the two above\n // have theirs (#1665): no emit is on the stack and no navigation is being\n // prepared, so both of those texts read as spurious here. Without the\n // refusal the revalidation defers to a navigation that may never commit, and\n // a state on a dropped route then has nothing left to revalidate it (#1759).\n if (this.#routes.isRevalidating()) {\n throw freezeThrownError(\n new RouterError(errorCodes.REENTRANT_NAVIGATION, {\n message:\n \"[router] cannot start a navigation from inside replace()'s revalidation — the revalidation would then defer to a commit that may never happen. Defer it: queueMicrotask(() => router.navigate(...)).\",\n }),\n );\n }\n }\n\n /**\n * Settles the FSM after a failed start pipeline, then re-throws so the\n * rejection still surfaces to the caller. Three cases, by what the pipeline\n * reached before throwing:\n *\n * - **Pre-commit, READY** (`isReady()` and no committed state): an interceptor\n * threw after `completeStart()` reached READY but before any state committed\n * (e.g. an activation guard blocked the start navigation) — return READY →\n * IDLE via `stop()` so the router is reusable.\n * - **Pre-commit, STARTING** (`isStarting()`): the pipeline threw before\n * `completeStart()` — a sync interceptor throw before `next()`, or a throw\n * inside the namespace before commit — so unwind STARTING → IDLE via\n * `sendFail`, which also emits TRANSITION_ERROR from STARTING (#668).\n * - **Post-commit, READY with committed state** (neither branch fires): a\n * loader/interceptor threw AFTER `navigateToState` committed and emitted\n * TRANSITION_SUCCESS (the SSR/RSC loader window). Keep the committed state —\n * rolling back would retract an observed success (\"phantom success\", #763);\n * the error still surfaces via the re-throw.\n */\n #unwindFailedStart(error: unknown): never {\n if (this.#eventBus.isReady() && this.#state.get() === undefined) {\n this.#eventBus.sendStop();\n } else if (this.#eventBus.isStarting()) {\n this.#eventBus.sendFail(undefined, error);\n }\n\n throw error;\n }\n\n #markDisposed(): void {\n this.navigate = throwDisposed;\n this.navigateToDefault = throwDisposed;\n this.navigateToNotFound = throwDisposed;\n this.start = throwDisposed;\n this.stop = throwDisposed;\n this.usePlugin = throwDisposed;\n\n this.subscribe = throwDisposed;\n this.subscribeLeave = throwDisposed;\n this.canNavigateTo = throwDisposed;\n }\n}\n\nfunction throwDisposed(): never {\n throw freezeThrownError(new RouterError(errorCodes.ROUTER_DISPOSED));\n}\n\n/** The bags a mutation could still be aimed at, weakly (#2148). */\nexport interface AdoptedOrigins {\n readonly defaultParams?: WeakRef<object>;\n readonly defaultSearch?: WeakRef<object>;\n}\n\n/**\n * Is this slot worth a weak note (#2148)?\n *\n * ⚠ Each test removes a watch that could never fire, and this is the only place\n * the rule is written:\n *\n * - a CALLBACK is not a container, so there is nothing for a mutation to detach;\n * - a FROZEN bag cannot move — which is what keeps a clone out, since\n * `cloneRouter` constructs from the base's frozen copies, and which also admits\n * a caller bag the application froze itself;\n * - core's own `defaultOptions` value is a process-wide singleton, and it reaches\n * a clone's constructor through the RESOLVED options — so without this test a\n * clone takes a reference to the shared `defaultSearch` literal, once per\n * request under SSR.\n *\n * `options-ownership-1832` pins all three.\n */\nfunction isWatchableBag(value: unknown): value is object {\n return (\n isBag(value) &&\n !Object.isFrozen(value) &&\n value !== defaultOptions.defaultParams &&\n value !== defaultOptions.defaultSearch\n );\n}\n\n/**\n * A weak note of where each adopted bag came from.\n *\n * ⚠ Reads `routerOptions`, which is CORE's object, so no application code runs\n * here — what it stores is the caller's bag held in that slot.\n * {@link isWatchableBag} decides which slots qualify.\n */\nfunction weakOrigins<Dependencies extends DefaultDependencies>(\n routerOptions: Omit<Partial<Options<Dependencies>>, \"logger\">,\n): AdoptedOrigins {\n const { defaultParams, defaultSearch } = routerOptions;\n\n return freeze({\n ...(isWatchableBag(defaultParams) && {\n defaultParams: new WeakRef(defaultParams),\n }),\n ...(isWatchableBag(defaultSearch) && {\n defaultSearch: new WeakRef(defaultSearch),\n }),\n });\n}\n\n/**\n * What the `forwardState` chain is handed when a plugin is ON it (#1849).\n *\n * An interceptor is application code and the bags it receives are the\n * CALLER's. Read one and forward it, and the value the interceptor acted on\n * is not the value `canonicalize` reads a moment later — measured on a\n * getter-backed bag, the interceptor saw `S1` while the URL printed `S2`, on\n * both doors. One shallow copy per channel makes those two reads one.\n *\n * ⚠ A spread, NOT `normalizeChannel`. That one drops a key whose value is\n * `undefined`, and `undefined` is `persistent-params`' removal marker — the\n * copy would erase the signal before the plugin could read it. Measured:\n * eight of that package's cells red on the `normalizeChannel` form.\n *\n * ⚠ Absence passes through on BOTH spellings. `{ ...null }` is `{}`, which\n * would turn \"no bag\" into \"empty bag\" above the code that tells them apart.\n */\nfunction snapshotForwarded(\n name: string,\n params: Params,\n search?: SearchParams,\n): [string, Params, SearchParams | undefined] {\n return [name, adoptChannel(params), adoptChannel(search)];\n}\n\n/**\n * Is this option slot a BAG rather than a callback or an absent value?\n *\n * ⚑ Takes `unknown` deliberately. Spelled inline, TypeScript narrows\n * `typeof x === \"object\"` against the declared union — which carries no `null` —\n * and `no-unnecessary-condition` then calls the null check redundant. It is not:\n * `typeof null === \"object\"`, `null` is what a config from `JSON.parse` or\n * `cfg.x ?? null` actually carries, and it must fall through to the validator\n * that names the option rather than reach `Object.keys` and raise a bare\n * `TypeError`. Same disagreement between the static type and the runtime that\n * `adoptForeignBag` spells out for its own `== null` guard.\n *\n * ⚠ The callback arm falls through here too, and must: a function is the\n * application's, stays the application's, and is called — not enumerated.\n */\nfunction isBag(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\n/**\n * Core-owned copies of the three `Options` sub-bags core HOLDS (#2171).\n *\n * ⚑ **The boundary is here, and it is one place for the three.** Before this,\n * core froze the level its own spread minted and stopped (#1832), so every\n * nested bag stayed the caller's object: held live, re-read on every\n * `navigateToDefault`, and handed back by identity through `getOptions()`.\n * #2145 retired the contracts that kept it that way; the measurements are\n * there.\n *\n * ⚠ **`queryParams` is deliberately NOT here, and that is a measured exception\n * rather than an omission.** Adopting it means handing back\n * `snapshotQueryParams`' copy, which carries the four DECLARED names only — so\n * a mis-spelled `arrayFromat` disappears from `getOptions().queryParams` and\n * `@real-router/validation-plugin` loses the unknown-option report it raises\n * for exactly that typo. Measured on this branch: the key set went from\n * `[\"arrayFormat\", \"arrayFromat\", \"extra\"]` to `[\"arrayFormat\"]`, with all 815\n * of that plugin's cells still green — an UNPINNED behaviour, which is why the\n * cost is recorded here as well as in `query-strategy-formats-1796.test.ts`.\n * What #2171 retired for that slot is its clone-time RE-READ instead:\n * `cloneRouter` inherits the base's resolved strategies, which is where the\n * #2032 defect actually lived.\n *\n * ⚠ **Each slot keeps the read semantics it already had.** `limits` is\n * own-enumerable, matching `createLimits`' spread and the `#limitKeys`\n * snapshot below — a copy that saw MORE than the spread would make the clone\n * stricter than its base, which is #1961's own divergence. `defaultParams` /\n * `defaultSearch` are own-enumerable too, the rule `packages/core/CLAUDE.md` ›\n * Supported Input Shapes already states for channel bags.\n *\n * ⚠ **Only what the caller PASSED.** `defaultOptions` supplies the absent slots\n * one frame later, inside `OptionsNamespace`, and those are core's own frozen\n * constants — copying them would allocate per router and own nothing new.\n *\n * ⚠ **A callback arm is not a bag.** `defaultParams` and `defaultSearch` each\n * accept a function, and a function is the application's, stays the\n * application's, and is called — not enumerated.\n *\n * ⚠ NOT a deep freeze. Deciding depth by asking each nested bag for its\n * `constructor` leaves an array inside a frozen bag writable, and moving that\n * array moves what the router navigates to. `options-ownership-1832.test.ts`\n * owns that shape list.\n */\nfunction adoptOptionBags<Dependencies extends DefaultDependencies>(\n routerOptions: Omit<Partial<Options<Dependencies>>, \"logger\">,\n): Omit<Partial<Options<Dependencies>>, \"logger\"> {\n const adopted = { ...routerOptions };\n\n if (routerOptions.limits != null) {\n adopted.limits = copyOwnData(\"limits\", routerOptions.limits);\n }\n\n if (isBag(routerOptions.defaultParams)) {\n adopted.defaultParams = copyOwnData(\n \"defaultParams\",\n routerOptions.defaultParams,\n );\n }\n\n if (isBag(routerOptions.defaultSearch)) {\n adopted.defaultSearch = copyOwnData(\n \"defaultSearch\",\n routerOptions.defaultSearch,\n );\n }\n\n return adopted;\n}\n\n/** The frozen empty snapshot, for a caller that supplied no `queryParams` at all. */\nconst EMPTY_QUERY_PARAMS: QueryParamsConfig = Object.freeze({});\n\n/**\n * Coerces one format slot to its STRING key, once, at snapshot time.\n *\n * ⚑ The snapshot copies the four values, and copying a value by reference is not\n * the same as capturing it. `requireStrategy` coerces each one with\n * `ToPropertyKey` to look it up, so an object-valued format is re-read on every\n * matcher build — and the matcher is rebuilt more often than \"at construction\"\n * suggests: `setRootPath`, `replace()`, and `dispose()`, which reaches\n * `resetStore` → `rebuildTreeInPlace` → `createMatcher`.\n *\n * Without it a `{ toString }` answering `\"none\"` then `\"bogusTypo\"` constructs\n * cleanly and makes **`dispose()` throw** the config error, out of a method that\n * is idempotent by contract and is called from `finally` blocks — where a throw\n * discards whatever error was already travelling. Freezing the CONTAINER does\n * not reach this: it stops the bag being swapped, and a single slot can still\n * answer twice.\n *\n * Coercing here means the caller's object is read exactly once per router, at\n * construction, and every later rebuild resolves from a string. It does not\n * change which configs are refused — `requireStrategy` sees the same key it\n * would have computed — only how many times the caller is asked.\n *\n * ⚠ `typeof` first is a PERF TERM, and a tiny one — it is NOT a guard, and\n * reading it as one is what this paragraph exists to prevent. Measured both\n * ways. INERT: delete the branch, so every non-nullish slot goes through\n * `String(value)`, and the whole suite stays green — for a string\n * `String(s)` returns `s` itself, and `ToString` of a String consults no user\n * code, so nothing observable rides on the test. WORTH: the branch saves\n * ~0.9 ns per slot, i.e. ~3.5 ns per `createRouter`, against a construction\n * measured at ~13.6 µs — 0.03 %, two orders of magnitude under the 10 %\n * CodSpeed gate, and nothing in the gate measures it. It stays for the reason\n * the same shape stays in `requireStrategy`: the coercion is reserved for\n * exactly the values that are not already keys.\n *\n * ⚑ So it is an EQUIVALENT MUTANT by construction: no test can kill it, and a\n * mutation run reporting this branch as survived is right. This note is the\n * answer to that report — do not \"cover\" it with a test that cannot fail.\n *\n * ⚠ NULLISH IS ABSENCE, and both halves carry weight. Guarding `undefined`\n * alone lets `null` reach `String(null)` and become the STRING `\"null\"`, which\n * `makeOptions`' `?? DEFAULT_QUERY_PARAMS.x` can then never rescue, because it\n * is handed a non-nullish value. `null` is what a config from `JSON.parse`,\n * from YAML, or from `cfg.x ?? null` actually carries — never `undefined` — so\n * this is the reachable half of \"nullish\", not the exotic one.\n *\n * ⚠ A `symbol` is deliberately NOT special-cased, and the reason is NOT that\n * `String` throws on one: it does not. `String(Symbol(\"x\"))` is `\"Symbol(x)\"` —\n * the single legal symbol stringification, which is why a template literal\n * (`${symbol}`) and `symbol + \"\"` throw where this call does not. That is what\n * makes the named refusal possible: `requireStrategy` receives `\"Symbol(x)\"`,\n * finds no such key, and reports the option by name.\n */\nfunction asKey<K extends keyof QueryParamsConfig>(\n field: K,\n bag: QueryParamsConfig,\n): QueryParamsConfig[K] | undefined {\n // ⚑ The READ happens HERE, inside the guarded region, and that placement is\n // the point. Reading the slot at the CALL SITE — `asKey(\"arrayFormat\",\n // queryParams.arrayFormat)` — invokes an accessor-backed bag's getter one\n // frame ABOVE this try/catch, so a `{ get arrayFormat() { throw } }` config\n // escapes `createRouter` as a raw `Error`, with no `cause` and no option\n // named, against the paragraph below. An accessor-backed config is the\n // ordinary lazy-config spelling, not an exotic one.\n //\n // ⚑ The container is not read before this point (#1832): core freezes only the\n // level it owns, so nothing asks a caller's bag for `constructor`, and a Proxy\n // whose trap throws on that slot reaches this try/catch like any other. Pinned\n // by the CONTROL cell in `query-strategy-formats-1796.test.ts`.\n let value: QueryParamsConfig[K] | undefined;\n\n try {\n value = bag[field];\n } catch (error) {\n throw new TypeError(\n `[router.constructor] Invalid \"queryParams.${field}\": reading it threw.`,\n { cause: error },\n );\n }\n\n // `== null` is the intent: BOTH nullish values mean \"the caller said nothing\",\n // and `makeOptions`' `??` downstream is what turns that into the default.\n if (value == null) {\n return undefined;\n }\n\n if (typeof value === \"string\") {\n return value;\n }\n\n // ⚠ `String(value)` runs the CALLER's code, and this snapshot moved that call\n // into `createRouter`. Uncaught, an application's own exception escapes the\n // constructor naming no option at all — strictly less useful than the named\n // refusal one line down, and a shape `options.test.ts` pins the opposite of\n // for the sibling `defaultRoute` slot. So the coercion answers for itself: a\n // value we cannot READ is a config fault about THIS field, and the original\n // error rides along as `cause` rather than being replaced by it.\n //\n // ⚠ The message does not name `toString`, and that is deliberate — TWO shapes\n // land here and only one of them threw. A `toString` that RETURNS a symbol\n // makes `String()` throw from the conversion, not from the callback; saying\n // \"its toString threw\" would be false for exactly the case a developer would\n // find hardest to see. `cause` carries the real mechanism.\n try {\n // The cast is the honest shape: the STATIC type says this slot is one of the\n // declared union members, and the runtime disagrees — that is the whole\n // reason the coercion exists. What comes back may name no strategy at all,\n // and `requireStrategy` is the one that decides, by the same key it would\n // have computed itself.\n return String(value) as QueryParamsConfig[K];\n } catch (error) {\n throw new TypeError(\n `[router.constructor] Invalid \"queryParams.${field}\": its value cannot be converted to a string.`,\n { cause: error },\n );\n }\n}\n\n/**\n * ⚑ Read ONCE, here, and hand the KEY downstream — the same treatment\n * `snapshotQueryParams` gives `queryParams`, the next property in the literal\n * below, and for the same reason (#1839).\n *\n * The declared type is a union of four literals, and that union is precisely\n * what cannot be trusted: the option reaches here from JavaScript consumers and\n * from configs assembled at runtime, which is the population `SegmentMatcher`'s\n * own `\"default\"` fallback exists for. Stored raw in\n * `RoutesStore.matcherOptions`, an object-valued encoding is left for the\n * matcher's constructor to coerce — so a `toString`- or\n * `Symbol.toPrimitive`-backed VALUE is read again on every matcher rebuild:\n * `add` / `remove` / `replace` / `clear` / `setRootPath`, and the `resetStore`\n * that `dispose()` goes through. (A getter\n * on the OPTIONS BAG was never affected — the constructor's rest-spread\n * materialises it once.)\n *\n * Coercing here moves that into construction, where application code is\n * expected and where a throw is loud and total. `cloneRouter` inherits the key\n * rather than re-reading the option (#1877), so the unit is one read per router\n * TREE.\n *\n * ⚠ NOT `asKey`: that helper is typed `keyof QueryParamsConfig`, takes a\n * `QueryParamsConfig` bag, and hardcodes `queryParams.${field}` into its\n * message, so reusing it would widen a guard four other call sites depend on.\n *\n * The table lookup and the `\"default\"` fallback stay in `SegmentMatcher`, which\n * already stores the key it tested. This only guarantees that what it tests is\n * plain data by the time it gets there.\n */\nfunction snapshotEncodingKey(\n value: unknown,\n): NonNullable<CreateMatcherOptions[\"urlParamsEncoding\"]> {\n // `== null` is the intent: both nullish spellings mean \"the caller said\n // nothing\", and `exactOptionalPropertyTypes` forbids answering `undefined`.\n // This arm is not cosmetic and it is pinned: without it the stored key would\n // read `\"null\"`, and that slot is published through\n // `@real-router/core/validation`.\n if (value == null) {\n return \"default\";\n }\n\n try {\n // Identity for a string, `ToString` for anything else. There is no\n // `typeof value === \"string\"` fast path in front of this: it would run once\n // per router constructor, it was never benchmarked, and `String(\"uri\")` is\n // already `\"uri\"` — an unmeasured branch that changes no answer is a branch\n // no mutation can pin. The matcher's table lookup rejects whatever comes out\n // and falls back to `\"default\"`, exactly as it did when it ran this coercion\n // itself; the lint rule reads the declared union, which is what this\n // distrusts.\n // eslint-disable-next-line @typescript-eslint/no-base-to-string -- see above\n return String(value) as NonNullable<\n CreateMatcherOptions[\"urlParamsEncoding\"]\n >;\n } catch (error) {\n throw new TypeError(\n `[router.constructor] Invalid \"urlParamsEncoding\": coercing it threw.`,\n { cause: error },\n );\n }\n}\n\n/**\n * A plain-data copy of the caller's `queryParams`, read once.\n *\n * ⚑ The four names are written out, and that is a hand enumeration of\n * `search-params`' `Options` — bound to it by the `search-params Options ↔\n * snapshotQueryParams' copy` relation in\n * `tests/functional/type-mirror-authority.test.ts`, which derives the key set\n * from the type and fails if a fifth field is added without reaching here. A\n * spread would not need the list, but would drop the two shapes the comment at\n * the call site names.\n *\n * ⚠ These reads WALK the prototype chain, deliberately, and they are not the\n * class #1798 closed one directory over. That rule is about a key whose NAME\n * comes from a route declaration read off the CALLER's data bag, where an\n * `Object.prototype` member makes an empty bag answer \"filled\". Here the four\n * names are literals written above, none of them is a member of\n * `Object.prototype`, and the chain walk is the FEATURE — it is what lets one\n * config be layered over another. Do not \"fix\" this to `Object.hasOwn`.\n */\nfunction snapshotQueryParams(\n queryParams: QueryParamsConfig | undefined,\n): QueryParamsConfig {\n // `!` rather than `=== undefined`: the STATIC type says the container is an\n // object or absent, and the runtime disagrees — `{ queryParams: null }` is\n // reachable from JavaScript and from a config assembled at runtime. Mirrors\n // `makeOptions`' own `!opts` guard, which is the collaborator this feeds.\n if (!queryParams) {\n return EMPTY_QUERY_PARAMS;\n }\n\n // ⚠ Into locals FIRST, and this is the whole point of the helper rather than a\n // style choice. `...(queryParams.x !== undefined && { x: queryParams.x })`\n // reads the property TWICE — once for the test, once for the value — which is\n // the exact TOCTOU this snapshot exists to collapse, merely moved out of\n // `makeOptions` and into here. Measured with a getter that answers differently\n // on its second call: the router ran on the SECOND value while the test that\n // admitted it saw the first.\n // ⚑ FROZEN, and for the reason `encode.ts` freezes its three defaults: this\n // object is reachable from outside core through `getInternals`\n // (`@real-router/core/validation`), and every matcher rebuild re-reads it. The\n // slot it replaced is sealed by nobody else — `OptionsNamespace` freezes only\n // the level it owns (#1832) — so a plain literal would be writable: a write took\n // effect on the next rebuild, and `Object.defineProperty` could re-install an\n // accessor in the very slot this snapshot exists to empty, restoring the defect\n // it fixes. Nothing in the repo writes it, so the freeze costs nothing and makes\n // read-only structural rather than conventional.\n const arrayFormat = asKey(\"arrayFormat\", queryParams);\n const booleanFormat = asKey(\"booleanFormat\", queryParams);\n const nullFormat = asKey(\"nullFormat\", queryParams);\n const numberFormat = asKey(\"numberFormat\", queryParams);\n\n return freeze({\n ...(arrayFormat !== undefined && { arrayFormat }),\n ...(booleanFormat !== undefined && { booleanFormat }),\n ...(nullFormat !== undefined && { nullFormat }),\n ...(numberFormat !== undefined && { numberFormat }),\n });\n}\n\n/**\n * Derives CreateMatcherOptions from router Options.\n * Maps core option names to matcher option names.\n */\nfunction deriveMatcherOptions<Dependencies extends DefaultDependencies>(\n options: Readonly<Options<Dependencies>>,\n): CreateMatcherOptions {\n // ⚑ The CONTAINER is frozen too, not only the snapshot inside it, and that is\n // the half a first pass missed. Freezing the snapshot stops a WRITE INTO it;\n // it does nothing about REPLACING the slot that holds it — and the slot is\n // reachable, through the very surface cited as the reason to freeze at all:\n // `getInternals(router).routeGetStore().matcherOptions` on the published\n // `@real-router/core/validation` subpath. Measured: swapping `queryParams`\n // there for `{ arrayFormat: \"bogusTypo\" }` made `add`, `setRootPath` and\n // `dispose()` throw, i.e. it restored the defect verbatim. Frozen, the write\n // fails at the write site instead.\n return freeze({\n strictTrailingSlash: options.trailingSlash === \"strict\",\n caseSensitive: options.caseSensitive,\n strictQueryParams: options.queryParamsMode === \"strict\",\n urlParamsEncoding: snapshotEncodingKey(options.urlParamsEncoding),\n // SNAPSHOT, not the caller's reference. `queryParams` is supported input and\n // may be accessor- or Proxy-backed, and this object is stored once as\n // `RoutesStore.matcherOptions` and re-read by `createMatcher` on EVERY matcher\n // rebuild — `add` / `remove` / `replace` / `setRootPath`, and `resetStore`,\n // which `dispose()` goes through. A live getter there is application code\n // running inside a teardown that core documents as holding together \"only\n // because no user code runs in them\" (CLAUDE.md, INVARIANTS Route Management\n // #17/#18): a getter that answered differently on the rebuild threw out of\n // `dispose()` AFTER `sendDispose()`, so `isDisposed()` was already true, the\n // idempotency early-return swallowed every retry, and everything BELOW the\n // throw never ran — `markDisposed`, the lifecycle teardown and the dependency\n // reset — so the router leaked every DI reference, per request, in an SSR\n // scope. ⚠ The event-bus `clearAll` is ABOVE it and does run; and what such a\n // router still answers is `buildPath` / `canNavigateTo` / `has`, not\n // `navigate` (the FSM is already down, so that one refuses — with the wrong\n // reason, `ROUTER_NOT_STARTED`). Measured on the pre-fix build against a\n // clean-dispose control.\n //\n // The snapshot reads each field exactly ONCE, during construction, where\n // application code is expected; every later read sees plain data. That also\n // collapses the TOCTOU inside `makeOptions`, which tests a field and then\n // re-reads it for the value. ⚠ Not \"each field twice\" — its fast path is a\n // `&&` chain, so it stops at the first DEFINED field: for the bag a router\n // actually passes, `arrayFormat` is read twice and the other three once.\n //\n // ⚑ ONCE, by the snapshot, and by nothing else: the freeze stops at the level\n // core owns (#1832), so it neither reads a value here nor asks for a\n // descriptor. Measured on a Proxy bag — four named reads, zero descriptor\n // traps — in `query-strategy-formats-1796.test.ts`. The count AFTER\n // construction is ZERO.\n // ⚠ Read by NAME, not `{ ...queryParams }`, and the difference is measured\n // rather than stylistic: a spread copies own ENUMERABLE keys, so an inherited\n // format (`Object.create({ arrayFormat: \"brackets\" })` — layering one config\n // over another) or an own non-enumerable one was silently dropped and the\n // router fell back to the defaults. Both worked before the snapshot, because\n // a plain `opts.arrayFormat` walks the prototype chain. Reading by name keeps\n // that lookup and still yields plain own data.\n //\n // The conditional spread is `exactOptionalPropertyTypes`: an optional\n // property may be absent but not present-and-`undefined`, and `makeOptions`\n // treats the two identically anyway (its fast path tests `=== undefined`).\n queryParams: snapshotQueryParams(options.queryParams),\n });\n}\n"],"mappings":"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"}
|