@real-router/core 0.84.0 → 0.85.1
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-CUtHMhRV.js → Router-D6629wsY.js} +2 -2
- package/dist/cjs/{Router-CUtHMhRV.js.map → Router-D6629wsY.js.map} +1 -1
- 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/buildParamMeta-0m11OIJS.js.map +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/internals.d.ts +7 -0
- package/dist/cjs/internals.d.ts.map +1 -1
- package/dist/esm/Router-hyjjNX3c.mjs +2 -0
- package/dist/esm/{Router-sWkNlsWW.mjs.map → Router-hyjjNX3c.mjs.map} +1 -1
- 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/buildParamMeta-D8td47fL.mjs.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/internals.d.mts +7 -0
- package/dist/esm/internals.d.mts.map +1 -1
- package/package.json +2 -2
- package/dist/cjs/Router.js +0 -2
- package/dist/cjs/Router.js.map +0 -1
- package/dist/cjs/RouterError.js +0 -2
- package/dist/cjs/RouterError.js.map +0 -1
- package/dist/cjs/api/cloneRouter.js +0 -2
- package/dist/cjs/api/cloneRouter.js.map +0 -1
- package/dist/cjs/api/getDependenciesApi.js +0 -2
- package/dist/cjs/api/getDependenciesApi.js.map +0 -1
- package/dist/cjs/api/getLifecycleApi.js +0 -2
- package/dist/cjs/api/getLifecycleApi.js.map +0 -1
- package/dist/cjs/api/getPluginApi.js +0 -2
- package/dist/cjs/api/getPluginApi.js.map +0 -1
- package/dist/cjs/api/getRoutesApi.js +0 -2
- package/dist/cjs/api/getRoutesApi.js.map +0 -1
- package/dist/cjs/api/helpers.js +0 -2
- package/dist/cjs/api/helpers.js.map +0 -1
- package/dist/cjs/channels/defaults.js +0 -2
- package/dist/cjs/channels/defaults.js.map +0 -1
- package/dist/cjs/channels/guard.js +0 -2
- package/dist/cjs/channels/guard.js.map +0 -1
- package/dist/cjs/channels/modeGate.js +0 -2
- package/dist/cjs/channels/modeGate.js.map +0 -1
- package/dist/cjs/constants.js +0 -2
- package/dist/cjs/constants.js.map +0 -1
- package/dist/cjs/createRouter.js +0 -2
- package/dist/cjs/createRouter.js.map +0 -1
- package/dist/cjs/engine/builder/buildTree.js +0 -2
- package/dist/cjs/engine/builder/buildTree.js.map +0 -1
- package/dist/cjs/engine/builder/computeCaches.js +0 -2
- package/dist/cjs/engine/builder/computeCaches.js.map +0 -1
- package/dist/cjs/engine/builder/createRouteTree.js +0 -2
- package/dist/cjs/engine/builder/createRouteTree.js.map +0 -1
- package/dist/cjs/engine/createMatcher.js +0 -2
- package/dist/cjs/engine/createMatcher.js.map +0 -1
- package/dist/cjs/engine/operations/routeTreeToDefinitions.js +0 -2
- package/dist/cjs/engine/operations/routeTreeToDefinitions.js.map +0 -1
- package/dist/cjs/engine/path-matcher/SegmentMatcher.js +0 -2
- package/dist/cjs/engine/path-matcher/SegmentMatcher.js.map +0 -1
- package/dist/cjs/engine/path-matcher/buildParamMeta.js +0 -2
- package/dist/cjs/engine/path-matcher/buildParamMeta.js.map +0 -1
- package/dist/cjs/engine/path-matcher/encoding.js +0 -2
- package/dist/cjs/engine/path-matcher/encoding.js.map +0 -1
- package/dist/cjs/engine/path-matcher/parseSegment.js +0 -2
- package/dist/cjs/engine/path-matcher/parseSegment.js.map +0 -1
- package/dist/cjs/engine/path-matcher/pathUtils.js +0 -2
- package/dist/cjs/engine/path-matcher/pathUtils.js.map +0 -1
- package/dist/cjs/engine/path-matcher/percentEncoding.js +0 -2
- package/dist/cjs/engine/path-matcher/percentEncoding.js.map +0 -1
- package/dist/cjs/engine/path-matcher/registration/buildParts.js +0 -2
- package/dist/cjs/engine/path-matcher/registration/buildParts.js.map +0 -1
- package/dist/cjs/engine/path-matcher/registration/context.js +0 -2
- package/dist/cjs/engine/path-matcher/registration/context.js.map +0 -1
- package/dist/cjs/engine/path-matcher/registration/errors.js +0 -2
- package/dist/cjs/engine/path-matcher/registration/errors.js.map +0 -1
- package/dist/cjs/engine/path-matcher/registration/index.js +0 -2
- package/dist/cjs/engine/path-matcher/registration/index.js.map +0 -1
- package/dist/cjs/engine/path-matcher/registration/trie.js +0 -2
- package/dist/cjs/engine/path-matcher/registration/trie.js.map +0 -1
- package/dist/cjs/engine/path-matcher/registration/trieNodes.js +0 -2
- package/dist/cjs/engine/path-matcher/registration/trieNodes.js.map +0 -1
- package/dist/cjs/engine/search-params/decode.js +0 -2
- package/dist/cjs/engine/search-params/decode.js.map +0 -1
- package/dist/cjs/engine/search-params/encode.js +0 -2
- package/dist/cjs/engine/search-params/encode.js.map +0 -1
- package/dist/cjs/engine/search-params/searchParams.js +0 -2
- package/dist/cjs/engine/search-params/searchParams.js.map +0 -1
- package/dist/cjs/engine/search-params/strategies/array.js +0 -2
- package/dist/cjs/engine/search-params/strategies/array.js.map +0 -1
- package/dist/cjs/engine/search-params/strategies/boolean.js +0 -2
- package/dist/cjs/engine/search-params/strategies/boolean.js.map +0 -1
- package/dist/cjs/engine/search-params/strategies/index.js +0 -2
- package/dist/cjs/engine/search-params/strategies/index.js.map +0 -1
- package/dist/cjs/engine/search-params/strategies/null.js +0 -2
- package/dist/cjs/engine/search-params/strategies/null.js.map +0 -1
- package/dist/cjs/engine/search-params/strategies/number.js +0 -2
- package/dist/cjs/engine/search-params/strategies/number.js.map +0 -1
- package/dist/cjs/engine/search-params/utils.js +0 -2
- package/dist/cjs/engine/search-params/utils.js.map +0 -1
- package/dist/cjs/engine/validation/route-batch.js +0 -2
- package/dist/cjs/engine/validation/route-batch.js.map +0 -1
- package/dist/cjs/engine/validation/routes.js +0 -2
- package/dist/cjs/engine/validation/routes.js.map +0 -1
- package/dist/cjs/getNavigator.js +0 -2
- package/dist/cjs/getNavigator.js.map +0 -1
- package/dist/cjs/guards.js +0 -2
- package/dist/cjs/guards.js.map +0 -1
- package/dist/cjs/helpers.js +0 -2
- package/dist/cjs/helpers.js.map +0 -1
- package/dist/cjs/internals.js +0 -2
- package/dist/cjs/internals.js.map +0 -1
- package/dist/cjs/limits.js +0 -2
- package/dist/cjs/limits.js.map +0 -1
- package/dist/cjs/namespaces/DependenciesNamespace/dependenciesStore.js +0 -2
- package/dist/cjs/namespaces/DependenciesNamespace/dependenciesStore.js.map +0 -1
- package/dist/cjs/namespaces/EventBusNamespace/EventBusNamespace.js +0 -2
- package/dist/cjs/namespaces/EventBusNamespace/EventBusNamespace.js.map +0 -1
- package/dist/cjs/namespaces/NavigationNamespace/InFlightNavigation.js +0 -2
- package/dist/cjs/namespaces/NavigationNamespace/InFlightNavigation.js.map +0 -1
- package/dist/cjs/namespaces/NavigationNamespace/NavigationNamespace.js +0 -2
- package/dist/cjs/namespaces/NavigationNamespace/NavigationNamespace.js.map +0 -1
- package/dist/cjs/namespaces/NavigationNamespace/constants.js +0 -2
- package/dist/cjs/namespaces/NavigationNamespace/constants.js.map +0 -1
- package/dist/cjs/namespaces/NavigationNamespace/transition/completeTransition.js +0 -2
- package/dist/cjs/namespaces/NavigationNamespace/transition/completeTransition.js.map +0 -1
- package/dist/cjs/namespaces/NavigationNamespace/transition/errorHandling.js +0 -2
- package/dist/cjs/namespaces/NavigationNamespace/transition/errorHandling.js.map +0 -1
- package/dist/cjs/namespaces/NavigationNamespace/transition/executeNavigation.js +0 -2
- package/dist/cjs/namespaces/NavigationNamespace/transition/executeNavigation.js.map +0 -1
- package/dist/cjs/namespaces/NavigationNamespace/transition/guardPhase.js +0 -2
- package/dist/cjs/namespaces/NavigationNamespace/transition/guardPhase.js.map +0 -1
- package/dist/cjs/namespaces/NavigationNamespace/transition/navigateToNotFound.js +0 -2
- package/dist/cjs/namespaces/NavigationNamespace/transition/navigateToNotFound.js.map +0 -1
- package/dist/cjs/namespaces/OptionsNamespace/OptionsNamespace.js +0 -2
- package/dist/cjs/namespaces/OptionsNamespace/OptionsNamespace.js.map +0 -1
- package/dist/cjs/namespaces/OptionsNamespace/constants.js +0 -2
- package/dist/cjs/namespaces/OptionsNamespace/constants.js.map +0 -1
- package/dist/cjs/namespaces/OptionsNamespace/helpers.js +0 -2
- package/dist/cjs/namespaces/OptionsNamespace/helpers.js.map +0 -1
- package/dist/cjs/namespaces/OptionsNamespace/validators.js +0 -2
- package/dist/cjs/namespaces/OptionsNamespace/validators.js.map +0 -1
- package/dist/cjs/namespaces/PluginsNamespace/PluginsNamespace.js +0 -2
- package/dist/cjs/namespaces/PluginsNamespace/PluginsNamespace.js.map +0 -1
- package/dist/cjs/namespaces/PluginsNamespace/constants.js +0 -2
- package/dist/cjs/namespaces/PluginsNamespace/constants.js.map +0 -1
- package/dist/cjs/namespaces/PluginsNamespace/validators.js +0 -2
- package/dist/cjs/namespaces/PluginsNamespace/validators.js.map +0 -1
- package/dist/cjs/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.js +0 -2
- package/dist/cjs/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.js.map +0 -1
- package/dist/cjs/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.js +0 -2
- package/dist/cjs/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.js.map +0 -1
- package/dist/cjs/namespaces/RouterLifecycleNamespace/constants.js +0 -2
- package/dist/cjs/namespaces/RouterLifecycleNamespace/constants.js.map +0 -1
- package/dist/cjs/namespaces/RoutesNamespace/RoutesNamespace.js +0 -2
- package/dist/cjs/namespaces/RoutesNamespace/RoutesNamespace.js.map +0 -1
- package/dist/cjs/namespaces/RoutesNamespace/constants.js +0 -2
- package/dist/cjs/namespaces/RoutesNamespace/constants.js.map +0 -1
- package/dist/cjs/namespaces/RoutesNamespace/forwardChain.js +0 -2
- package/dist/cjs/namespaces/RoutesNamespace/forwardChain.js.map +0 -1
- package/dist/cjs/namespaces/RoutesNamespace/helpers.js +0 -2
- package/dist/cjs/namespaces/RoutesNamespace/helpers.js.map +0 -1
- package/dist/cjs/namespaces/RoutesNamespace/routeGuards.js +0 -2
- package/dist/cjs/namespaces/RoutesNamespace/routeGuards.js.map +0 -1
- package/dist/cjs/namespaces/RoutesNamespace/routesStore.js +0 -2
- package/dist/cjs/namespaces/RoutesNamespace/routesStore.js.map +0 -1
- package/dist/cjs/namespaces/StateNamespace/StateNamespace.js +0 -2
- package/dist/cjs/namespaces/StateNamespace/StateNamespace.js.map +0 -1
- package/dist/cjs/pipeline/buildURL.js +0 -2
- package/dist/cjs/pipeline/buildURL.js.map +0 -1
- package/dist/cjs/pipeline/canonicalize.js +0 -2
- package/dist/cjs/pipeline/canonicalize.js.map +0 -1
- package/dist/cjs/pipeline/materialize.js +0 -2
- package/dist/cjs/pipeline/materialize.js.map +0 -1
- package/dist/cjs/routerFSM.js +0 -2
- package/dist/cjs/routerFSM.js.map +0 -1
- package/dist/cjs/transitionPath.js +0 -2
- package/dist/cjs/transitionPath.js.map +0 -1
- package/dist/cjs/utils/event-emitter/EventEmitter.js +0 -2
- package/dist/cjs/utils/event-emitter/EventEmitter.js.map +0 -1
- package/dist/cjs/utils/fsm/fsm.js +0 -2
- package/dist/cjs/utils/fsm/fsm.js.map +0 -1
- package/dist/cjs/utils/logger/RouterLogger.js +0 -2
- package/dist/cjs/utils/logger/RouterLogger.js.map +0 -1
- package/dist/cjs/utils/logger/constants.js +0 -2
- package/dist/cjs/utils/logger/constants.js.map +0 -1
- package/dist/cjs/wiring/wireNamespaces.js +0 -2
- package/dist/cjs/wiring/wireNamespaces.js.map +0 -1
- package/dist/esm/Router-sWkNlsWW.mjs +0 -2
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"routesStore.js","names":["createRouteTree","createMatcher","createEmptyConfig","anyForwardConfigured","resolveForwardChain","STANDARD_ROUTE_KEYS","sanitizeRoute","queryParamsOf","routeTreeToDefinitions"],"sources":["../../../../src/namespaces/RoutesNamespace/routesStore.ts"],"sourcesContent":["// 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 createMatcher,\n createRouteTree,\n routeTreeToDefinitions,\n} from \"../../engine\";\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// =============================================================================\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, 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, string[]>;\n resolvedForwardMap: Record<string, string>;\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 across every route-CRUD path by `isActiveRoute-forward-arm` in\n * `tests/functional/routes/is-active-route.test.ts`.\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, Object.create(null) as Record<string, string>);\n store.routeCustomFields = Object.create(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 */\nfunction 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 = Object.create(null) as Record<string, string>;\n\n for (const fromRoute of Object.keys(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 = Object.fromEntries(\n Object.entries(route).filter(([key]) => !STANDARD_ROUTE_KEYS.has(key)),\n );\n\n if (Object.keys(customFields).length > 0) {\n routeCustomFields[fullName] = 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 if (route.defaultParams) {\n config.defaultParams[fullName] = route.defaultParams;\n }\n\n if (route.defaultSearch) {\n config.defaultSearch[fullName] = route.defaultSearch;\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 * validation-extraction (`d1ebff80`) 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: string,\n methodName: string,\n): void {\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 * 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\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 Object.create(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: Object.create(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.** It used to run here, one line\n * before the swap, which is early enough for `add` and too late for `replace`:\n * `replace` erases the old definition guards BEFORE calling this, so a batch\n * this check refused left the tree intact and the guards gone — a previously\n * guarded route freely activatable. That is the #1193 fail-open shape verbatim,\n * which is why the guard COMPILE was hoisted into the callers; the channel\n * check now sits beside it, 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 */\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,\n defaultParams,\n defaultSearch,\n decodeParams,\n encodeParams,\n canActivate,\n canDeactivate,\n } = updates;\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 (О-7).\n if (nextCustomFields !== undefined) {\n if (Object.keys(nextCustomFields).length > 0) {\n store.routeCustomFields[name] = 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 Object.create(null) as RouteConfig[\"forwardMap\"],\n config.forwardMap,\n );\n const forwardFnMap = Object.assign(\n Object.create(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 // eslint-disable-next-line unicorn/prefer-object-iterable-methods -- see above\n for (const key of Object.keys(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 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 assertNoInternalNamesInBatch(routes, \"addRoute\");\n assertNoDuplicateNamesInBatch(routes, \"\", \"addRoute\");\n\n const artifacts = buildReplaceArtifacts(routes, \"\", 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 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 return store;\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("../../constants.js"),t=require("../../helpers.js"),n=require("../../pipeline/canonicalize.js"),r=require("../../pipeline/buildURL.js"),i=require("../../pipeline/materialize.js");var a=class{#e=void 0;#t=void 0;#n;get(){return this.#e}set(e){this.#t=this.#e,this.#e=e?t.freezeStateShell(e):void 0}getPrevious(){return this.#t}reset(){this.#e=void 0,this.#t=void 0}setDependencies(e){this.#n=e}makeState(t,a,o,s){let c=this.#n.port(),l=n.canonicalize(c,t,a??e.EMPTY_PARAMS,o,{resolveForward:!1});return i.materialize(l,{path:s??r.buildURL(l,c)})}areStatesEqual(e,n,r=!0){if(!e||!n)return!!e==!!n;if(e.name!==n.name)return!1;if(r){let r=this.#n.getUrlParams(e.name);for(let i of r)if(!t.areParamValuesEqual(e.params[i],n.params[i]))return!1;return!0}return o(e.params,n.params)&&o(e.search,n.search)}};function o(e,n){let r=Object.keys(e);if(r.length!==Object.keys(n).length)return!1;for(let i of r)if(!(i in n)||!t.areParamValuesEqual(e[i],n[i]))return!1;return!0}exports.StateNamespace=a;
|
|
2
|
-
//# sourceMappingURL=StateNamespace.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"StateNamespace.js","names":["#frozenState","#previousState","freezeStateShell","#deps","canonicalize","EMPTY_PARAMS","materialize","buildURL","areParamValuesEqual"],"sources":["../../../../src/namespaces/StateNamespace/StateNamespace.ts"],"sourcesContent":["// packages/core/src/namespaces/StateNamespace/StateNamespace.ts\n\nimport { EMPTY_PARAMS } from \"../../constants\";\nimport { areParamValuesEqual, freezeStateShell } from \"../../helpers\";\nimport { buildURL, canonicalize, materialize } from \"../../pipeline\";\n\nimport type { StateNamespaceDependencies } from \"./types\";\nimport type { Params, SearchParams, State } from \"../../types\";\n\n/**\n * Independent namespace for managing router state storage and creation.\n *\n * Static methods handle validation (called by facade).\n * Instance methods handle state storage, freezing, and creation.\n */\nexport class StateNamespace {\n /**\n * Cached frozen state - avoids structuredClone on every getState() call.\n */\n #frozenState: State | undefined = undefined;\n\n /**\n * Previous state before the last setState call.\n */\n #previousState: State | undefined = undefined;\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.#frozenState as State<P> | undefined; // NOSONAR -- generic narrowing needed for public API\n }\n\n /**\n * Sets the current router state.\n *\n * The state is deeply frozen before storage to ensure immutability.\n * The previous state is preserved and accessible via `getPrevious()`.\n *\n * @param state - Already validated by facade, or undefined to clear\n */\n set(state: State | undefined): void {\n // Preserve current state as previous before updating\n this.#previousState = this.#frozenState;\n\n // If state is already frozen (from makeState()), use it directly.\n // For external states, freeze in place without cloning.\n this.#frozenState = state ? freezeStateShell(state) : undefined;\n }\n\n /**\n * Returns the previous router state (before the last navigation).\n */\n getPrevious(): State | undefined {\n return this.#previousState;\n }\n\n reset(): void {\n this.#frozenState = undefined;\n this.#previousState = undefined;\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, even when\n * `skipFreeze=true` is passed to defer the outer `Object.freeze(state)` call.\n * This keeps params-freezing invariants independent of transition-pipeline\n * mutation (e.g. `completeTransition` attaching `state.transition`).\n *\n * **The LITERAL form of the pipeline** (nav-pipeline Phase 4). This method\n * used to carry its own copy of stage ③ (merge each channel's route default\n * UNDER the caller's value) and of the mode gate — a second, parallel\n * canonicalisation living outside `src/pipeline`. Two terminals for one rule\n * is not a style problem: #1584's existence precondition landed on the\n * pipeline's terminal and NOT on this one, because it was found by sweeping\n * `canonicalize`'s PORT consumers, and this method read its own dependency\n * bag. It now IS `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, from the ONE implementation (`canonicalize`) —\n // this method no longer carries its own. `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. `buildURL` prints through\n // `port.buildPath` — the interceptable `ctx.buildPath` this method already\n // used — so the interceptor zone is unchanged, and the URL 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 // ⚠ No `skipFreeze` arm: the parameter died when Phase 2 moved the two\n // callers that used it (`canNavigateTo`, `isActiveRoute`) onto\n // `materialize({ skipFreeze: true })` directly, and the old body hid the\n // death because it forwarded `undefined` into a slot that needs no branch.\n // The public `PluginApi.makeState` type has four parameters and both call\n // sites pass four; unfreezing a state is the transition 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 return materialize<P, S>(canonical, {\n path: path ?? buildURL(canonical, port),\n });\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 if (state1.name !== 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 const urlParams = this.#deps.getUrlParams(state1.name);\n\n for (const urlParam of urlParams) {\n if (\n !areParamValuesEqual(state1.params[urlParam], state2.params[urlParam])\n ) {\n return false;\n }\n }\n\n return true;\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\n/**\n * Shallow key/value equality of two param-like records (path params or query),\n * using {@link areParamValuesEqual} per key so array values compare by content.\n */\nfunction recordsShallowEqual(\n left: Readonly<Record<string, unknown>>,\n right: Readonly<Record<string, unknown>>,\n): boolean {\n const leftKeys = Object.keys(left);\n\n if (leftKeys.length !== Object.keys(right).length) {\n return false;\n }\n\n for (const key of leftKeys) {\n if (!(key in right) || !areParamValuesEqual(left[key], right[key])) {\n return false;\n }\n }\n\n return true;\n}\n"],"mappings":"mMAeA,IAAa,EAAb,KAA4B,CAI1B,GAAkC,IAAA,GAKlC,GAAoC,IAAA,GAKpC,GAYA,KAAuD,CACrD,OAAO,KAAKA,EACd,CAUA,IAAI,EAAgC,CAElC,KAAKC,GAAiB,KAAKD,GAI3B,KAAKA,GAAe,EAAQE,EAAAA,iBAAiB,CAAK,EAAI,IAAA,EACxD,CAKA,aAAiC,CAC/B,OAAO,KAAKD,EACd,CAEA,OAAc,CACZ,KAAKD,GAAe,IAAA,GACpB,KAAKC,GAAiB,IAAA,EACxB,CAUA,gBAAgB,EAAwC,CACtD,KAAKE,GAAQ,CACf,CAuCA,UACE,EACA,EACA,EACA,EACa,CAKb,IAAM,EAAO,KAAKA,GAAM,KAAK,EACvB,EAAYC,EAAAA,aAAa,EAAM,EAAM,GAAUC,EAAAA,aAAc,EAAQ,CACzE,eAAgB,EAClB,CAAC,EAkBD,OAAOC,EAAAA,YAAkB,EAAW,CAClC,KAAM,GAAQC,EAAAA,SAAS,EAAW,CAAI,CACxC,CAAC,CACH,CAUA,eACE,EACA,EACA,EAAoB,GACX,CACT,GAAI,CAAC,GAAU,CAAC,EACd,MAAO,CAAC,CAAC,GAAW,CAAC,CAAC,EAGxB,GAAI,EAAO,OAAS,EAAO,KACzB,MAAO,GAGT,GAAI,EAAmB,CAGrB,IAAM,EAAY,KAAKJ,GAAM,aAAa,EAAO,IAAI,EAErD,IAAK,IAAM,KAAY,EACrB,GACE,CAACK,EAAAA,oBAAoB,EAAO,OAAO,GAAW,EAAO,OAAO,EAAS,EAErE,MAAO,GAIX,MAAO,EACT,CAKA,OACE,EAAoB,EAAO,OAAQ,EAAO,MAAM,GAChD,EAAoB,EAAO,OAAQ,EAAO,MAAM,CAEpD,CACF,EAMA,SAAS,EACP,EACA,EACS,CACT,IAAM,EAAW,OAAO,KAAK,CAAI,EAEjC,GAAI,EAAS,SAAW,OAAO,KAAK,CAAK,CAAC,CAAC,OACzC,MAAO,GAGT,IAAK,IAAM,KAAO,EAChB,GAAI,EAAE,KAAO,IAAU,CAACA,EAAAA,oBAAoB,EAAK,GAAM,EAAM,EAAI,EAC/D,MAAO,GAIX,MAAO,EACT"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"buildURL.js","names":[],"sources":["../../../src/pipeline/buildURL.ts"],"sourcesContent":["// 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 * specifically through the interceptable `ctx.buildPath`, which is a permanent\n * decision and not a milestone-1 stopgap: reaching for the engine's\n * `matcher.buildPath` would silently stop running `persistent-params`'\n * `buildPath` interceptor on the navigate path (see {@link RouteResolver}).\n */\nexport function buildURL(canonical: Canonical, port: RouteResolver): string {\n return port.buildPath(canonical.name, canonical.path, canonical.query);\n}\n"],"mappings":"AAiBA,SAAgB,EAAS,EAAsB,EAA6B,CAC1E,OAAO,EAAK,UAAU,EAAU,KAAM,EAAU,KAAM,EAAU,KAAK,CACvE"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("../channels/defaults.js"),t=require("../constants.js"),n=require("../channels/modeGate.js"),r=require("../helpers.js");function i(e,t,n,r,i){let a=e.pathNames(t);if(a!==void 0)for(let e of Object.keys(n))!r.includes(e)&&!a.includes(e)&&i(t,e)}function a(a,o,s,c,l){let u=l?.resolveForward===!1?{name:o,params:s,search:c}:a.resolveForward(o,s,c),d=u.name,f=r.normalizeParams(u.params),p=l?.diagnoseUndeclared===!0?a.reportUndeclaredParamKey:void 0;p&&i(a,d,f,a.queryNames(d),p);let m=a.defaultParams(d),h=a.defaultSearch(d);if((u.search===void 0||u.search===t.EMPTY_SEARCH)&&m===void 0&&h===void 0)return{name:d,path:f,query:t.EMPTY_SEARCH};let g=a.queryNames(d),_=a.reportDroppedQueryKey,v=r.mergeWithDefault(l?.resolveForward===!1?e.withholdFilledSlots(h,f,g):h,u.search,t.EMPTY_SEARCH);return{name:d,path:r.mergeWithDefault(m,f,t.EMPTY_PARAMS,!0),query:a.admitsUndeclaredQuery()?v:n.admittedSearch(v,g,e=>{_!==void 0&&a.pathNames(d)!==void 0&&_(d,e)})}}exports.canonicalize=a;
|
|
2
|
-
//# sourceMappingURL=canonicalize.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"canonicalize.js","names":["normalizeParams","EMPTY_SEARCH","mergeWithDefault","withholdFilledSlots","EMPTY_PARAMS","admittedSearch"],"sources":["../../../src/pipeline/canonicalize.ts"],"sourcesContent":["// packages/core/src/pipeline/canonicalize.ts\n\nimport { admittedSearch, withholdFilledSlots } from \"../channels\";\nimport { EMPTY_PARAMS, EMPTY_SEARCH } from \"../constants\";\nimport { mergeWithDefault, normalizeParams } from \"../helpers\";\n\nimport type { RouteResolver } from \"./port\";\nimport type { Canonical } from \"./types\";\nimport type { Params, SearchParams } from \"../types\";\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 (`ba0f6b18b`), so the resolving form\n * REFUSES a mis-channelled bag while the literal form simply does not look.\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 */\nfunction 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 Object.keys(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 (`mergeWithDefault`,\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 * Channels are frozen here, at merge time — NOT in `materialize`. The two\n * freezes are different things: `materialize`'s `skipFreeze` governs the state\n * object (the navigate path defers it so `completeTransition` can attach\n * `transition`), while `params` / `search` must be immutable the moment a guard\n * can see them. `mergeWithDefault` also copies before freezing, so the caller's\n * own bag is never frozen out from under it.\n *\n * ⚠ The option bags at the call sites are INLINE LITERALS on purpose (#1589).\n * Hoisting them to shared frozen module constants was tried and measured worse:\n * `buildPath` and `isActiveRoute` slowed 6.6–10.5 % while sites that pass no\n * options moved 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 const forwarded =\n opts?.resolveForward === false\n ? { name, params, search }\n : port.resolveForward(name, 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 = normalizeParams(forwarded.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; `ba0f6b18b` 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 used to be what made a config the router itself had\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 // ⚠ The channels are still FROZEN here (canonicalize invariant #4): `pathBag`\n // is `normalizeParams`' own fresh object, so it is frozen in place, and\n // `EMPTY_SEARCH` is the shared frozen singleton.\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, and the two call sites that\n // used to mint one now 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 `?`. That term WAS in the condition until #1589 — and it was redundant\n // against fact 1, because an empty bag has nothing to drop however many names\n // are declared. Established, not argued: the term survives all 3808 tests, and\n // a 33-probe × 3-mode matrix over a `?`-declaring route with no defaults is\n // byte-identical without it. Dropping it 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 measured +6.5 % there. Both\n // were built and 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 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 // (The pipeline's second one is `materialize`'s options bag, at the call site:\n // two object literals per navigation over the pre-pipeline form.)\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. (It used to be wired as a plain closure — always truthy — so\n // the check read as taken and bare core paid that lookup with no sink behind\n // it; both sinks report their absence honestly now.)\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 const query = mergeWithDefault(queryDefaults, forwarded.search, EMPTY_SEARCH);\n\n return {\n name: resolvedName,\n // `valueIsOwned` (#1589): `pathBag` is `normalizeParams`' own fresh object —\n // never its input — so the merge freezes it in place instead of copying a bag\n // that was already copied one line above. Only the PATH channel may say this;\n // `forwarded.search` above comes from the caller or the seam.\n path: mergeWithDefault(defaultPath, pathBag, EMPTY_PARAMS, true),\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 one and only cast to the brand in the codebase — reviewed once, here.\n } as Canonical;\n}\n"],"mappings":"wIAoEA,SAAS,EACP,EACA,EACA,EACA,EACA,EACM,CACN,IAAM,EAAe,EAAK,UAAU,CAAY,EAE5C,OAAiB,IAAA,GAIrB,IAAK,IAAM,KAAO,OAAO,KAAK,CAAO,EAC/B,CAAC,EAAc,SAAS,CAAG,GAAK,CAAC,EAAa,SAAS,CAAG,GAC5D,EAAO,EAAc,CAAG,CAG9B,CAgCA,SAAgB,EACd,EACA,EACA,EACA,EACA,EACW,CAIX,IAAM,EACJ,GAAM,iBAAmB,GACrB,CAAE,OAAM,SAAQ,QAAO,EACvB,EAAK,eAAe,EAAM,EAAQ,CAAM,EACxC,EAAe,EAAU,KAKzB,EAAUA,EAAAA,gBAAgB,EAAU,MAAM,EAmB1C,EACJ,GAAM,qBAAuB,GACzB,EAAK,yBACL,IAAA,GAEF,GAMF,EACE,EACA,EACA,EACA,EAAK,WAAW,CAAY,EAC5B,CACF,EA6DF,IAAM,EAAc,EAAK,cAAc,CAAY,EAC7C,EAAe,EAAK,cAAc,CAAY,EAEpD,IACG,EAAU,SAAW,IAAA,IAAa,EAAU,SAAWC,EAAAA,eACxD,IAAgB,IAAA,IAChB,IAAiB,IAAA,GAWjB,MAAO,CALL,KAAM,EACN,KAAM,EACN,MAAOA,EAAAA,YAGK,EAUhB,IAAM,EAAgB,EAAK,WAAW,CAAY,EAoC5C,EAAW,EAAK,sBAOhB,EAAQC,EAAAA,iBAJZ,GAAM,iBAAmB,GACrBC,EAAAA,oBAAoB,EAAc,EAAS,CAAa,EACxD,EAEwC,EAAU,OAAQF,EAAAA,YAAY,EAE5E,MAAO,CACL,KAAM,EAKN,KAAMC,EAAAA,iBAAiB,EAAa,EAASE,EAAAA,aAAc,EAAI,EAM/D,MAAO,EAAK,sBAAsB,EAC9B,EACAC,EAAAA,eAAe,EAAuB,EAAgB,GAAQ,CAW1D,IAAa,IAAA,IACb,EAAK,UAAU,CAAY,IAAM,IAAA,IAEjC,EAAS,EAAc,CAAG,CAE9B,CAAC,CAEP,CACF"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("../constants.js"),t=require("../helpers.js");function n(n,r){let i={name:n.name,params:n.path,search:n.query,path:r.path,context:{},...!r.skipFreeze&&{transition:e.DEFAULT_TRANSITION}};return Object.freeze(i.params),r.skipFreeze?i:t.freezeStateShell(i)}exports.materialize=n;
|
|
2
|
-
//# sourceMappingURL=materialize.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"materialize.js","names":["DEFAULT_TRANSITION","freezeStateShell"],"sources":["../../../src/pipeline/materialize.ts"],"sourcesContent":["// 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\nexport interface MaterializeOptions {\n /**\n * The already-built URL (stage ⑤a). Required: the entry points that could\n * want a lazily-built path (`canNavigateTo`, `isActiveRoute`) settled the\n * question in Phase 2 by calling `buildURL` themselves, so `materialize`\n * never grew the port argument the milestone-1 docs left open.\n */\n path: string;\n /**\n * Defer `Object.freeze` of the STATE OBJECT — the navigate path passes `true`\n * so `completeTransition` can attach `transition`. It does NOT affect the\n * channels: `params` / `search` are frozen at merge time in `canonicalize`.\n */\n skipFreeze?: boolean;\n}\n\n/**\n * Stage ⑤b — the State of a canonical intent, and THE shape of a router State.\n * Accepts nothing but a {@link Canonical}.\n *\n * The shape used to live in `helpers.createStateObject`, shared with\n * `StateNamespace.makeState`; Phase 4 folded that producer onto the pipeline and\n * left the helper with a single caller and a docstring whose whole justification\n * (\"one constructor for both producers\") had expired. It is inlined here — ⑤b IS\n * \"make the state object\", so the shape belongs to the primitive that owns the\n * stage rather than to a helper one import away.\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 * 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 */\nexport function materialize<\n P extends Params = Params,\n S extends SearchParams = SearchParams,\n>(canonical: Canonical, opts: MaterializeOptions): 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 = {\n name: canonical.name,\n params: canonical.path as P,\n search: canonical.query as S,\n path: opts.path,\n context: {},\n ...(!opts.skipFreeze && { transition: DEFAULT_TRANSITION }),\n } as State<P, S>;\n\n // The path channel is frozen HERE, at the publication boundary — `materialize`\n // is the one place a `Canonical` becomes something user code can hold (#1598).\n // BEFORE the `skipFreeze` branch on purpose: that flag defers the state SHELL\n // (the navigate path attaches `transition` and lets plugins write `context`\n // after the fact), never the channels, so guards see frozen bags either way.\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 Object.freeze(state.params);\n\n return opts.skipFreeze ? state : freezeStateShell(state);\n}\n"],"mappings":"8DA2CA,SAAgB,EAGd,EAAsB,EAAuC,CAS7D,IAAM,EAAQ,CACZ,KAAM,EAAU,KAChB,OAAQ,EAAU,KAClB,OAAQ,EAAU,MAClB,KAAM,EAAK,KACX,QAAS,CAAC,EACV,GAAI,CAAC,EAAK,YAAc,CAAE,WAAYA,EAAAA,kBAAmB,CAC3D,EAgBA,OAFA,OAAO,OAAO,EAAM,MAAM,EAEnB,EAAK,WAAa,EAAQC,EAAAA,iBAAiB,CAAK,CACzD"}
|
package/dist/cjs/routerFSM.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("./utils/fsm/fsm.js"),t={IDLE:`IDLE`,STARTING:`STARTING`,READY:`READY`,TRANSITION_STARTED:`TRANSITION_STARTED`,LEAVE_APPROVED:`LEAVE_APPROVED`,DISPOSED:`DISPOSED`},n={START:`START`,STARTED:`STARTED`,NAVIGATE:`NAVIGATE`,LEAVE_APPROVE:`LEAVE_APPROVE`,COMPLETE:`COMPLETE`,FAIL:`FAIL`,CANCEL:`CANCEL`,STOP:`STOP`,DISPOSE:`DISPOSE`},r={initial:t.IDLE,context:null,transitions:{[t.IDLE]:{[n.START]:t.STARTING,[n.DISPOSE]:t.DISPOSED},[t.STARTING]:{[n.STARTED]:t.READY,[n.FAIL]:t.IDLE,[n.STOP]:t.IDLE,[n.DISPOSE]:t.DISPOSED},[t.READY]:{[n.NAVIGATE]:t.TRANSITION_STARTED,[n.FAIL]:t.READY,[n.STOP]:t.IDLE,[n.DISPOSE]:t.DISPOSED},[t.TRANSITION_STARTED]:{[n.NAVIGATE]:t.TRANSITION_STARTED,[n.LEAVE_APPROVE]:t.LEAVE_APPROVED,[n.CANCEL]:t.READY,[n.FAIL]:t.READY,[n.DISPOSE]:t.DISPOSED},[t.LEAVE_APPROVED]:{[n.NAVIGATE]:t.TRANSITION_STARTED,[n.COMPLETE]:t.READY,[n.CANCEL]:t.READY,[n.FAIL]:t.READY,[n.DISPOSE]:t.DISPOSED},[t.DISPOSED]:{}}};function i(){return new e.FSM(r)}exports.createRouterFSM=i,exports.routerEvents=n,exports.routerStates=t;
|
|
2
|
-
//# sourceMappingURL=routerFSM.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"routerFSM.js","names":["FSM"],"sources":["../../src/routerFSM.ts"],"sourcesContent":["// packages/core/src/routerFSM.ts\n\nimport { FSM } from \"./utils/fsm\";\n\nimport type { NavigationOptions, State } from \"./types\";\nimport type { FSMConfig } 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.\n *\n * - START: Begin router initialization\n * - STARTED: Router initialization complete\n * - NAVIGATE: Begin navigation\n * - COMPLETE: Navigation completed successfully\n * - FAIL: Navigation or initialization failed\n * - CANCEL: Navigation cancelled\n * - STOP: Stop router\n * - DISPOSE: Dispose router (R2+)\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} as const;\n\nexport type RouterEvent = (typeof routerEvents)[keyof typeof routerEvents];\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 */\nexport interface RouterPayloads {\n NAVIGATE: { toState: State; fromState?: State | undefined };\n LEAVE_APPROVE: { toState: State; fromState?: State | undefined };\n COMPLETE: {\n toState: State;\n fromState?: State | undefined;\n opts?: NavigationOptions | undefined;\n };\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 (FAIL, self-loop for early validation errors), 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 */\nconst routerFSMConfig: FSMConfig<RouterState, RouterEvent, null> = {\n initial: routerStates.IDLE,\n context: null,\n transitions: {\n [routerStates.IDLE]: {\n [routerEvents.START]: routerStates.STARTING,\n [routerEvents.DISPOSE]: routerStates.DISPOSED,\n },\n [routerStates.STARTING]: {\n [routerEvents.STARTED]: routerStates.READY,\n [routerEvents.FAIL]: routerStates.IDLE,\n [routerEvents.STOP]: routerStates.IDLE,\n [routerEvents.DISPOSE]: routerStates.DISPOSED,\n },\n [routerStates.READY]: {\n [routerEvents.NAVIGATE]: routerStates.TRANSITION_STARTED,\n [routerEvents.FAIL]: routerStates.READY,\n [routerEvents.STOP]: routerStates.IDLE,\n [routerEvents.DISPOSE]: routerStates.DISPOSED,\n },\n [routerStates.TRANSITION_STARTED]: {\n [routerEvents.NAVIGATE]: routerStates.TRANSITION_STARTED,\n [routerEvents.LEAVE_APPROVE]: routerStates.LEAVE_APPROVED,\n [routerEvents.CANCEL]: routerStates.READY,\n [routerEvents.FAIL]: routerStates.READY,\n [routerEvents.DISPOSE]: routerStates.DISPOSED,\n },\n [routerStates.LEAVE_APPROVED]: {\n [routerEvents.NAVIGATE]: routerStates.TRANSITION_STARTED,\n [routerEvents.COMPLETE]: routerStates.READY,\n [routerEvents.CANCEL]: routerStates.READY,\n [routerEvents.FAIL]: routerStates.READY,\n [routerEvents.DISPOSE]: routerStates.DISPOSED,\n },\n [routerStates.DISPOSED]: {},\n },\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 null,\n RouterPayloads\n> {\n return new FSM<RouterState, RouterEvent, null, RouterPayloads>(\n routerFSMConfig,\n );\n}\n"],"mappings":"sCAiBa,EAAe,CAC1B,KAAM,OACN,SAAU,WACV,MAAO,QACP,mBAAoB,qBACpB,eAAgB,iBAChB,SAAU,UACZ,EAgBa,EAAe,CAC1B,MAAO,QACP,QAAS,UACT,SAAU,WACV,cAAe,gBACf,SAAU,WACV,KAAM,OACN,OAAQ,SACR,KAAM,OACN,QAAS,SACX,EAsCM,EAA6D,CACjE,QAAS,EAAa,KACtB,QAAS,KACT,YAAa,EACV,EAAa,MAAO,EAClB,EAAa,OAAQ,EAAa,UAClC,EAAa,SAAU,EAAa,QACvC,GACC,EAAa,UAAW,EACtB,EAAa,SAAU,EAAa,OACpC,EAAa,MAAO,EAAa,MACjC,EAAa,MAAO,EAAa,MACjC,EAAa,SAAU,EAAa,QACvC,GACC,EAAa,OAAQ,EACnB,EAAa,UAAW,EAAa,oBACrC,EAAa,MAAO,EAAa,OACjC,EAAa,MAAO,EAAa,MACjC,EAAa,SAAU,EAAa,QACvC,GACC,EAAa,oBAAqB,EAChC,EAAa,UAAW,EAAa,oBACrC,EAAa,eAAgB,EAAa,gBAC1C,EAAa,QAAS,EAAa,OACnC,EAAa,MAAO,EAAa,OACjC,EAAa,SAAU,EAAa,QACvC,GACC,EAAa,gBAAiB,EAC5B,EAAa,UAAW,EAAa,oBACrC,EAAa,UAAW,EAAa,OACrC,EAAa,QAAS,EAAa,OACnC,EAAa,MAAO,EAAa,OACjC,EAAa,SAAU,EAAa,QACvC,GACC,EAAa,UAAW,CAAC,CAC5B,CACF,EAOA,SAAgB,GAKd,CACA,OAAO,IAAIA,EAAAA,IACT,CACF,CACF"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=[];Object.freeze(e);function t(e){let t=e.split(`.`),n=t.length,r=[t[0]],i=t[0].length;for(let a=1;a<n-1;a++)i+=1+t[a].length,r.push(e.slice(0,i));return r.push(e),r}const n=new Set([`string`,`number`,`boolean`]);function r(e){return n.has(typeof e)}function i(e,t,n,i){let a=t[e];if(!a||typeof a!=`object`)return!0;for(let e of Object.keys(a)){let t=n.params[e],a=i.params[e];if(r(t)&&r(a)&&String(t)!==String(a))return!1}return!0}function a(e,t,n,r,a,o){for(let s=0;s<o;s++){let o=r[s];if(o!==a[s]||!i(o,e,t,n))return s}return o}const o=new Map;function s(e){let t=o.get(e);if(t)return t;let n=c(e);return Object.freeze(n),o.set(e,n),n}function c(e){if(!e)return[``];let n=e.indexOf(`.`);if(n===-1)return[e];let r=e.indexOf(`.`,n+1);if(r===-1)return[e.slice(0,n),e];let i=e.indexOf(`.`,r+1);return i===-1?[e.slice(0,n),e.slice(0,r),e]:e.indexOf(`.`,i+1)===-1?[e.slice(0,n),e.slice(0,r),e.slice(0,i),e]:t(e)}let l,u,d=null,f,p,m=null;function h(t,n,r){if(!n)return{intersection:``,toActivate:s(t.name),toDeactivate:e};let i=r(t.name),o=r(n.name);if(!i&&!o)return{intersection:``,toActivate:s(t.name),toDeactivate:s(n.name)};let c=s(t.name),l=s(n.name),u=Math.min(l.length,c.length),d=a(i??o,t,n,c,l,u),f;if(d>=l.length)f=e;else if(d===0&&l.length===1)f=l;else{f=[];for(let e=l.length-1;e>=d;e--)f.push(l[e])}let p=d===0?c:c.slice(d);return{intersection:d>0?l[d-1]:``,toDeactivate:f,toActivate:p}}function g(e,t,n){if(d!==null&&e===l&&t===u)return d;if(m!==null&&e===f&&t===p)return m;let r=h(e,t,n);return f=l,p=u,m=d,l=e,u=t,d=r,r}exports.getTransitionPath=g,exports.nameToIDs=s;
|
|
2
|
-
//# sourceMappingURL=transitionPath.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"transitionPath.js","names":[],"sources":["../../src/transitionPath.ts"],"sourcesContent":["// packages/core/src/transitionPath.ts\n\nimport type { State } from \"./types\";\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 Object.keys(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 Object.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"],"mappings":"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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e={maxListeners:0,warnListeners:0};var t=class{#e=new Map;#t=new Set;#n=null;#r=e;#i;#a;constructor(e){e?.limits&&(this.#r=e.limits),this.#i=e?.onListenerError??null,this.#a=e?.onListenerWarn??null}static validateCallback(e,t){if(typeof e!=`function`)throw TypeError(`Expected callback to be a function for event ${t}`)}setLimits(e){this.#r=e}on(e,t){let n=this.#e.get(e),r=n?.size??0;if(n?.has(t))throw Error(`Duplicate listener for "${e}"`);let{maxListeners:i,warnListeners:a}=this.#r;if(i!==0&&r>=i)throw Error(`Listener limit (${i}) reached for "${e}"`);a!==0&&r===a&&this.#a!==null&&(this.#n??=new Set,this.#n.has(e)||(this.#a(e,a),this.#n.add(e)));let o=n;return o===void 0&&(o=new Set,this.#e.set(e,o)),o.add(t),()=>{this.off(e,t)}}off(e,t){let n=this.#e.get(e);n&&(n.delete(t),n.size===0&&(this.#e.delete(e),this.#n?.delete(e)))}emit(e,t,n,r,i){let a=this.#e.get(e);if(!a||a.size===0||this.#t.has(e))return;let o=arguments.length-1;this.#t.add(e);try{if(a.size===1){let[s]=a;this.#o(e,s,o,t,n,r,i)}else{let s=[...a];for(let a of s)this.#o(e,a,o,t,n,r,i)}}finally{this.#t.delete(e)}}clearAll(){this.#e.clear(),this.#n=null}listenerCount(e){return this.#e.get(e)?.size??0}isDispatching(e){return this.#t.has(e)}#o(e,t,n,r,i,a,o){try{let s=this.#s(t,n,r,i,a,o);s!=null&&typeof s.then==`function`&&Promise.resolve(s).catch(t=>{this.#i?.(e,t)})}catch(t){this.#i?.(e,t)}}#s(e,t,n,r,i,a){switch(t){case 0:return e();case 1:return e(n);case 2:return e(n,r);case 3:return e(n,r,i);default:return e(n,r,i,a)}}};exports.EventEmitter=t;
|
|
2
|
-
//# sourceMappingURL=EventEmitter.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"EventEmitter.js","names":["#callbacks","#dispatching","#onListenerError","#onListenerWarn","#limits","#warnedEvents","#invokeIsolated","#callListener"],"sources":["../../../../src/utils/event-emitter/EventEmitter.ts"],"sourcesContent":["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 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.#onListenerError?.(eventName, error);\n },\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"],"mappings":"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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
function e(e,t,n){let r=e[t];if(r===void 0)throw Error(`[FSM.${n}] state "${t}" is not declared in config.transitions`);return r}var t=class{#e;#t;#n=0;#r=null;#i;#a;#o=[];constructor(t){this.#e=t.initial,this.#i=t.context,this.#a=t.transitions,this.#t=e(t.transitions,t.initial,`constructor`);for(let n of Object.keys(t.transitions)){let r=t.transitions[n];for(let n of Object.keys(r)){let i=r[n];i!==void 0&&e(t.transitions,i,`constructor`)}}}send(e,...t){let n=this.#t[e];if(n===void 0)return this.#e;let r=this.#e;this.#e=n,this.#t=this.#a[n];let i=t[0];if(this.#r!==null){let t=this.#r.get(r)?.get(e);t!==void 0&&t(i)}if(this.#n>0){let t={from:r,to:n,event:e,payload:i};for(let e of this.#o)e!==null&&e(t)}return this.#e}canSend(e){return this.#t[e]!==void 0}getState(){return this.#e}getContext(){return this.#i}on(t,n,r){e(this.#a,t,`on`),this.#r??=new Map;let i=this.#r.get(t);i||(i=new Map,this.#r.set(t,i));let a=r;return i.set(n,a),()=>{let e=this.#r?.get(t);e?.get(n)===a&&e.delete(n)}}onTransition(e){let t=this.#o.indexOf(null),n;t===-1?(n=this.#o.length,this.#o.push(e)):(this.#o[t]=e,n=t),this.#n++;let r=!0;return()=>{r&&(r=!1,this.#o[n]=null,this.#n--)}}};exports.FSM=t;
|
|
2
|
-
//# sourceMappingURL=fsm.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fsm.js","names":["#context","#transitions","#listeners","#state","#currentTransitions","#actions","#listenerCount"],"sources":["../../../../src/utils/fsm/fsm.ts"],"sourcesContent":["import type { FSMConfig, TransitionInfo, TransitionListener } from \"./types\";\n\n/**\n * Shared guard for the engine-wide invariant \"the state is declared in\n * `config.transitions`\". Applied at every state-entry-point (constructor\n * `initial` and `on`'s `from`) so an undeclared state fails loud with\n * an explicit error instead of bricking the FSM or dead-registering an action\n * (#885). Returns the state's transition map for the caller to reuse.\n */\nfunction requireDeclared<TStates extends string, TEvents extends string>(\n transitions: Record<TStates, Partial<Record<TEvents, TStates>>>,\n state: TStates,\n where: string,\n): Partial<Record<TEvents, TStates>> {\n const stateTransitions = transitions[state];\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime guard for JS / cast / string-typed callers passing a state outside TStates\n if (stateTransitions === undefined) {\n throw new Error(\n `[FSM.${where}] state \"${state}\" is not declared in config.transitions`,\n );\n }\n\n return stateTransitions;\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()`.\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: Partial<Record<TEvents, TStates>>;\n #listenerCount = 0;\n #actions: Map<TStates, Map<TEvents, (payload: unknown) => void>> | null =\n null;\n readonly #context: TContext;\n readonly #transitions: Record<TStates, Partial<Record<TEvents, TStates>>>;\n readonly #listeners: (TransitionListener<\n TStates,\n TEvents,\n TPayloadMap\n > | null)[] = [];\n\n constructor(config: FSMConfig<TStates, TEvents, TContext>) {\n this.#state = config.initial;\n this.#context = config.context;\n this.#transitions = config.transitions;\n this.#currentTransitions = requireDeclared(\n config.transitions,\n config.initial,\n \"constructor\",\n );\n\n // #1159: validate table closure — every declared transition target must\n // itself be a declared state. `send()` applies table values\n // (`this.#transitions[nextState]`) without re-checking, so a dangling\n // target would silently enter an undeclared state (violating Validity #1)\n // and brick `canSend()` (violating No-bricking #10). One cold-path\n // O(states×events) pass at construction fails loud instead — the fourth\n // state-entry-point, mirroring the `initial` / `on` guards. Explicit\n // `undefined` values are the declared \"no transition\" no-op (send() returns\n // the current state) and are skipped. Post-construction mutation of the\n // shared table stays a documented GIGO boundary (Edge #5).\n for (const state of Object.keys(config.transitions)) {\n const stateTransitions = config.transitions[state as TStates];\n\n for (const event of Object.keys(stateTransitions)) {\n const target = stateTransitions[event as TEvents];\n\n if (target !== undefined) {\n requireDeclared(config.transitions, target, \"constructor\");\n }\n }\n }\n }\n\n send<E extends TEvents>(\n event: E,\n ...args: E extends keyof TPayloadMap ? [TPayloadMap[E]] : [undefined?]\n ): TStates {\n const nextState = this.#currentTransitions[event];\n\n if (nextState === undefined) {\n return this.#state;\n }\n\n const from = this.#state;\n\n this.#state = nextState;\n this.#currentTransitions = this.#transitions[nextState];\n\n const payload = args[0] as TPayloadMap[TEvents] | undefined;\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 // `info` is structurally a valid TransitionInfo, but the distributive\n // union can't be matched to one variant while `event`/`payload` are\n // generic here — erase through `unknown` (TS2352), same spirit as the\n // `args[0]` cast above.\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 canSend(event: TEvents): boolean {\n return this.#currentTransitions[event] !== undefined;\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 requireDeclared(this.#transitions, from, \"on\");\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"],"mappings":"AASA,SAAS,EACP,EACA,EACA,EACmC,CACnC,IAAM,EAAmB,EAAY,GAGrC,GAAI,IAAqB,IAAA,GACvB,MAAU,MACR,QAAQ,EAAM,WAAW,EAAM,wCACjC,EAGF,OAAO,CACT,CAYA,IAAa,EAAb,KAKE,CACA,GACA,GACA,GAAiB,EACjB,GACE,KACF,GACA,GACA,GAIc,CAAC,EAEf,YAAY,EAA+C,CACzD,KAAKG,GAAS,EAAO,QACrB,KAAKH,GAAW,EAAO,QACvB,KAAKC,GAAe,EAAO,YAC3B,KAAKG,GAAsB,EACzB,EAAO,YACP,EAAO,QACP,aACF,EAYA,IAAK,IAAM,KAAS,OAAO,KAAK,EAAO,WAAW,EAAG,CACnD,IAAM,EAAmB,EAAO,YAAY,GAE5C,IAAK,IAAM,KAAS,OAAO,KAAK,CAAgB,EAAG,CACjD,IAAM,EAAS,EAAiB,GAE5B,IAAW,IAAA,IACb,EAAgB,EAAO,YAAa,EAAQ,aAAa,CAE7D,CACF,CACF,CAEA,KACE,EACA,GAAG,EACM,CACT,IAAM,EAAY,KAAKA,GAAoB,GAE3C,GAAI,IAAc,IAAA,GAChB,OAAO,KAAKD,GAGd,IAAM,EAAO,KAAKA,GAElB,KAAKA,GAAS,EACd,KAAKC,GAAsB,KAAKH,GAAa,GAE7C,IAAM,EAAU,EAAK,GAErB,GAAI,KAAKI,KAAa,KAAM,CAC1B,IAAM,EAAS,KAAKA,GAAS,IAAI,CAAI,CAAC,EAAE,IAAI,CAAK,EAE7C,IAAW,IAAA,IACb,EAAO,CAAO,CAElB,CAGA,GAAI,KAAKC,GAAiB,EAAG,CAK3B,IAAM,EAAO,CACX,OACA,GAAI,EACJ,QACA,SACF,EAEA,IAAK,IAAM,KAAY,KAAKJ,GACtB,IAAa,MACf,EAAS,CAAI,CAGnB,CAEA,OAAO,KAAKC,EACd,CAEA,QAAQ,EAAyB,CAC/B,OAAO,KAAKC,GAAoB,KAAW,IAAA,EAC7C,CAEA,UAAoB,CAClB,OAAO,KAAKD,EACd,CAEA,YAAuB,CACrB,OAAO,KAAKH,EACd,CAEA,GACE,EACA,EACA,EAGY,CACZ,EAAgB,KAAKC,GAAc,EAAM,IAAI,EAE7C,KAAKI,KAAa,IAAI,IAEtB,IAAI,EAAe,KAAKA,GAAS,IAAI,CAAI,EAEpC,IACH,EAAe,IAAI,IACnB,KAAKA,GAAS,IAAI,EAAM,CAAY,GAGtC,IAAM,EAAiB,EAIvB,OAFA,EAAa,IAAI,EAAO,CAAc,MAEzB,CAEX,IAAM,EAAW,KAAKA,IAAU,IAAI,CAAI,EAEpC,GAAU,IAAI,CAAK,IAAM,GAC3B,EAAS,OAAO,CAAK,CAEzB,CACF,CAEA,aACE,EACY,CACZ,IAAM,EAAY,KAAKH,GAAW,QAAQ,IAAI,EAC1C,EAEA,IAAc,IAChB,EAAQ,KAAKA,GAAW,OACxB,KAAKA,GAAW,KAAK,CAAQ,IAE7B,KAAKA,GAAW,GAAa,EAC7B,EAAQ,GAGV,KAAKI,KACL,IAAI,EAAa,GAEjB,UAAa,CACN,IAIL,EAAa,GACb,KAAKJ,GAAW,GAAS,KAEzB,KAAKI,KACP,CACF,CACF"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("./constants.js");var t=class{#e={level:`all`,callbackIgnoresLevel:!1};#t=0;#n=!1;constructor(e){e&&this.configure(e)}configure(t){let n=t.level;if(n!==void 0){if(!Object.hasOwn(e.LEVEL_CONFIGS,n))throw Error(`Invalid log level: "${n}". Valid levels are: ${Object.keys(e.LEVEL_CONFIGS).join(`, `)}`);this.#e.level=n,this.#t=e.LEVEL_CONFIGS[n]}Object.hasOwn(t,`callback`)&&(this.#e.callback=t.callback);let r=t.callbackIgnoresLevel;r!==void 0&&(this.#e.callbackIgnoresLevel=r)}getConfig(){return{level:this.#e.level,callback:this.#e.callback,callbackIgnoresLevel:this.#e.callbackIgnoresLevel}}log(e,t,...n){this.#r(`log`,e,t,n)}warn(e,t,...n){this.#r(`warn`,e,t,n)}error(e,t,...n){this.#r(`error`,e,t,n)}#r(t,n,r,i){if(this.#e.level===`none`&&!this.#e.callbackIgnoresLevel)return;let a=e.LOG_LEVELS[t]<this.#t;a||this.#i(t,n,r,i),this.#a(t,n,r,a,i)}#i(e,t,n,r){if(typeof console<`u`&&typeof console[e]==`function`){let i=t?`[${t}] ${n}`:n;console[e](i,...r)}}#a(e,t,n,r,i){if(!(!this.#e.callback||!this.#e.callbackIgnoresLevel&&r)&&!this.#n){this.#n=!0;try{let r=this.#e.callback(e,t,n,...i);r!=null&&typeof r.then==`function`&&Promise.resolve(r).catch(e=>{this.#o(`[Logger] Error in async callback:`,e)})}catch(e){this.#o(`[Logger] Error in callback:`,e)}finally{this.#n=!1}}}#o(e,t){typeof console<`u`&&typeof console.error==`function`&&console.error(e,t)}};exports.RouterLogger=t;
|
|
2
|
-
//# sourceMappingURL=RouterLogger.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"RouterLogger.js","names":["#config","LEVEL_CONFIGS","#currentThreshold","#writeLog","LOG_LEVELS","#writeToConsole","#invokeCallback","#inCallback","#reportError"],"sources":["../../../../src/utils/logger/RouterLogger.ts"],"sourcesContent":["// packages/core/src/utils/logger/RouterLogger.ts\n\nimport { LOG_LEVELS, LEVEL_CONFIGS } from \"./constants\";\n\nimport type {\n LogLevel,\n LoggerConfig,\n LogLevelConfig,\n LogCallback,\n} from \"../../types\";\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 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 // Read each field ONCE into a local — an unstable getter must not be re-read\n // between validation and storage: re-reading could pass validation with a\n // valid level and then store a later, unvalidated one, disabling the\n // threshold filter (a TOCTOU, #1162).\n const level = config.level;\n\n if (level !== undefined) {\n // Validate that the provided level is a valid configuration level\n if (!Object.hasOwn(LEVEL_CONFIGS, level)) {\n throw new Error(\n `Invalid log level: \"${level}\". Valid levels are: ${Object.keys(LEVEL_CONFIGS).join(\", \")}`,\n );\n }\n\n this.#config.level = level;\n this.#currentThreshold = LEVEL_CONFIGS[level];\n }\n if (Object.hasOwn(config, \"callback\")) {\n this.#config.callback = config.callback;\n }\n\n const callbackIgnoresLevel = config.callbackIgnoresLevel;\n\n if (callbackIgnoresLevel !== undefined) {\n this.#config.callbackIgnoresLevel = 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"],"mappings":"kCA2CA,IAAa,EAAb,KAA0B,CAExB,GAAyC,CACvC,MAAO,MACP,qBAAsB,EACxB,EAGA,GAAoB,EAQpB,GAAc,GAWd,YAAY,EAAgC,CACtC,GACF,KAAK,UAAU,CAAM,CAEzB,CAwBA,UAAU,EAAqC,CAK7C,IAAM,EAAQ,EAAO,MAErB,GAAI,IAAU,IAAA,GAAW,CAEvB,GAAI,CAAC,OAAO,OAAOC,EAAAA,cAAe,CAAK,EACrC,MAAU,MACR,uBAAuB,EAAM,uBAAuB,OAAO,KAAKA,EAAAA,aAAa,CAAC,CAAC,KAAK,IAAI,GAC1F,EAGF,KAAKD,GAAQ,MAAQ,EACrB,KAAKE,GAAoBD,EAAAA,cAAc,EACzC,CACI,OAAO,OAAO,EAAQ,UAAU,IAClC,KAAKD,GAAQ,SAAW,EAAO,UAGjC,IAAM,EAAuB,EAAO,qBAEhC,IAAyB,IAAA,KAC3B,KAAKA,GAAQ,qBAAuB,EAExC,CAcA,WAA0B,CACxB,MAAO,CACL,MAAO,KAAKA,GAAQ,MACpB,SAAU,KAAKA,GAAQ,SACvB,qBAAsB,KAAKA,GAAQ,oBACrC,CACF,CAiBA,IAAI,EAAiB,EAAiB,GAAG,EAAuB,CAC9D,KAAKG,GAAU,MAAO,EAAS,EAAS,CAAI,CAC9C,CAkBA,KAAK,EAAiB,EAAiB,GAAG,EAAuB,CAC/D,KAAKA,GAAU,OAAQ,EAAS,EAAS,CAAI,CAC/C,CAkBA,MAAM,EAAiB,EAAiB,GAAG,EAAuB,CAChE,KAAKA,GAAU,QAAS,EAAS,EAAS,CAAI,CAChD,CAiBA,GACE,EACA,EACA,EACA,EACM,CAIN,GAAI,KAAKH,GAAQ,QAAU,QAAU,CAAC,KAAKA,GAAQ,qBACjD,OASF,IAAM,EAJoBI,EAAAA,WAAW,GAIS,KAAKF,GAG9C,GACH,KAAKG,GAAgB,EAAO,EAAS,EAAS,CAAI,EAIpD,KAAKC,GAAgB,EAAO,EAAS,EAAS,EAAmB,CAAI,CACvE,CAiBA,GACE,EACA,EACA,EACA,EACM,CAGN,GACE,OAAO,QAAY,KACnB,OAAO,QAAQ,IAAW,WAC1B,CAIA,IAAM,EAAmB,EAAU,IAAI,EAAQ,IAAI,IAAY,EAE/D,QAAQ,EAAM,CAAC,EAAkB,GAAG,CAAI,CAC1C,CACF,CAsBA,GACE,EACA,EACA,EACA,EACA,EACM,CAGJ,MAAC,KAAKN,GAAQ,UACb,CAAC,KAAKA,GAAQ,sBAAwB,IASrC,MAAKO,GAMT,MAAKA,GAAc,GACnB,GAAI,CASF,IAAM,EAAkB,KAAKP,GAAQ,SACnC,EACA,EACA,EACA,GAAG,CACL,EAGE,GAAW,MAEX,OAAQ,EAAgC,MAAS,YAEjD,QAAQ,QAAQ,CAA8B,CAAC,CAAC,MAC7C,GAAmB,CAClB,KAAKQ,GAAa,oCAAqC,CAAK,CAC9D,CACF,CAEJ,OAAS,EAAO,CAEd,KAAKA,GAAa,8BAA+B,CAAK,CACxD,QAAU,CACR,KAAKD,GAAc,EACrB,CAjCmB,CAkCrB,CAKA,GAAa,EAAiB,EAAsB,CAC9C,OAAO,QAAY,KAAe,OAAO,QAAQ,OAAU,YAC7D,QAAQ,MAAM,EAAS,CAAK,CAEhC,CACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","names":[],"sources":["../../../../src/utils/logger/constants.ts"],"sourcesContent":["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"],"mappings":"AAyBA,MAAa,EAAuC,OAAO,OAAO,CAChE,IAAK,EACL,KAAM,EACN,MAAO,CACT,CAAC,EAyCY,EAAgD,OAAO,OAAO,CACzE,IAAK,EACL,aAAc,EACd,aAAc,EACd,KAAM,CACR,CAAC"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("../internals.js"),t=require("../namespaces/OptionsNamespace/helpers.js"),n=require("../pipeline/canonicalize.js"),r=require("../pipeline/buildURL.js"),i=require("../pipeline/materialize.js");function a(t){let n=o(t),r=f(t),i=()=>e.getInternals(t.router).validator;s(t),c(t,i),l(t,n,i),u(t,r,i),d(t,n,i),p(t,r),m(t),h(t,r)}function o(e){let{router:t,dependenciesStore:n}=e,r=e=>n.dependencies[e];return e=>e(t,r)}function s(e){e.dependenciesStore.limits=e.limits,e.eventBus.setLimits({maxListeners:e.limits.maxListeners,warnListeners:e.limits.warnListeners})}function c(e,t){e.eventBus.setValidatorAccessor(t)}function l(t,n,r){let i={logger:e.getInternals(t.router).logger,compileFactory:n,getValidator:r};t.routeLifecycle.setDependencies(i)}function u(t,n,r){let i={logger:e.getInternals(t.router).logger,getValidator:r,port:n,addActivateGuard:(e,n,r)=>{t.routeLifecycle.addCanActivate(e,n,!0,r)},addDeactivateGuard:(e,n,r)=>{t.routeLifecycle.addCanDeactivate(e,n,!0,r)},compileGuard:(e,n)=>t.routeLifecycle.compileGuardFactory(e,n),getState:()=>t.state.get(),areStatesEqual:(e,n,r)=>t.state.areStatesEqual(e,n,r),getDependency:e=>t.dependenciesStore.dependencies[e]};t.routes.setDependencies(i),t.routes.setLifecycleNamespace(t.routeLifecycle)}function d(t,n,r){let i={logger:e.getInternals(t.router).logger,addEventListener:(e,n)=>t.eventBus.addEventListener(e,n),canNavigate:()=>t.eventBus.canBeginTransition(),compileFactory:n,getValidator:r};t.plugins.setDependencies(i)}function f(t){let n=e.getInternals(t.router),r=t.routes.getStore(),i=(e,t)=>{n.validator?.state.reportUndeclaredParamKey(e,t)},a=(e,t)=>{n.validator?.state.reportDroppedQueryKey(e,t)};return{resolveForward:(e,t,r)=>n.forwardState(e,t,r),defaultParams:e=>r.config.defaultParams[e],defaultSearch:e=>r.config.defaultSearch[e],buildPath:(e,t,r)=>n.buildPath(e,t,r),queryNames:e=>t.routes.getQueryParams(e),pathNames:e=>t.routes.hasRoute(e)?t.routes.getUrlParams(e):void 0,admitsUndeclaredQuery:()=>t.options.get().queryParamsMode===`loose`,get reportDroppedQueryKey(){return n.validator?a:void 0},get reportUndeclaredParamKey(){return n.validator?i:void 0}}}function p(a,o){let s={logger:e.getInternals(a.router).logger,getOptions:()=>a.options.get(),hasRoute:e=>a.routes.hasRoute(e),getQueryParams:e=>a.routes.getQueryParams(e),getMetaForState:e=>a.routes.getMetaForState(e),getState:()=>a.state.get(),setState:e=>{a.state.set(e)},buildNavigateState:(t,s,c)=>{e.getInternals(a.router).validator?.routes.validateStateBuilderArgs(t,s,`navigate`);let l=n.canonicalize(o,t,s,c,{diagnoseUndeclared:!0});if(a.routes.getMetaForState(l.name)!==void 0)return i.materialize(l,{path:r.buildURL(l,o),skipFreeze:!0})},resolveDefault:()=>{let n=a.options.get(),r=e.getInternals(a.router),i=t.resolveOption(n.defaultRoute,e=>a.dependenciesStore.dependencies[e]),o=t.resolveOption(n.defaultParams,e=>a.dependenciesStore.dependencies[e]),s=t.resolveOption(n.defaultSearch,e=>a.dependenciesStore.dependencies[e]);return typeof n.defaultRoute==`function`&&r.validator?.options.validateResolvedDefaultRoute(i,r.routeGetStore()),{route:i,params:o,search:s}},startTransition:(e,t)=>{a.eventBus.sendNavigate(e,t)},cancelNavigation:e=>{a.eventBus.sendCancelIfPossible(a.state.get(),e)},sendTransitionDone:(e,t,n)=>{a.eventBus.sendComplete(e,t,n)},sendTransitionFail:(e,t,n)=>{a.eventBus.sendFail(e,t,n)},emitTransitionError:(e,t,n)=>{a.eventBus.sendFailSafe(e,t,n)},emitTransitionSuccess:(e,t,n)=>{a.eventBus.emitTransitionSuccess(e,t,n)},sendLeaveApprove:(e,t)=>{a.eventBus.sendLeaveApprove(e,t)},canNavigate:()=>a.eventBus.canBeginTransition(),getLifecycleFunctions:()=>a.routeLifecycle.getFunctions(),isActive:()=>a.router.isActive(),isTransitioning:()=>a.eventBus.isTransitioning(),clearCanDeactivate:e=>{a.routeLifecycle.clearCanDeactivate(e,`external`)},hasLeaveListeners:()=>a.eventBus.hasLeaveListeners(),hasPreCommitListeners:()=>a.eventBus.hasPreCommitListeners(),awaitLeaveListeners:(e,t,n)=>a.eventBus.awaitLeaveListeners(e,t,n)};a.navigation.setDependencies(s)}function m(e){e.lifecycle.setDependencies({getOptions:()=>e.options.get(),navigateToState:(t,n)=>e.navigation.navigateToState(t,n),navigateToNotFound:t=>e.navigation.navigateToNotFound(t),clearState:()=>{e.state.set(void 0)},matchPath:t=>e.routes.matchPath(t,e.options.get()),completeStart:()=>{e.eventBus.sendStarted()},isIdle:()=>e.eventBus.isIdle(),emitTransitionError:(t,n,r)=>{e.eventBus.sendFail(t,n,r)}})}function h(e,t){e.state.setDependencies({port:()=>t,getUrlParams:t=>e.routes.getUrlParams(t)})}exports.wireNamespaces=a;
|
|
2
|
-
//# sourceMappingURL=wireNamespaces.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"wireNamespaces.js","names":["getInternals","canonicalize","materialize","buildURL","resolveOption"],"sources":["../../../src/wiring/wireNamespaces.ts"],"sourcesContent":["// packages/core/src/wiring/wireNamespaces.ts\n\nimport { getInternals } from \"../internals\";\nimport { resolveOption } from \"../namespaces/OptionsNamespace\";\nimport { buildURL, canonicalize, materialize } from \"../pipeline\";\n\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 `ctx.buildPath`, because the navigate path prints its URL\n * through that interceptable — reaching for the engine's matcher instead would\n * silently drop `persistent-params`' `buildPath` interceptor from the navigate\n * path.\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 buildPath: (name, params, search) => ctx.buildPath(name, params, search),\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 // Read per call, not captured: `queryParamsMode` lives in the options\n // namespace, which `setOption` can rewrite after wiring.\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 read per call, exactly like\n // `admitsUndeclaredQuery` above.\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 setState: (state) => {\n ns.state.set(state);\n },\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`. `skipFreeze`\n // defers the freeze of the state OBJECT for the transition pipeline; the\n // channels were already frozen at merge time.\n return materialize(canonical, {\n path: buildURL(canonical, port),\n skipFreeze: true,\n });\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: (toState, fromState) => {\n ns.eventBus.sendNavigate(toState, fromState);\n },\n cancelNavigation: (reason) => {\n ns.eventBus.sendCancelIfPossible(ns.state.get(), reason);\n },\n sendTransitionDone: (state, fromState, opts) => {\n ns.eventBus.sendComplete(state, fromState, opts);\n },\n sendTransitionFail: (toState, fromState, error) => {\n ns.eventBus.sendFail(toState, fromState, error);\n },\n emitTransitionError: (toState, fromState, error) => {\n ns.eventBus.sendFailSafe(toState, fromState, error);\n },\n emitTransitionSuccess: (toState, fromState, opts) => {\n ns.eventBus.emitTransitionSuccess(toState, fromState, opts);\n },\n sendLeaveApprove: (toState, fromState) => {\n ns.eventBus.sendLeaveApprove(toState, fromState);\n },\n canNavigate: () => ns.eventBus.canBeginTransition(),\n getLifecycleFunctions: () => ns.routeLifecycle.getFunctions(),\n isActive: () => ns.router.isActive(),\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 clearCanDeactivate: (name: string) => {\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 navigateToNotFound: (path) => ns.navigation.navigateToNotFound(path),\n clearState: () => {\n ns.state.set(undefined);\n },\n matchPath: (path) => ns.routes.matchPath(path, ns.options.get()),\n completeStart: () => {\n ns.eventBus.sendStarted();\n },\n isIdle: () => ns.eventBus.isIdle(),\n emitTransitionError: (toState, fromState, error) => {\n ns.eventBus.sendFail(toState, fromState, error);\n },\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. The seven members that used to live here — both default\n // maps, `getQueryParams`, `hasRoute`, `admitsUndeclaredQuery`,\n // `getDropReporter`, `buildPath` — existed only to feed a second copy of\n // stage ③ and the mode gate, and left with it.\n port: () => port,\n getUrlParams: (name) => ns.routes.getUrlParams(name),\n });\n}\n"],"mappings":"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"}
|