@real-router/core 0.79.1 → 0.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -45
- package/dist/cjs/Router-BIYDZRgF.js +2 -0
- package/dist/cjs/Router-BIYDZRgF.js.map +1 -0
- package/dist/cjs/Router.js +1 -1
- package/dist/cjs/Router.js.map +1 -1
- package/dist/cjs/api/cloneRouter.d.ts +1 -1
- package/dist/cjs/api/cloneRouter.js.map +1 -1
- package/dist/cjs/api.js +1 -2
- package/dist/cjs/api.js.map +1 -1
- package/dist/cjs/buildParamMeta-DniluAIV.js.map +1 -1
- package/dist/cjs/engine/builder/buildTree.js +1 -1
- package/dist/cjs/engine/builder/buildTree.js.map +1 -1
- package/dist/cjs/engine/path-matcher/registration/trieNodes.js.map +1 -1
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js +1 -2
- package/dist/cjs/internals.d.ts +1 -2
- package/dist/cjs/internals.d.ts.map +1 -1
- package/dist/cjs/internals.js.map +1 -1
- package/dist/cjs/namespaces/EventBusNamespace/EventBusNamespace.js.map +1 -1
- package/dist/cjs/routerFSM.js +1 -1
- package/dist/cjs/routerFSM.js.map +1 -1
- package/dist/cjs/types/base.d.ts +10 -1
- package/dist/cjs/types/base.d.ts.map +1 -1
- package/dist/cjs/types/router.d.ts +1 -1
- package/dist/cjs/types.d.ts +2 -2
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/cjs/utils/event-emitter/EventEmitter.js.map +1 -0
- package/dist/cjs/utils/fsm/fsm.js.map +1 -0
- package/dist/cjs/utils/logger/RouterLogger.js.map +1 -0
- package/dist/cjs/utils/logger/constants.js.map +1 -0
- package/dist/cjs/validation.js +1 -2
- package/dist/esm/Router-BIui2eew.mjs +2 -0
- package/dist/esm/Router-BIui2eew.mjs.map +1 -0
- package/dist/esm/api/cloneRouter.d.mts +1 -1
- package/dist/esm/api.mjs +1 -1
- package/dist/esm/api.mjs.map +1 -1
- package/dist/esm/buildParamMeta-bOLhLF9h.mjs.map +1 -1
- package/dist/esm/index.d.mts +2 -2
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/internals.d.mts +1 -2
- package/dist/esm/internals.d.mts.map +1 -1
- package/dist/esm/types/base.d.mts +10 -1
- package/dist/esm/types/base.d.mts.map +1 -1
- package/dist/esm/types/router.d.mts +1 -1
- package/dist/esm/types.d.mts +2 -2
- package/dist/esm/types.d.mts.map +1 -1
- package/package.json +4 -13
- package/dist/cjs/Router-DGUed2F4.js +0 -2
- package/dist/cjs/Router-DGUed2F4.js.map +0 -1
- package/dist/cjs/cloneRouter-Ch25z3Z2.js +0 -2
- package/dist/cjs/cloneRouter-Ch25z3Z2.js.map +0 -1
- package/dist/cjs/foundation/event-emitter/EventEmitter.js.map +0 -1
- package/dist/cjs/foundation/fsm/fsm.js.map +0 -1
- package/dist/cjs/foundation/logger/RouterLogger.js.map +0 -1
- package/dist/cjs/foundation/logger/constants.js.map +0 -1
- package/dist/cjs/utils/createRequestScope.d.ts +0 -111
- package/dist/cjs/utils/createRequestScope.d.ts.map +0 -1
- package/dist/cjs/utils/createRequestScope.js +0 -2
- package/dist/cjs/utils/createRequestScope.js.map +0 -1
- package/dist/cjs/utils/getStaticPaths.d.ts +0 -7
- package/dist/cjs/utils/getStaticPaths.d.ts.map +0 -1
- package/dist/cjs/utils/getStaticPaths.js +0 -2
- package/dist/cjs/utils/getStaticPaths.js.map +0 -1
- package/dist/cjs/utils/hydrateRouter.d.ts +0 -69
- package/dist/cjs/utils/hydrateRouter.d.ts.map +0 -1
- package/dist/cjs/utils/hydrateRouter.js +0 -2
- package/dist/cjs/utils/hydrateRouter.js.map +0 -1
- package/dist/cjs/utils/serializeRouterState.d.ts +0 -84
- package/dist/cjs/utils/serializeRouterState.d.ts.map +0 -1
- package/dist/cjs/utils/serializeRouterState.js +0 -2
- package/dist/cjs/utils/serializeRouterState.js.map +0 -1
- package/dist/cjs/utils/serializeState.d.ts +0 -50
- package/dist/cjs/utils/serializeState.d.ts.map +0 -1
- package/dist/cjs/utils/serializeState.js +0 -2
- package/dist/cjs/utils/serializeState.js.map +0 -1
- package/dist/cjs/utils.d.ts +0 -6
- package/dist/cjs/utils.js +0 -2
- package/dist/cjs/utils.js.map +0 -1
- package/dist/esm/Router-Bk6PfSE8.mjs +0 -2
- package/dist/esm/Router-Bk6PfSE8.mjs.map +0 -1
- package/dist/esm/cloneRouter-D4rnqmi9.mjs +0 -2
- package/dist/esm/cloneRouter-D4rnqmi9.mjs.map +0 -1
- package/dist/esm/utils/createRequestScope.d.mts +0 -111
- package/dist/esm/utils/createRequestScope.d.mts.map +0 -1
- package/dist/esm/utils/getStaticPaths.d.mts +0 -7
- package/dist/esm/utils/getStaticPaths.d.mts.map +0 -1
- package/dist/esm/utils/hydrateRouter.d.mts +0 -69
- package/dist/esm/utils/hydrateRouter.d.mts.map +0 -1
- package/dist/esm/utils/serializeRouterState.d.mts +0 -84
- package/dist/esm/utils/serializeRouterState.d.mts.map +0 -1
- package/dist/esm/utils/serializeState.d.mts +0 -50
- package/dist/esm/utils/serializeState.d.mts.map +0 -1
- package/dist/esm/utils.d.mts +0 -6
- package/dist/esm/utils.mjs +0 -2
- package/dist/esm/utils.mjs.map +0 -1
- /package/dist/cjs/{foundation → utils}/event-emitter/EventEmitter.js +0 -0
- /package/dist/cjs/{foundation → utils}/fsm/fsm.js +0 -0
- /package/dist/cjs/{foundation → utils}/logger/RouterLogger.js +0 -0
- /package/dist/cjs/{foundation → utils}/logger/constants.js +0 -0
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("./Router-DGUed2F4.js"),t=require("./buildParamMeta-DniluAIV.js");function n(t){if(t())throw new e.n(e.T.ROUTER_DISPOSED)}function r(t){if(t())throw new e.n(e.T.REENTRANT_TREE_MUTATION)}const i=new WeakMap;function a(r){let a=i.get(r);if(a)return a;let o=t.u(r),s={makeState:(e,t,n,r)=>(o.validator?.state.validateMakeStateArgs(e,t,n),o.makeState(e,t,n,r?.params)),buildState:(e,t)=>{o.validator?.routes.validateStateBuilderArgs(e,t,`buildState`);let{name:n,params:r}=o.forwardState(e,t);return o.buildStateResolved(n,r)},forwardState:(e,t)=>(o.validator?.routes.validateStateBuilderArgs(e,t,`forwardState`),o.forwardState(e,t)),matchPath:e=>(o.validator?.routes.validateMatchPathArgs(e),o.matchPath(e,o.getOptions())),navigateToState:(e,t)=>(n(o.isDisposed),o.validator?.navigation.validateNavigateToStateArgs(e),t!==void 0&&o.validator?.navigation.validateNavigationOptions(t,`navigateToState`),o.navigateToState(e,t)),setRootPath:e=>{n(o.isDisposed),o.validator?.routes.validateSetRootPathArgs(e),o.setRootPath(e)},getRootPath:o.getRootPath,addEventListener:(e,t)=>(n(o.isDisposed),o.validator?.eventBus.validateListenerArgs(e,t),o.addEventListener(e,t)),buildNavigationState:(e,t={})=>{o.validator?.routes.validateStateBuilderArgs(e,t,`buildNavigationState`);let{name:n,params:r}=o.forwardState(e,t),i=o.buildStateResolved(n,r);if(i)return o.makeState(i.name,i.params,o.buildPath(i.name,i.params),i.meta)},getOptions:o.getOptions,getTree:o.getTree,addInterceptor:(e,t)=>{n(o.isDisposed),o.validator?.plugins.validateAddInterceptorArgs(e,t);let r=o.interceptors.get(e);r||(r=[],o.interceptors.set(e,r)),r.push(t);let i=!1;return()=>{i||(i=!0,r.splice(r.indexOf(t),1))}},getRouteConfig:e=>{let t=o.routeGetStore();if(t.matcher.hasRoute(e))return t.routeCustomFields[e]},extendRouter:t=>{n(o.isDisposed);let i=Object.keys(t);for(let t of i)if(t in r)throw new e.n(e.T.PLUGIN_CONFLICT,{message:`Cannot extend router: property "${t}" already exists`});for(let e of i)r[e]=t[e];let a={keys:i};o.routerExtensions.push(a);let s=!1;return()=>{if(s)return;s=!0;for(let e of a.keys)delete r[e];let e=o.routerExtensions.indexOf(a);e!==-1&&o.routerExtensions.splice(e,1)}},emitTransitionError:e=>{n(o.isDisposed),o.emitTransitionError(e)},claimContextNamespace:t=>{if(n(o.isDisposed),typeof t!=`string`||t===``)throw TypeError(`[claimContextNamespace] namespace must be a non-empty string, got ${typeof t==`string`?`an empty string`:typeof t}`);if(o.contextClaimRecords.has(t))throw new e.n(e.T.CONTEXT_NAMESPACE_ALREADY_CLAIMED,{message:`Cannot claim context namespace: "${t}" is already claimed by another plugin`});return o.contextClaimRecords.add(t),{write(e,n){t===`__proto__`?Object.defineProperty(e.context,t,{value:n,writable:!0,enumerable:!0,configurable:!0}):e.context[t]=n},release(){o.contextClaimRecords.delete(t)}}}};return i.set(r,s),s}function o(e){let r=t.u(e),i=r.routeGetStore().lifecycleNamespace;return{addActivateGuard(e,t){n(r.isDisposed),r.validator?.routes.validateRouteName(e,`addActivateGuard`),r.validator?.lifecycle.validateHandler(t,`addActivateGuard`),i.addCanActivate(e,t)},addDeactivateGuard(e,t){n(r.isDisposed),r.validator?.routes.validateRouteName(e,`addDeactivateGuard`),r.validator?.lifecycle.validateHandler(t,`addDeactivateGuard`),i.addCanDeactivate(e,t)},removeActivateGuard(e){n(r.isDisposed),r.validator?.routes.validateRouteName(e,`removeActivateGuard`),i.clearCanActivate(e,`external`)},removeDeactivateGuard(e){n(r.isDisposed),r.validator?.routes.validateRouteName(e,`removeDeactivateGuard`),i.clearCanDeactivate(e,`external`)}}}function s(n,r,i){let a=t.u(n);if(a.isDisposed())throw new e.n(e.T.ROUTER_DISPOSED);a.validator?.dependencies.validateCloneArgs(r);let s=a.routeGetStore(),c=e.x(s.tree),l=s.config,u=s.resolvedForwardMap,d=s.routeCustomFields,{options:f,dependencies:p,pluginFactories:m,loggerConfig:h}=a.getCloneState(),{definition:g,external:_}=s.lifecycleNamespace.getFactoriesByOrigin(),v={...p,...r},y=i?.logger?{...h,...i.logger}:h,b=new e.t(c,{...f,logger:y},v),x=t.u(b),S=x.routeGetStore(),C=S.lifecycleNamespace;e._(S.config,l),Object.assign(S.resolvedForwardMap,u),Object.assign(S.routeCustomFields,d),s.rootPath!==``&&x.setRootPath(s.rootPath);let[w,T]=g,[E,D]=_;for(let[e,t]of Object.entries(w))C.addCanDeactivate(e,t,!0);for(let[e,t]of Object.entries(T))C.addCanActivate(e,t,!0);let O=o(b);for(let[e,t]of Object.entries(E))O.addDeactivateGuard(e,t);for(let[e,t]of Object.entries(D))O.addActivateGuard(e,t);let k=new Set(x.getCloneState().pluginFactories),A=m.filter(e=>!k.has(e));return A.length>0&&b.usePlugin(...A),b}Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return n}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return s}});
|
|
2
|
-
//# sourceMappingURL=cloneRouter-Ch25z3Z2.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cloneRouter-Ch25z3Z2.js","names":["RouterError","errorCodes","getInternals","RouterError","errorCodes","getInternals","getInternals","RouterError","errorCodes","routeTreeToDefinitions","RouterClass"],"sources":["../../src/api/helpers.ts","../../src/api/getPluginApi.ts","../../src/api/getLifecycleApi.ts","../../src/api/cloneRouter.ts"],"sourcesContent":["// packages/core/src/api/helpers.ts\n\nimport { errorCodes } from \"../constants\";\nimport { RouterError } from \"../RouterError\";\n\nexport function throwIfDisposed(isDisposed: () => boolean): void {\n if (isDisposed()) {\n throw new RouterError(errorCodes.ROUTER_DISPOSED);\n }\n}\n\n/**\n * Bans synchronous reentrant route-CRUD: a CRUD op called while a `TREE_CHANGED`\n * emit is on the stack (i.e. from inside a `subscribeChanges` handler) throws\n * `REENTRANT_TREE_MUTATION` BEFORE mutating — the tree stays atomic (#1032).\n * Deferred CRUD (`queueMicrotask` / `await`) runs after the dispatch settles and\n * is unaffected; CRUD from a transition listener is not a TREE_CHANGED dispatch.\n */\nexport function throwIfReentrantTreeMutation(isEmitting: () => boolean): void {\n if (isEmitting()) {\n throw new RouterError(errorCodes.REENTRANT_TREE_MUTATION);\n }\n}\n","import { throwIfDisposed } from \"./helpers\";\nimport { errorCodes } from \"../constants\";\nimport { getInternals } from \"../internals\";\nimport { RouterError } from \"../RouterError\";\n\nimport type { PluginApi } from \"./types\";\nimport type {\n ContextNamespaceClaim,\n DefaultDependencies,\n Params,\n Router,\n State,\n} from \"../types\";\n\n// Cache the assembled PluginApi per router — mirrors getNavigator() (#525):\n// avoids re-allocating the closure-bag on each call (plugins call this once\n// at init, but tests + nested plugins poll it), and gives spy/stub helpers\n// a stable object identity to attach to (e.g. spying on\n// `getPluginApi(router).navigateToState` to inject errors in popstate\n// recovery tests).\nconst cache = new WeakMap<object, PluginApi>();\n\nexport function getPluginApi<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(router: Router<Dependencies>): PluginApi {\n const cached = cache.get(router);\n\n if (cached) {\n return cached;\n }\n\n const ctx = getInternals(router);\n const api: PluginApi = {\n makeState: (name, params, path, meta) => {\n ctx.validator?.state.validateMakeStateArgs(name, params, path);\n\n return ctx.makeState(\n name,\n params,\n path,\n meta?.params as\n Record<string, Record<string, \"url\" | \"query\">> | undefined,\n );\n },\n buildState: (routeName, routeParams) => {\n ctx.validator?.routes.validateStateBuilderArgs(\n routeName,\n routeParams,\n \"buildState\",\n );\n\n const { name, params } = ctx.forwardState(routeName, routeParams);\n\n return ctx.buildStateResolved(name, params);\n },\n forwardState: <P extends Params = Params>(\n routeName: string,\n routeParams: P,\n ) => {\n ctx.validator?.routes.validateStateBuilderArgs(\n routeName,\n routeParams,\n \"forwardState\",\n );\n\n return ctx.forwardState(routeName, routeParams);\n },\n matchPath: (path) => {\n ctx.validator?.routes.validateMatchPathArgs(path);\n\n return ctx.matchPath(path, ctx.getOptions());\n },\n navigateToState: (state, options) => {\n throwIfDisposed(ctx.isDisposed);\n\n ctx.validator?.navigation.validateNavigateToStateArgs(state);\n\n if (options !== undefined) {\n ctx.validator?.navigation.validateNavigationOptions(\n options,\n \"navigateToState\",\n );\n }\n\n return ctx.navigateToState(state, options);\n },\n setRootPath: (rootPath) => {\n throwIfDisposed(ctx.isDisposed);\n\n ctx.validator?.routes.validateSetRootPathArgs(rootPath);\n\n ctx.setRootPath(rootPath);\n },\n getRootPath: ctx.getRootPath,\n addEventListener: (eventName, cb) => {\n throwIfDisposed(ctx.isDisposed);\n\n ctx.validator?.eventBus.validateListenerArgs(eventName, cb);\n\n return ctx.addEventListener(eventName, cb);\n },\n buildNavigationState: (name, params = {}) => {\n ctx.validator?.routes.validateStateBuilderArgs(\n name,\n params,\n \"buildNavigationState\",\n );\n\n const { name: resolvedName, params: resolvedParams } = ctx.forwardState(\n name,\n params,\n );\n const routeInfo = ctx.buildStateResolved(resolvedName, resolvedParams);\n\n if (!routeInfo) {\n return;\n }\n\n return ctx.makeState(\n routeInfo.name,\n routeInfo.params,\n ctx.buildPath(routeInfo.name, routeInfo.params),\n routeInfo.meta,\n );\n },\n getOptions: ctx.getOptions,\n getTree: ctx.getTree,\n addInterceptor: (method, fn) => {\n throwIfDisposed(ctx.isDisposed);\n ctx.validator?.plugins.validateAddInterceptorArgs(method, fn);\n let list = ctx.interceptors.get(method);\n\n if (!list) {\n list = [];\n ctx.interceptors.set(method, list);\n }\n\n list.push(fn);\n\n // Idempotency flag (#1198). Without it, a double call would `indexOf(fn)`\n // again and splice a DUPLICATE registration of the same fn — silently\n // deactivating another plugin's interceptor whose own unsubscribe was never\n // called. The `Unsubscribe` contract is documented idempotent. The flag\n // guarantees exactly one splice of a still-present `fn`, so no `index !== -1`\n // guard is needed (it would be dead — the second call returns above).\n let removed = false;\n\n return () => {\n if (removed) {\n return;\n }\n\n removed = true;\n list.splice(list.indexOf(fn), 1);\n };\n },\n getRouteConfig: (name) => {\n const store = ctx.routeGetStore();\n\n // Stryker disable next-line ConditionalExpression,BlockStatement: equivalent — a missing route yields routeCustomFields[name] === undefined, identical to this early return\n if (!store.matcher.hasRoute(name)) {\n return;\n }\n\n return store.routeCustomFields[name];\n },\n extendRouter: (extensions: Record<string, unknown>) => {\n throwIfDisposed(ctx.isDisposed);\n\n const keys = Object.keys(extensions);\n\n for (const key of keys) {\n if (key in router) {\n throw new RouterError(errorCodes.PLUGIN_CONFLICT, {\n message: `Cannot extend router: property \"${key}\" already exists`,\n });\n }\n }\n\n for (const key of keys) {\n (router as Record<string, unknown>)[key] = extensions[key];\n }\n\n const extensionRecord = { keys };\n\n ctx.routerExtensions.push(extensionRecord);\n\n let removed = false;\n\n return () => {\n if (removed) {\n return;\n }\n\n removed = true;\n\n for (const key of extensionRecord.keys) {\n delete (router as Record<string, unknown>)[key];\n }\n\n const idx = ctx.routerExtensions.indexOf(extensionRecord);\n\n // Stryker disable next-line ConditionalExpression,EqualityOperator,UnaryOperator,BlockStatement: equivalent — this splice only tidies the `routerExtensions` TRACKING array; the router INSTANCE is cleaned by the `delete router[key]` loop above, and dispose()'s safety-net re-deletes any leaked key harmlessly. So no mutation of this guard/splice is behaviourally observable (full suite green with `===`, `+1`, and an empty body). Contrast the addInterceptor splice, which IS observable through buildPath and is killed behaviourally by invariantGuardMutants.test.ts.\n if (idx !== -1) {\n ctx.routerExtensions.splice(idx, 1);\n }\n };\n },\n emitTransitionError: (error) => {\n throwIfDisposed(ctx.isDisposed);\n ctx.emitTransitionError(error);\n },\n claimContextNamespace: (namespace: string) => {\n throwIfDisposed(ctx.isDisposed);\n\n // Input-shape guard, symmetric with the other always-on invariant guards\n // (subscribe / start / navigateToNotFound each typeof-check their input).\n // A non-string namespace coerces to an inconsistent key (\"42\"); an empty\n // string is a meaningless namespace (#1191 N4).\n if (typeof namespace !== \"string\" || namespace === \"\") {\n throw new TypeError(\n `[claimContextNamespace] namespace must be a non-empty string, got ${\n typeof namespace === \"string\" ? \"an empty string\" : typeof namespace\n }`,\n );\n }\n\n if (ctx.contextClaimRecords.has(namespace)) {\n throw new RouterError(errorCodes.CONTEXT_NAMESPACE_ALREADY_CLAIMED, {\n message: `Cannot claim context namespace: \"${namespace}\" is already claimed by another plugin`,\n });\n }\n\n ctx.contextClaimRecords.add(namespace);\n\n return {\n write(state: State, value: unknown) {\n // `state.context[namespace] = value` dispatches into the inherited\n // Object.prototype.__proto__ setter for the literal key \"__proto__\",\n // swapping the prototype instead of creating an own entry — the data\n // then vanishes from Object.keys / serializeRouterState (#1191 N3).\n // Mirror search-params' assignParam: defineProperty writes a genuine\n // own property; normal names keep the plain-assignment fast path.\n if (namespace === \"__proto__\") {\n Object.defineProperty(state.context, namespace, {\n value,\n writable: true,\n enumerable: true,\n configurable: true,\n });\n } else {\n state.context[namespace] = value;\n }\n },\n release() {\n ctx.contextClaimRecords.delete(namespace);\n },\n } satisfies ContextNamespaceClaim;\n },\n };\n\n cache.set(router, api);\n\n return api;\n}\n","import { throwIfDisposed } from \"./helpers\";\nimport { getInternals } from \"../internals\";\n\nimport type { LifecycleApi } from \"./types\";\nimport type { DefaultDependencies, Router } from \"../types\";\n\nexport function getLifecycleApi<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(router: Router<Dependencies>): LifecycleApi<Dependencies> {\n const ctx = getInternals(router);\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guaranteed set after wiring\n const lifecycleNamespace = ctx.routeGetStore().lifecycleNamespace!;\n\n return {\n addActivateGuard(name, handler) {\n throwIfDisposed(ctx.isDisposed);\n\n ctx.validator?.routes.validateRouteName(name, \"addActivateGuard\");\n ctx.validator?.lifecycle.validateHandler(handler, \"addActivateGuard\");\n\n // Handler-limit enforcement lives at the namespace registration choke point\n // (RouteLifecycleNamespace.#registerHandler) so all paths are bounded\n // uniformly — see #961.\n lifecycleNamespace.addCanActivate(name, handler);\n },\n\n addDeactivateGuard(name, handler) {\n throwIfDisposed(ctx.isDisposed);\n\n ctx.validator?.routes.validateRouteName(name, \"addDeactivateGuard\");\n ctx.validator?.lifecycle.validateHandler(handler, \"addDeactivateGuard\");\n\n lifecycleNamespace.addCanDeactivate(name, handler);\n },\n\n removeActivateGuard(name) {\n throwIfDisposed(ctx.isDisposed);\n\n ctx.validator?.routes.validateRouteName(name, \"removeActivateGuard\");\n\n // Inverse of addActivateGuard (external): clears only the external guard;\n // a route-config (definition) canActivate survives (#1171).\n lifecycleNamespace.clearCanActivate(name, \"external\");\n },\n\n removeDeactivateGuard(name) {\n throwIfDisposed(ctx.isDisposed);\n\n ctx.validator?.routes.validateRouteName(name, \"removeDeactivateGuard\");\n\n // Inverse of addDeactivateGuard (external): clears only the external guard;\n // a route-config (definition) canDeactivate survives (#1171).\n lifecycleNamespace.clearCanDeactivate(name, \"external\");\n },\n };\n}\n","import { errorCodes } from \"../constants\";\nimport { routeTreeToDefinitions } from \"../engine\";\nimport { getInternals } from \"../internals\";\nimport { getLifecycleApi } from \"./getLifecycleApi\";\nimport { assignConfigEntries } from \"../namespaces/RoutesNamespace/helpers\";\nimport { Router as RouterClass } from \"../Router\";\nimport { RouterError } from \"../RouterError\";\n\nimport type {\n DefaultDependencies,\n LoggerConfig,\n Router,\n Route,\n} from \"../types\";\n\n/**\n * Per-clone overrides beyond dependencies.\n */\nexport interface CloneOptions {\n /**\n * Per-clone logger config override, merged **over** the base router's resolved\n * logger config. Primary use: per-request `traceId` in SSR — a fresh\n * `callback` closed over the request id, while `level` inherits the base.\n * Omitted keys inherit the base (level / callback / callbackIgnoresLevel).\n *\n * Override is by **config**, not a logger instance: `RouterLogger` is\n * core-internal (only its `{ log, warn, error }` interface is public), so\n * nothing outside core constructs one — configuration is the whole surface.\n */\n logger?: Partial<LoggerConfig>;\n}\n\n/**\n * Build an independent router instance that shares the route tree, options,\n * lifecycle guards, and plugin factories of `router`. The primary use case\n * is **SSR multi-tenancy** — one base router per process, one clone per\n * request.\n *\n * @param router - Source router (must not be disposed).\n * @param dependencies - Optional per-clone overrides merged on top of the\n * base router's dependencies. Always **fresh per call** in the documented\n * SSR pattern: pass per-request state here, never store it in the base.\n *\n * @remarks\n *\n * **Dependency merge — shallow by design.** `base.dependencies` are spread\n * into the clone via `{ ...sourceDeps, ...dependencies }`. Top-level keys\n * are new objects, but **values are shared by reference**: a `Map`, `Set`,\n * class instance, function, or nested plain object stored in\n * `base.dependencies` is the **same instance** in every clone. Mutations\n * in one clone are visible in the base and in every sibling clone.\n *\n * This is intentional. `structuredClone` of dep values is **not** applied\n * because it would:\n * - strip class prototypes (`new DbClient()` → plain object, methods lost)\n * - reject functions and symbols (`DataCloneError`)\n * - fragment singleton pools (one connection pool per request — pool\n * semantics destroyed)\n * - reject circular references\n *\n * **SSR rule of thumb.** Place values in `base.dependencies` according to\n * their lifecycle:\n *\n * - **Singletons / shared services** → `base.dependencies`. Examples: DB\n * client, connection pool, logger, config, feature-flag client. Process-\n * wide pooling depends on sharing these by reference.\n * - **Per-request state** → the `dependencies` override parameter (or\n * `createRequestScope`'s `deps` argument). Examples: `currentUser`,\n * `traceId`, `sessionId`, `abortSignal`. The override is applied last,\n * so it wins over base keys; pass a fresh object per call.\n *\n * Cross-request data leaks are **only possible** when per-request mutable\n * state is incorrectly placed in `base.dependencies`. The override slot is\n * the safe channel.\n *\n * @example\n * ```typescript\n * // Server boot — singletons only\n * const base = createRouter(routes, options, {\n * db: new DbClient(dbUrl),\n * logger,\n * });\n *\n * // Per request — fresh override per call\n * const clone = cloneRouter(base, {\n * currentUser,\n * traceId,\n * });\n * // clone.deps.db === base.deps.db ✓ shared pool (intentional)\n * // clone.deps.currentUser ✓ unique per request\n * ```\n *\n * @see createRequestScope — `@real-router/core/utils` SSR helper that\n * wraps this function and injects `abortSignal` automatically.\n */\nexport function cloneRouter<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n router: Router<Dependencies>,\n dependencies?: Dependencies,\n opts?: CloneOptions,\n): RouterClass<Dependencies> {\n const ctx = getInternals(router);\n\n if (ctx.isDisposed()) {\n throw new RouterError(errorCodes.ROUTER_DISPOSED);\n }\n\n ctx.validator?.dependencies.validateCloneArgs(dependencies);\n\n // Get source store directly\n const sourceStore = ctx.routeGetStore();\n const routes = routeTreeToDefinitions(sourceStore.tree);\n const routeConfig = sourceStore.config;\n const resolvedForwardMap = sourceStore.resolvedForwardMap;\n const routeCustomFields = sourceStore.routeCustomFields;\n\n const {\n options,\n dependencies: sourceDeps,\n pluginFactories,\n loggerConfig,\n } = ctx.getCloneState();\n // Origin-aware factory snapshot — definition guards are re-registered with\n // `isFromDefinition=true` on the clone so `replace()` can still strip them\n // via `clearDefinitionGuards()`. External guards take the public lifecycle\n // API path so they survive `replace()` symmetric with the base.\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guaranteed set after wiring\n const sourceLifecycleNamespace = sourceStore.lifecycleNamespace!;\n const { definition: definitionFactories, external: externalFactories } =\n sourceLifecycleNamespace.getFactoriesByOrigin();\n\n const mergedDeps = {\n ...sourceDeps,\n ...dependencies,\n } as Dependencies;\n\n // The clone builds its OWN logger (isolation, #724) but INHERITS the base's\n // resolved config — frozen options don't carry `logger`, so without this the\n // clone would fall back to the default logger and lose the base's\n // callback/level (an M1 regression the singleton used to mask). A per-request\n // `opts.logger` override (e.g. a traceId-bound callback) merges on top.\n const clonedLoggerConfig: Partial<LoggerConfig> = opts?.logger\n ? { ...loggerConfig, ...opts.logger }\n : loggerConfig;\n\n const newRouter = new RouterClass<Dependencies>(\n routes as Route<Dependencies>[],\n { ...options, logger: clonedLoggerConfig },\n mergedDeps,\n );\n\n const newCtx = getInternals(newRouter);\n const newStore = newCtx.routeGetStore();\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guaranteed set after wiring\n const newLifecycleNamespace = newStore.lifecycleNamespace!;\n\n // Copy the source config + store-level maps BEFORE re-registering guards\n // (#1331 review): the definition-guard factories re-executed below must\n // observe the fully-built clone (encoders/decoders/defaultParams/custom\n // fields), mirroring the constructor where flushPendingGuards runs after the\n // store is complete. The five RouteConfig sub-maps go through a single\n // enumeration so a newly added config field is carried over automatically\n // (#965); resolvedForwardMap and routeCustomFields are store-level (not part\n // of RouteConfig) and stay explicit.\n assignConfigEntries(newStore.config, routeConfig);\n Object.assign(newStore.resolvedForwardMap, resolvedForwardMap);\n Object.assign(newStore.routeCustomFields, routeCustomFields);\n\n // #1175: carry the source rootPath. It lives in the store (not options/config),\n // and neither routeTreeToDefinitions nor getCloneState include it — so a clone\n // of a base configured with `setRootPath(\"/app\")` would otherwise build/match\n // under \"\" and 404 every request of a sub-path SSR deployment. setRootPath\n // rebuilds the tree in place with the just-copied config; the rebuild is only\n // paid when a rootPath is actually set, and it runs before the definition-guard\n // factories below so they observe the fully-built clone (rootPath included).\n if (sourceStore.rootPath !== \"\") {\n newCtx.setRootPath(sourceStore.rootPath);\n }\n\n const [definitionDeactivate, definitionActivate] = definitionFactories;\n const [externalDeactivate, externalActivate] = externalFactories;\n\n for (const [name, handler] of Object.entries(definitionDeactivate)) {\n newLifecycleNamespace.addCanDeactivate(name, handler, true);\n }\n\n for (const [name, handler] of Object.entries(definitionActivate)) {\n newLifecycleNamespace.addCanActivate(name, handler, true);\n }\n\n const lifecycle = getLifecycleApi(newRouter);\n\n for (const [name, handler] of Object.entries(externalDeactivate)) {\n lifecycle.addDeactivateGuard(name, handler);\n }\n\n for (const [name, handler] of Object.entries(externalActivate)) {\n lifecycle.addActivateGuard(name, handler);\n }\n\n // Plugin replay runs last and skips factories that a (contract-violating)\n // definition-guard factory already registered on the clone during the\n // re-compilation above — without the filter every clone would double-apply\n // such a plugin: once via the factory, once via this replay (#1331 review).\n const alreadyRegistered = new Set(newCtx.getCloneState().pluginFactories);\n const pluginsToReplay = pluginFactories.filter(\n (factory) => !alreadyRegistered.has(factory),\n );\n\n // Stryker disable next-line EqualityOperator: equivalent — `>= 0` is always true, but `usePlugin(...[])` with an empty spread is a no-op, so entering the block on an empty list behaves identically to skipping it. (ConditionalExpression stays live: `→false` skips a real plugin list and is killable.)\n if (pluginsToReplay.length > 0) {\n newRouter.usePlugin(...pluginsToReplay);\n }\n\n return newRouter;\n}\n"],"mappings":"kFAKA,SAAgB,EAAgB,EAAiC,CAC/D,GAAI,EAAW,EACb,MAAM,IAAIA,EAAAA,EAAYC,EAAAA,EAAW,eAAe,CAEpD,CASA,SAAgB,EAA6B,EAAiC,CAC5E,GAAI,EAAW,EACb,MAAM,IAAID,EAAAA,EAAYC,EAAAA,EAAW,uBAAuB,CAE5D,CCFA,MAAM,EAAQ,IAAI,QAElB,SAAgB,EAEd,EAAyC,CACzC,IAAM,EAAS,EAAM,IAAI,CAAM,EAE/B,GAAI,EACF,OAAO,EAGT,IAAM,EAAMC,EAAAA,EAAa,CAAM,EACzB,EAAiB,CACrB,WAAY,EAAM,EAAQ,EAAM,KAC9B,EAAI,WAAW,MAAM,sBAAsB,EAAM,EAAQ,CAAI,EAEtD,EAAI,UACT,EACA,EACA,EACA,GAAM,MAER,GAEF,YAAa,EAAW,IAAgB,CACtC,EAAI,WAAW,OAAO,yBACpB,EACA,EACA,YACF,EAEA,GAAM,CAAE,OAAM,UAAW,EAAI,aAAa,EAAW,CAAW,EAEhE,OAAO,EAAI,mBAAmB,EAAM,CAAM,CAC5C,EACA,cACE,EACA,KAEA,EAAI,WAAW,OAAO,yBACpB,EACA,EACA,cACF,EAEO,EAAI,aAAa,EAAW,CAAW,GAEhD,UAAY,IACV,EAAI,WAAW,OAAO,sBAAsB,CAAI,EAEzC,EAAI,UAAU,EAAM,EAAI,WAAW,CAAC,GAE7C,iBAAkB,EAAO,KACvB,EAAgB,EAAI,UAAU,EAE9B,EAAI,WAAW,WAAW,4BAA4B,CAAK,EAEvD,IAAY,IAAA,IACd,EAAI,WAAW,WAAW,0BACxB,EACA,iBACF,EAGK,EAAI,gBAAgB,EAAO,CAAO,GAE3C,YAAc,GAAa,CACzB,EAAgB,EAAI,UAAU,EAE9B,EAAI,WAAW,OAAO,wBAAwB,CAAQ,EAEtD,EAAI,YAAY,CAAQ,CAC1B,EACA,YAAa,EAAI,YACjB,kBAAmB,EAAW,KAC5B,EAAgB,EAAI,UAAU,EAE9B,EAAI,WAAW,SAAS,qBAAqB,EAAW,CAAE,EAEnD,EAAI,iBAAiB,EAAW,CAAE,GAE3C,sBAAuB,EAAM,EAAS,CAAC,IAAM,CAC3C,EAAI,WAAW,OAAO,yBACpB,EACA,EACA,sBACF,EAEA,GAAM,CAAE,KAAM,EAAc,OAAQ,GAAmB,EAAI,aACzD,EACA,CACF,EACM,EAAY,EAAI,mBAAmB,EAAc,CAAc,EAEhE,KAIL,OAAO,EAAI,UACT,EAAU,KACV,EAAU,OACV,EAAI,UAAU,EAAU,KAAM,EAAU,MAAM,EAC9C,EAAU,IACZ,CACF,EACA,WAAY,EAAI,WAChB,QAAS,EAAI,QACb,gBAAiB,EAAQ,IAAO,CAC9B,EAAgB,EAAI,UAAU,EAC9B,EAAI,WAAW,QAAQ,2BAA2B,EAAQ,CAAE,EAC5D,IAAI,EAAO,EAAI,aAAa,IAAI,CAAM,EAEjC,IACH,EAAO,CAAC,EACR,EAAI,aAAa,IAAI,EAAQ,CAAI,GAGnC,EAAK,KAAK,CAAE,EAQZ,IAAI,EAAU,GAEd,UAAa,CACP,IAIJ,EAAU,GACV,EAAK,OAAO,EAAK,QAAQ,CAAE,EAAG,CAAC,EACjC,CACF,EACA,eAAiB,GAAS,CACxB,IAAM,EAAQ,EAAI,cAAc,EAG3B,KAAM,QAAQ,SAAS,CAAI,EAIhC,OAAO,EAAM,kBAAkB,EACjC,EACA,aAAe,GAAwC,CACrD,EAAgB,EAAI,UAAU,EAE9B,IAAM,EAAO,OAAO,KAAK,CAAU,EAEnC,IAAK,IAAM,KAAO,EAChB,GAAI,KAAO,EACT,MAAM,IAAIC,EAAAA,EAAYC,EAAAA,EAAW,gBAAiB,CAChD,QAAS,mCAAmC,EAAI,iBAClD,CAAC,EAIL,IAAK,IAAM,KAAO,EAChB,EAAoC,GAAO,EAAW,GAGxD,IAAM,EAAkB,CAAE,MAAK,EAE/B,EAAI,iBAAiB,KAAK,CAAe,EAEzC,IAAI,EAAU,GAEd,UAAa,CACX,GAAI,EACF,OAGF,EAAU,GAEV,IAAK,IAAM,KAAO,EAAgB,KAChC,OAAQ,EAAmC,GAG7C,IAAM,EAAM,EAAI,iBAAiB,QAAQ,CAAe,EAGpD,IAAQ,IACV,EAAI,iBAAiB,OAAO,EAAK,CAAC,CAEtC,CACF,EACA,oBAAsB,GAAU,CAC9B,EAAgB,EAAI,UAAU,EAC9B,EAAI,oBAAoB,CAAK,CAC/B,EACA,sBAAwB,GAAsB,CAO5C,GANA,EAAgB,EAAI,UAAU,EAM1B,OAAO,GAAc,UAAY,IAAc,GACjD,MAAU,UACR,qEACE,OAAO,GAAc,SAAW,kBAAoB,OAAO,GAE/D,EAGF,GAAI,EAAI,oBAAoB,IAAI,CAAS,EACvC,MAAM,IAAID,EAAAA,EAAYC,EAAAA,EAAW,kCAAmC,CAClE,QAAS,oCAAoC,EAAU,uCACzD,CAAC,EAKH,OAFA,EAAI,oBAAoB,IAAI,CAAS,EAE9B,CACL,MAAM,EAAc,EAAgB,CAO9B,IAAc,YAChB,OAAO,eAAe,EAAM,QAAS,EAAW,CAC9C,QACA,SAAU,GACV,WAAY,GACZ,aAAc,EAChB,CAAC,EAED,EAAM,QAAQ,GAAa,CAE/B,EACA,SAAU,CACR,EAAI,oBAAoB,OAAO,CAAS,CAC1C,CACF,CACF,CACF,EAIA,OAFA,EAAM,IAAI,EAAQ,CAAG,EAEd,CACT,CClQA,SAAgB,EAEd,EAA0D,CAC1D,IAAM,EAAMC,EAAAA,EAAa,CAAM,EAEzB,EAAqB,EAAI,cAAc,CAAC,CAAC,mBAE/C,MAAO,CACL,iBAAiB,EAAM,EAAS,CAC9B,EAAgB,EAAI,UAAU,EAE9B,EAAI,WAAW,OAAO,kBAAkB,EAAM,kBAAkB,EAChE,EAAI,WAAW,UAAU,gBAAgB,EAAS,kBAAkB,EAKpE,EAAmB,eAAe,EAAM,CAAO,CACjD,EAEA,mBAAmB,EAAM,EAAS,CAChC,EAAgB,EAAI,UAAU,EAE9B,EAAI,WAAW,OAAO,kBAAkB,EAAM,oBAAoB,EAClE,EAAI,WAAW,UAAU,gBAAgB,EAAS,oBAAoB,EAEtE,EAAmB,iBAAiB,EAAM,CAAO,CACnD,EAEA,oBAAoB,EAAM,CACxB,EAAgB,EAAI,UAAU,EAE9B,EAAI,WAAW,OAAO,kBAAkB,EAAM,qBAAqB,EAInE,EAAmB,iBAAiB,EAAM,UAAU,CACtD,EAEA,sBAAsB,EAAM,CAC1B,EAAgB,EAAI,UAAU,EAE9B,EAAI,WAAW,OAAO,kBAAkB,EAAM,uBAAuB,EAIrE,EAAmB,mBAAmB,EAAM,UAAU,CACxD,CACF,CACF,CCwCA,SAAgB,EAGd,EACA,EACA,EAC2B,CAC3B,IAAM,EAAMC,EAAAA,EAAa,CAAM,EAE/B,GAAI,EAAI,WAAW,EACjB,MAAM,IAAIC,EAAAA,EAAYC,EAAAA,EAAW,eAAe,EAGlD,EAAI,WAAW,aAAa,kBAAkB,CAAY,EAG1D,IAAM,EAAc,EAAI,cAAc,EAChC,EAASC,EAAAA,EAAuB,EAAY,IAAI,EAChD,EAAc,EAAY,OAC1B,EAAqB,EAAY,mBACjC,EAAoB,EAAY,kBAEhC,CACJ,UACA,aAAc,EACd,kBACA,gBACE,EAAI,cAAc,EAOhB,CAAE,WAAY,EAAqB,SAAU,GADlB,EAAY,mBAElB,qBAAqB,EAE1C,EAAa,CACjB,GAAG,EACH,GAAG,CACL,EAOM,EAA4C,GAAM,OACpD,CAAE,GAAG,EAAc,GAAG,EAAK,MAAO,EAClC,EAEE,EAAY,IAAIC,EAAAA,EACpB,EACA,CAAE,GAAG,EAAS,OAAQ,CAAmB,EACzC,CACF,EAEM,EAASJ,EAAAA,EAAa,CAAS,EAC/B,EAAW,EAAO,cAAc,EAEhC,EAAwB,EAAS,mBAUvC,EAAA,EAAoB,EAAS,OAAQ,CAAW,EAChD,OAAO,OAAO,EAAS,mBAAoB,CAAkB,EAC7D,OAAO,OAAO,EAAS,kBAAmB,CAAiB,EASvD,EAAY,WAAa,IAC3B,EAAO,YAAY,EAAY,QAAQ,EAGzC,GAAM,CAAC,EAAsB,GAAsB,EAC7C,CAAC,EAAoB,GAAoB,EAE/C,IAAK,GAAM,CAAC,EAAM,KAAY,OAAO,QAAQ,CAAoB,EAC/D,EAAsB,iBAAiB,EAAM,EAAS,EAAI,EAG5D,IAAK,GAAM,CAAC,EAAM,KAAY,OAAO,QAAQ,CAAkB,EAC7D,EAAsB,eAAe,EAAM,EAAS,EAAI,EAG1D,IAAM,EAAY,EAAgB,CAAS,EAE3C,IAAK,GAAM,CAAC,EAAM,KAAY,OAAO,QAAQ,CAAkB,EAC7D,EAAU,mBAAmB,EAAM,CAAO,EAG5C,IAAK,GAAM,CAAC,EAAM,KAAY,OAAO,QAAQ,CAAgB,EAC3D,EAAU,iBAAiB,EAAM,CAAO,EAO1C,IAAM,EAAoB,IAAI,IAAI,EAAO,cAAc,CAAC,CAAC,eAAe,EAClE,EAAkB,EAAgB,OACrC,GAAY,CAAC,EAAkB,IAAI,CAAO,CAC7C,EAOA,OAJI,EAAgB,OAAS,GAC3B,EAAU,UAAU,GAAG,CAAe,EAGjC,CACT"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"EventEmitter.js","names":["#callbacks","#dispatching","#onListenerError","#onListenerWarn","#limits","#warnedEvents","#invokeIsolated","#callListener"],"sources":["../../../../src/foundation/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":"AAMA,MAAM,EAAqC,CACzC,aAAc,EACd,cAAe,CACjB,EAWA,IAAa,EAAb,KAAuE,CACrE,GAAsB,IAAI,IAK1B,GAAwB,IAAI,IAC5B,GAAoC,KACpC,GAA8B,EAC9B,GAEA,GAEA,YAAY,EAA+B,CACrC,GAAS,SACX,KAAKI,GAAU,EAAQ,QAGzB,KAAKF,GAAmB,GAAS,iBAAmB,KACpD,KAAKC,GAAkB,GAAS,gBAAkB,IACpD,CAKA,OAAO,iBACL,EACA,EAEwB,CACxB,GAAI,OAAO,GAAO,WAChB,MAAU,UACR,gDAAgD,GAClD,CAEJ,CAKA,UAAU,EAAkC,CAC1C,KAAKC,GAAU,CACjB,CAaA,GACE,EACA,EACa,CACb,IAAM,EAAW,KAAKJ,GAAW,IAAI,CAAS,EACxC,EAAO,GAAU,MAAQ,EAE/B,GAAI,GAAU,IAAI,CAAE,EAClB,MAAU,MAAM,2BAA2B,EAAU,EAAE,EAGzD,GAAM,CAAE,eAAc,iBAAkB,KAAKI,GAI7C,GAAI,IAAiB,GAAK,GAAQ,EAChC,MAAU,MACR,mBAAmB,EAAa,iBAAiB,EAAU,EAC7D,EAUA,IAAkB,GAClB,IAAS,GACT,KAAKD,KAAoB,OAEzB,KAAKE,KAAkB,IAAI,IAEtB,KAAKA,GAAc,IAAI,CAAS,IACnC,KAAKF,GAAgB,EAAW,CAAa,EAC7C,KAAKE,GAAc,IAAI,CAAS,IAMpC,IAAI,EAAM,EASV,OAPI,IAAQ,IAAA,KACV,EAAM,IAAI,IACV,KAAKL,GAAW,IAAI,EAAW,CAAG,GAGpC,EAAI,IAAI,CAAE,MAEG,CACX,KAAK,IAAI,EAAW,CAAE,CACxB,CACF,CAKA,IACE,EACA,EACM,CACN,IAAM,EAAM,KAAKA,GAAW,IAAI,CAAS,EAEpC,IAIL,EAAI,OAAO,CAAE,EAET,EAAI,OAAS,IAIf,KAAKA,GAAW,OAAO,CAAS,EAChC,KAAKK,IAAe,OAAO,CAAS,GAExC,CAgBA,KACE,EACA,EACA,EACA,EACA,EACM,CACN,IAAM,EAAM,KAAKL,GAAW,IAAI,CAAS,EAOzC,GALI,CAAC,GAAO,EAAI,OAAS,GAKrB,KAAKC,GAAa,IAAI,CAAS,EACjC,OAIF,IAAM,EAAO,UAAU,OAAS,EAEhC,KAAKA,GAAa,IAAI,CAAS,EAE/B,GAAI,CAEF,GAAI,EAAI,OAAS,EAAG,CAClB,GAAM,CAAC,GAAM,EAEb,KAAKK,GAAgB,EAAW,EAAI,EAAM,EAAM,EAAM,EAAM,CAAI,CAClE,KAAO,CACL,IAAM,EAAY,CAAC,GAAG,CAAG,EAEzB,IAAK,IAAM,KAAM,EACf,KAAKA,GAAgB,EAAW,EAAI,EAAM,EAAM,EAAM,EAAM,CAAI,CAEpE,CACF,QAAU,CACR,KAAKL,GAAa,OAAO,CAAS,CACpC,CACF,CAaA,UAAiB,CACf,KAAKD,GAAW,MAAM,EACtB,KAAKK,GAAgB,IACvB,CAKA,cAAc,EAA6C,CACzD,OAAO,KAAKL,GAAW,IAAI,CAAS,CAAC,EAAE,MAAQ,CACjD,CAQA,cAAc,EAA8C,CAC1D,OAAO,KAAKC,GAAa,IAAI,CAAS,CACxC,CAUA,GACE,EACA,EACA,EACA,EACA,EACA,EACA,EACM,CACN,GAAI,CACF,IAAM,EAAS,KAAKM,GAAc,EAAI,EAAM,EAAM,EAAM,EAAM,CAAI,EAUhE,GAAW,MAEX,OAAQ,EAAgC,MAAS,YAEjD,QAAQ,QAAQ,CAA8B,CAAC,CAAC,MAC7C,GAAmB,CAClB,KAAKL,KAAmB,EAAW,CAAK,CAC1C,CACF,CAEJ,OAAS,EAAO,CACd,KAAKA,KAAmB,EAAW,CAAK,CAC1C,CACF,CAEA,GACE,EACA,EACA,EACA,EACA,EACA,EACS,CACT,OAAQ,EAAR,CACE,IAAK,GACH,OAAQ,EAAqB,EAE/B,IAAK,GACH,OAAQ,EAA+B,CAAI,EAE7C,IAAK,GACH,OAAQ,EAA2C,EAAM,CAAI,EAE/D,IAAK,GACH,OAAQ,EACN,EACA,EACA,CACF,EAEF,QACE,OACE,EACA,EAAM,EAAM,EAAM,CAAI,CAE5B,CACF,CAIF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fsm.js","names":["#context","#transitions","#listeners","#state","#currentTransitions","#actions","#listenerCount"],"sources":["../../../../src/foundation/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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"RouterLogger.js","names":["#config","LEVEL_CONFIGS","#currentThreshold","#writeLog","LOG_LEVELS","#writeToConsole","#invokeCallback","#inCallback","#reportError"],"sources":["../../../../src/foundation/logger/RouterLogger.ts"],"sourcesContent":["// packages/core/src/foundation/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/foundation/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,111 +0,0 @@
|
|
|
1
|
-
import { DefaultDependencies, Router } from "../types/router.js";
|
|
2
|
-
import { Router as Router$1 } from "../Router.js";
|
|
3
|
-
//#region src/utils/createRequestScope.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* Subset of Node's `http.IncomingMessage` that `createRequestScope` relies on:
|
|
6
|
-
* a `"close"` event indicating that the client disconnected (or the response
|
|
7
|
-
* was fully sent) and the standard `removeListener` cleanup hook.
|
|
8
|
-
*/
|
|
9
|
-
interface IncomingMessageLike {
|
|
10
|
-
on: (event: "close", listener: () => void) => unknown;
|
|
11
|
-
removeListener?: (event: "close", listener: () => void) => unknown;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Web `Request`-shaped object — anything carrying an `AbortSignal`. Web
|
|
15
|
-
* runtimes (Bun, Cloudflare Workers, Vite RSC) surface client-disconnect via
|
|
16
|
-
* `request.signal` directly, so no listener attachment is needed.
|
|
17
|
-
*/
|
|
18
|
-
interface RequestLike {
|
|
19
|
-
signal: AbortSignal;
|
|
20
|
-
}
|
|
21
|
-
type RequestScopeSource = IncomingMessageLike | RequestLike;
|
|
22
|
-
interface RequestScope<Dependencies extends DefaultDependencies = DefaultDependencies> extends AsyncDisposable {
|
|
23
|
-
/**
|
|
24
|
-
* Per-request router clone. Carries `abortSignal` injected into its
|
|
25
|
-
* dependencies — loaders can `getDep("abortSignal")` and pass it to fetch /
|
|
26
|
-
* `withTimeout` for cooperative cancellation when the client disconnects.
|
|
27
|
-
*/
|
|
28
|
-
readonly router: Router$1<Dependencies>;
|
|
29
|
-
/**
|
|
30
|
-
* Aborts when the request closes (Node `IncomingMessage`'s `"close"` event)
|
|
31
|
-
* or when the upstream Web `Request.signal` aborts.
|
|
32
|
-
*/
|
|
33
|
-
readonly signal: AbortSignal;
|
|
34
|
-
/**
|
|
35
|
-
* Detach the close listener (if attached to a Node `IncomingMessage`) and
|
|
36
|
-
* dispose the cloned router. Idempotent — safe to call multiple times or in
|
|
37
|
-
* combination with `Symbol.asyncDispose`.
|
|
38
|
-
*/
|
|
39
|
-
dispose: () => Promise<void>;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Build a per-request router scope: clones `base`, attaches an `AbortSignal`
|
|
43
|
-
* tied to the request's lifetime, and exposes `dispose()` (plus
|
|
44
|
-
* `Symbol.asyncDispose` for `await using` declarations).
|
|
45
|
-
*
|
|
46
|
-
* Replaces the four-step boilerplate that every server entry repeats:
|
|
47
|
-
*
|
|
48
|
-
* 1. `new AbortController()` per request
|
|
49
|
-
* 2. `req.on("close", () => controller.abort())`
|
|
50
|
-
* 3. `cloneRouter(base, { ...deps, abortSignal: signal })`
|
|
51
|
-
* 4. `try { ... } finally { router.dispose() }`
|
|
52
|
-
*
|
|
53
|
-
* The signal is injected into the router clone under `abortSignal` so existing
|
|
54
|
-
* loaders that read `getDep("abortSignal")` keep working without changes. If
|
|
55
|
-
* `deps` already carries an `abortSignal`, the request-tied signal **wins** (it
|
|
56
|
-
* is spread last) — a caller-supplied `abortSignal` is intentionally overridden,
|
|
57
|
-
* since the scope's purpose is to own the request-lifecycle signal.
|
|
58
|
-
*
|
|
59
|
-
* ## `await using` compatibility
|
|
60
|
-
*
|
|
61
|
-
* The scope implements `Symbol.asyncDispose`, so `await using scope = …` is
|
|
62
|
-
* supported on runtimes that ship the well-known `Symbol.asyncDispose`:
|
|
63
|
-
*
|
|
64
|
-
* - **Node.js 24+** (full support; partial in 20.4–20.17 only for `fs`/`stream`)
|
|
65
|
-
* - **Bun 1.0.23+**, **Deno 1.37+**
|
|
66
|
-
* - **Chrome / Edge 127+**, **Firefox 141+**
|
|
67
|
-
* - **Safari**: not yet supported (irrelevant in practice — this helper is
|
|
68
|
-
* server-side only and never reaches the browser)
|
|
69
|
-
*
|
|
70
|
-
* On Node.js 22 LTS the well-known symbol is unavailable, so `await using`
|
|
71
|
-
* fails. **The bundled SSR examples therefore use the explicit
|
|
72
|
-
* `try/finally` + `await scope.dispose()` form**, which works on every
|
|
73
|
-
* runtime. Use `await using` only when you control the deployment target and
|
|
74
|
-
* know it ships the symbol.
|
|
75
|
-
*
|
|
76
|
-
* @example
|
|
77
|
-
* ```typescript
|
|
78
|
-
* // Explicit dispose — works on Node 18+, all browsers, every CI image
|
|
79
|
-
* export async function render(url: string, req: IncomingMessage) {
|
|
80
|
-
* const scope = createRequestScope(req, baseRouter, { currentUser });
|
|
81
|
-
* try {
|
|
82
|
-
* scope.router.usePlugin(ssrDataPluginFactory(loaders));
|
|
83
|
-
* return await renderShell(scope.router, url);
|
|
84
|
-
* } finally {
|
|
85
|
-
* await scope.dispose();
|
|
86
|
-
* }
|
|
87
|
-
* }
|
|
88
|
-
*
|
|
89
|
-
* // `await using` — Node 24+, Bun, Deno, modern browsers
|
|
90
|
-
* export async function render(url: string, req: IncomingMessage) {
|
|
91
|
-
* await using scope = createRequestScope(req, baseRouter, { currentUser });
|
|
92
|
-
* scope.router.usePlugin(ssrDataPluginFactory(loaders));
|
|
93
|
-
* return await renderShell(scope.router, url);
|
|
94
|
-
* }
|
|
95
|
-
*
|
|
96
|
-
* // Web runtime (signal already on the request)
|
|
97
|
-
* async function handler(request: Request) {
|
|
98
|
-
* const scope = createRequestScope(request, baseRouter, { db });
|
|
99
|
-
* try {
|
|
100
|
-
* scope.router.usePlugin(rscServerPluginFactory(loaders));
|
|
101
|
-
* return await render(scope.router, request.url);
|
|
102
|
-
* } finally {
|
|
103
|
-
* await scope.dispose();
|
|
104
|
-
* }
|
|
105
|
-
* }
|
|
106
|
-
* ```
|
|
107
|
-
*/
|
|
108
|
-
declare function createRequestScope<Dependencies extends DefaultDependencies = DefaultDependencies>(request: RequestScopeSource, base: Router<Dependencies>, deps?: Partial<Dependencies>): RequestScope<Dependencies>;
|
|
109
|
-
//#endregion
|
|
110
|
-
export { IncomingMessageLike, RequestLike, RequestScope, RequestScopeSource, createRequestScope };
|
|
111
|
-
//# sourceMappingURL=createRequestScope.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"createRequestScope.d.ts","names":[],"sources":["../../../src/utils/createRequestScope.ts"],"mappings":";;;;;;;;UAUiB;EACf,KAAK,gBAAgB;EACrB,kBAAkB,gBAAgB;;;;;;;UAQnB;EACf,QAAQ;;KAGE,qBAAqB,sBAAsB;UAEtC,aACf,qBAAqB,sBAAsB,6BACnC;;;;;;WAMC,QAAQ,SAAY;;;;;WAMpB,QAAQ;;;;;;EAOjB,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+ED,mBACd,qBAAqB,sBAAsB,qBAE3C,SAAS,oBACT,MAAM,OAAO,eACb,OAAO,QAAQ,gBACd,aAAa"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("../api/cloneRouter.js");function t(e){return`signal`in e&&typeof e.signal==`object`&&e.signal!==void 0&&typeof e.signal.aborted==`boolean`}function n(n,r,i){let a,o,s;if(t(n))s=n.signal;else{let e=new AbortController,t=()=>{e.abort()};s=e.signal,o=()=>{n.on(`close`,t)},a=()=>{n.removeListener?.(`close`,t)}}let c=e.cloneRouter(r,{...i,abortSignal:s});o?.();let l=!1,u=()=>l?Promise.resolve():(l=!0,a?.(),c.dispose(),Promise.resolve());return{router:c,signal:s,dispose:u,[Symbol.asyncDispose]:u}}exports.createRequestScope=n;
|
|
2
|
-
//# sourceMappingURL=createRequestScope.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"createRequestScope.js","names":["cloneRouter"],"sources":["../../../src/utils/createRequestScope.ts"],"sourcesContent":["import { cloneRouter } from \"../api/cloneRouter\";\n\nimport type { Router as RouterClass } from \"../Router\";\nimport type { DefaultDependencies, Router } from \"../types\";\n\n/**\n * Subset of Node's `http.IncomingMessage` that `createRequestScope` relies on:\n * a `\"close\"` event indicating that the client disconnected (or the response\n * was fully sent) and the standard `removeListener` cleanup hook.\n */\nexport interface IncomingMessageLike {\n on: (event: \"close\", listener: () => void) => unknown;\n removeListener?: (event: \"close\", listener: () => void) => unknown;\n}\n\n/**\n * Web `Request`-shaped object — anything carrying an `AbortSignal`. Web\n * runtimes (Bun, Cloudflare Workers, Vite RSC) surface client-disconnect via\n * `request.signal` directly, so no listener attachment is needed.\n */\nexport interface RequestLike {\n signal: AbortSignal;\n}\n\nexport type RequestScopeSource = IncomingMessageLike | RequestLike;\n\nexport interface RequestScope<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> extends AsyncDisposable {\n /**\n * Per-request router clone. Carries `abortSignal` injected into its\n * dependencies — loaders can `getDep(\"abortSignal\")` and pass it to fetch /\n * `withTimeout` for cooperative cancellation when the client disconnects.\n */\n readonly router: RouterClass<Dependencies>;\n\n /**\n * Aborts when the request closes (Node `IncomingMessage`'s `\"close\"` event)\n * or when the upstream Web `Request.signal` aborts.\n */\n readonly signal: AbortSignal;\n\n /**\n * Detach the close listener (if attached to a Node `IncomingMessage`) and\n * dispose the cloned router. Idempotent — safe to call multiple times or in\n * combination with `Symbol.asyncDispose`.\n */\n dispose: () => Promise<void>;\n}\n\nfunction isRequestLike(request: RequestScopeSource): request is RequestLike {\n return (\n \"signal\" in request &&\n typeof (request as Partial<RequestLike>).signal === \"object\" &&\n (request as Partial<RequestLike>).signal !== undefined &&\n typeof request.signal.aborted === \"boolean\"\n );\n}\n\n/**\n * Build a per-request router scope: clones `base`, attaches an `AbortSignal`\n * tied to the request's lifetime, and exposes `dispose()` (plus\n * `Symbol.asyncDispose` for `await using` declarations).\n *\n * Replaces the four-step boilerplate that every server entry repeats:\n *\n * 1. `new AbortController()` per request\n * 2. `req.on(\"close\", () => controller.abort())`\n * 3. `cloneRouter(base, { ...deps, abortSignal: signal })`\n * 4. `try { ... } finally { router.dispose() }`\n *\n * The signal is injected into the router clone under `abortSignal` so existing\n * loaders that read `getDep(\"abortSignal\")` keep working without changes. If\n * `deps` already carries an `abortSignal`, the request-tied signal **wins** (it\n * is spread last) — a caller-supplied `abortSignal` is intentionally overridden,\n * since the scope's purpose is to own the request-lifecycle signal.\n *\n * ## `await using` compatibility\n *\n * The scope implements `Symbol.asyncDispose`, so `await using scope = …` is\n * supported on runtimes that ship the well-known `Symbol.asyncDispose`:\n *\n * - **Node.js 24+** (full support; partial in 20.4–20.17 only for `fs`/`stream`)\n * - **Bun 1.0.23+**, **Deno 1.37+**\n * - **Chrome / Edge 127+**, **Firefox 141+**\n * - **Safari**: not yet supported (irrelevant in practice — this helper is\n * server-side only and never reaches the browser)\n *\n * On Node.js 22 LTS the well-known symbol is unavailable, so `await using`\n * fails. **The bundled SSR examples therefore use the explicit\n * `try/finally` + `await scope.dispose()` form**, which works on every\n * runtime. Use `await using` only when you control the deployment target and\n * know it ships the symbol.\n *\n * @example\n * ```typescript\n * // Explicit dispose — works on Node 18+, all browsers, every CI image\n * export async function render(url: string, req: IncomingMessage) {\n * const scope = createRequestScope(req, baseRouter, { currentUser });\n * try {\n * scope.router.usePlugin(ssrDataPluginFactory(loaders));\n * return await renderShell(scope.router, url);\n * } finally {\n * await scope.dispose();\n * }\n * }\n *\n * // `await using` — Node 24+, Bun, Deno, modern browsers\n * export async function render(url: string, req: IncomingMessage) {\n * await using scope = createRequestScope(req, baseRouter, { currentUser });\n * scope.router.usePlugin(ssrDataPluginFactory(loaders));\n * return await renderShell(scope.router, url);\n * }\n *\n * // Web runtime (signal already on the request)\n * async function handler(request: Request) {\n * const scope = createRequestScope(request, baseRouter, { db });\n * try {\n * scope.router.usePlugin(rscServerPluginFactory(loaders));\n * return await render(scope.router, request.url);\n * } finally {\n * await scope.dispose();\n * }\n * }\n * ```\n */\nexport function createRequestScope<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n request: RequestScopeSource,\n base: Router<Dependencies>,\n deps?: Partial<Dependencies>,\n): RequestScope<Dependencies> {\n let detach: (() => void) | undefined;\n let attach: (() => void) | undefined;\n let signal: AbortSignal;\n\n if (isRequestLike(request)) {\n signal = request.signal;\n } else {\n const controller = new AbortController();\n const onClose = (): void => {\n controller.abort();\n };\n\n signal = controller.signal;\n // Clone-before-attach (#969): defer attaching the \"close\" listener until\n // after cloneRouter succeeds. cloneRouter is synchronous, so no \"close\"\n // macrotask can fire in the gap; and if it throws (e.g. ROUTER_DISPOSED on\n // an already-disposed base) the helper exits without returning a scope\n // handle — so a listener attached here would strand on the request with no\n // way to detach it.\n attach = () => {\n request.on(\"close\", onClose);\n };\n detach = () => {\n request.removeListener?.(\"close\", onClose);\n };\n }\n\n const router = cloneRouter(base, {\n ...deps,\n abortSignal: signal,\n } as Dependencies);\n\n // The clone exists — now it is safe to attach the close listener (Node path\n // only; `attach` is undefined for the Web path).\n attach?.();\n\n let disposed = false;\n\n const dispose = (): Promise<void> => {\n if (disposed) {\n return Promise.resolve();\n }\n\n disposed = true;\n detach?.();\n router.dispose();\n\n return Promise.resolve();\n };\n\n return {\n router,\n signal,\n dispose,\n // eslint-disable-next-line unicorn/no-nonstandard-builtin-properties -- Symbol.asyncDispose is a standard ES2023 well-known symbol (`await using`)\n [Symbol.asyncDispose]: dispose,\n };\n}\n"],"mappings":"yCAkDA,SAAS,EAAc,EAAqD,CAC1E,MACE,WAAY,GACZ,OAAQ,EAAiC,QAAW,UACnD,EAAiC,SAAW,IAAA,IAC7C,OAAO,EAAQ,OAAO,SAAY,SAEtC,CAqEA,SAAgB,EAGd,EACA,EACA,EAC4B,CAC5B,IAAI,EACA,EACA,EAEJ,GAAI,EAAc,CAAO,EACvB,EAAS,EAAQ,WACZ,CACL,IAAM,EAAa,IAAI,gBACjB,MAAsB,CAC1B,EAAW,MAAM,CACnB,EAEA,EAAS,EAAW,OAOpB,MAAe,CACb,EAAQ,GAAG,QAAS,CAAO,CAC7B,EACA,MAAe,CACb,EAAQ,iBAAiB,QAAS,CAAO,CAC3C,CACF,CAEA,IAAM,EAASA,EAAAA,YAAY,EAAM,CAC/B,GAAG,EACH,YAAa,CACf,CAAiB,EAIjB,IAAS,EAET,IAAI,EAAW,GAET,MACA,EACK,QAAQ,QAAQ,GAGzB,EAAW,GACX,IAAS,EACT,EAAO,QAAQ,EAER,QAAQ,QAAQ,GAGzB,MAAO,CACL,SACA,SACA,WAEC,OAAO,cAAe,CACzB,CACF"}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { DefaultDependencies, Router } from "../types/router.js";
|
|
2
|
-
//#region src/utils/getStaticPaths.d.ts
|
|
3
|
-
type StaticPathEntries = Record<string, () => Promise<Record<string, string>[]>>;
|
|
4
|
-
declare function getStaticPaths<Dependencies extends DefaultDependencies = DefaultDependencies>(router: Router<Dependencies>, entries?: StaticPathEntries): Promise<string[]>;
|
|
5
|
-
//#endregion
|
|
6
|
-
export { StaticPathEntries, getStaticPaths };
|
|
7
|
-
//# sourceMappingURL=getStaticPaths.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"getStaticPaths.d.ts","names":[],"sources":["../../../src/utils/getStaticPaths.ts"],"mappings":";;KAKY,oBAAoB,qBAExB,QAAQ;iBA2BM,eACpB,qBAAqB,sBAAsB,qBAE3C,QAAQ,OAAO,eACf,UAAU,oBACT"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("../api/getPluginApi.js");function t(e,n){for(let r of e.children.values())r.children.size===0?n.push(r.fullName):t(r,n)}function n(e){let n=[];return t(e,n),n}async function r(t,r){let i=n(e.getPluginApi(t).getTree()),a=[];for(let e of i){let n=r?.[e];if(n){let r=await n();for(let n of r)a.push(t.buildPath(e,n))}else a.push(t.buildPath(e,{}))}return a}exports.getStaticPaths=r;
|
|
2
|
-
//# sourceMappingURL=getStaticPaths.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"getStaticPaths.js","names":["getPluginApi"],"sources":["../../../src/utils/getStaticPaths.ts"],"sourcesContent":["import { getPluginApi } from \"../api/getPluginApi\";\n\nimport type { RouteTree } from \"../engine\";\nimport type { DefaultDependencies, Router } from \"../types\";\n\nexport type StaticPathEntries = Record<\n string,\n () => Promise<Record<string, string>[]>\n>;\n\nfunction collectLeafRouteNames(node: RouteTree, result: string[]): void {\n for (const child of node.children.values()) {\n if (child.children.size === 0) {\n result.push(child.fullName);\n } else {\n // Accumulate into the shared array rather than\n // `result.push(...getLeafRouteNames(child))`: the spread passes one\n // argument per leaf, and V8 caps spread/apply arguments (~124k on Node 24),\n // so a section with that many static leaf routes threw\n // `RangeError: Maximum call stack size exceeded`. Accumulating also drops\n // the per-subtree intermediate-array allocation.\n collectLeafRouteNames(child, result);\n }\n }\n}\n\nfunction getLeafRouteNames(node: RouteTree): string[] {\n const result: string[] = [];\n\n collectLeafRouteNames(node, result);\n\n return result;\n}\n\nexport async function getStaticPaths<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n router: Router<Dependencies>,\n entries?: StaticPathEntries,\n): Promise<string[]> {\n const tree = getPluginApi(router).getTree();\n const leafRoutes = getLeafRouteNames(tree);\n const paths: string[] = [];\n\n for (const routeName of leafRoutes) {\n const entryFn = entries?.[routeName];\n\n if (entryFn) {\n const paramSets = await entryFn();\n\n for (const params of paramSets) {\n paths.push(router.buildPath(routeName, params));\n }\n } else {\n paths.push(router.buildPath(routeName, {}));\n }\n }\n\n return paths;\n}\n"],"mappings":"0CAUA,SAAS,EAAsB,EAAiB,EAAwB,CACtE,IAAK,IAAM,KAAS,EAAK,SAAS,OAAO,EACnC,EAAM,SAAS,OAAS,EAC1B,EAAO,KAAK,EAAM,QAAQ,EAQ1B,EAAsB,EAAO,CAAM,CAGzC,CAEA,SAAS,EAAkB,EAA2B,CACpD,IAAM,EAAmB,CAAC,EAI1B,OAFA,EAAsB,EAAM,CAAM,EAE3B,CACT,CAEA,eAAsB,EAGpB,EACA,EACmB,CAEnB,IAAM,EAAa,EADNA,EAAAA,aAAa,CAAM,CAAC,CAAC,QACM,CAAC,EACnC,EAAkB,CAAC,EAEzB,IAAK,IAAM,KAAa,EAAY,CAClC,IAAM,EAAU,IAAU,GAE1B,GAAI,EAAS,CACX,IAAM,EAAY,MAAM,EAAQ,EAEhC,IAAK,IAAM,KAAU,EACnB,EAAM,KAAK,EAAO,UAAU,EAAW,CAAM,CAAC,CAElD,MACE,EAAM,KAAK,EAAO,UAAU,EAAW,CAAC,CAAC,CAAC,CAE9C,CAEA,OAAO,CACT"}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { State } from "../types/base.js";
|
|
2
|
-
import { Router } from "../types/router.js";
|
|
3
|
-
//#region src/utils/hydrateRouter.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* Custom deserializer signature for {@link hydrateRouter} (#606). Compatible
|
|
6
|
-
* with `JSON.parse` (default), `devalue.parse`, `superjson.parse`, or any
|
|
7
|
-
* user-supplied function.
|
|
8
|
-
*/
|
|
9
|
-
type Deserialize = (json: string) => unknown;
|
|
10
|
-
interface HydrateRouterOptions {
|
|
11
|
-
/**
|
|
12
|
-
* Custom deserializer (e.g., `devalue.parse` / `superjson.parse`) for
|
|
13
|
-
* matching the `serialize` passed to {@link serializeRouterState}. Defaults
|
|
14
|
-
* to `JSON.parse`. Ignored when `source` is already an object.
|
|
15
|
-
*
|
|
16
|
-
* @default JSON.parse
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* ```typescript
|
|
20
|
-
* import * as devalue from "devalue";
|
|
21
|
-
* await hydrateRouter(router, ssrJson, { deserialize: devalue.parse });
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
24
|
-
deserialize?: Deserialize;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Hydrate a fresh router from server-serialized State (#563, #596).
|
|
28
|
-
*
|
|
29
|
-
* Accepts either a JSON string (parsed via `JSON.parse` by default, or
|
|
30
|
-
* `options.deserialize` when provided) or a State-shaped object. Extracts
|
|
31
|
-
* `state.path` and delegates to `router.start(state.path)` — the canonical
|
|
32
|
-
* URL is the source of truth for the router on hydration.
|
|
33
|
-
*
|
|
34
|
-
* The full parsed state (incl. `state.context.<namespace>` payloads) is
|
|
35
|
-
* deposited into a one-shot scratchpad on `RouterInternals.hydrationState`
|
|
36
|
-
* before `start()` is invoked and cleared in the matching `finally`. SSR
|
|
37
|
-
* loader plugins (`@real-router/ssr-data-plugin`,
|
|
38
|
-
* `@real-router/rsc-server-plugin`) read this scratchpad to skip their loader
|
|
39
|
-
* call when the server-resolved namespace value is already present — avoiding
|
|
40
|
-
* the post-hydration loader re-run on first paint.
|
|
41
|
-
*
|
|
42
|
-
* Single-shot semantics: the scratchpad is consumed during the first `start()`
|
|
43
|
-
* triggered by `hydrateRouter` regardless of route mismatch; subsequent
|
|
44
|
-
* `start()` calls run loaders normally.
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* ```typescript
|
|
48
|
-
* // Client
|
|
49
|
-
* const router = createAppRouter();
|
|
50
|
-
* router.usePlugin(browserPluginFactory());
|
|
51
|
-
* await hydrateRouter(router, window.__SSR_STATE__);
|
|
52
|
-
* ```
|
|
53
|
-
*
|
|
54
|
-
* @example
|
|
55
|
-
* ```typescript
|
|
56
|
-
* // With non-JSON types (Date / Map / Set / RegExp / BigInt) via devalue (#606)
|
|
57
|
-
* import * as devalue from "devalue";
|
|
58
|
-
*
|
|
59
|
-
* await hydrateRouter(router, window.__SSR_STATE__, {
|
|
60
|
-
* deserialize: devalue.parse,
|
|
61
|
-
* });
|
|
62
|
-
* ```
|
|
63
|
-
*/
|
|
64
|
-
declare function hydrateRouter(router: Router, source: string | {
|
|
65
|
-
path: string;
|
|
66
|
-
}, options?: HydrateRouterOptions): Promise<State>;
|
|
67
|
-
//#endregion
|
|
68
|
-
export { Deserialize, HydrateRouterOptions, hydrateRouter };
|
|
69
|
-
//# sourceMappingURL=hydrateRouter.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hydrateRouter.d.ts","names":[],"sources":["../../../src/utils/hydrateRouter.ts"],"mappings":";;;;;;;;KAUY,eAAe;UAEV;;;;;;;;;;;;;;EAcf,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyCM,cACpB,QAAQ,QACR;EAAmB;GACnB,UAAU,uBACT,QAAQ"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("../internals.js");async function t(t,n,r){let i=r?.deserialize??JSON.parse,a=typeof n==`string`?i(n):n,o=e.getInternals(t),s=o.hydrationState;o.hydrationState=a;try{return await t.start(a.path)}finally{o.hydrationState=s}}exports.hydrateRouter=t;
|
|
2
|
-
//# sourceMappingURL=hydrateRouter.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hydrateRouter.js","names":["getInternals"],"sources":["../../../src/utils/hydrateRouter.ts"],"sourcesContent":["import { getInternals } from \"../internals\";\n\nimport type { SerializedRouterState } from \"./serializeRouterState\";\nimport type { Router, State } from \"../types\";\n\n/**\n * Custom deserializer signature for {@link hydrateRouter} (#606). Compatible\n * with `JSON.parse` (default), `devalue.parse`, `superjson.parse`, or any\n * user-supplied function.\n */\nexport type Deserialize = (json: string) => unknown;\n\nexport interface HydrateRouterOptions {\n /**\n * Custom deserializer (e.g., `devalue.parse` / `superjson.parse`) for\n * matching the `serialize` passed to {@link serializeRouterState}. Defaults\n * to `JSON.parse`. Ignored when `source` is already an object.\n *\n * @default JSON.parse\n *\n * @example\n * ```typescript\n * import * as devalue from \"devalue\";\n * await hydrateRouter(router, ssrJson, { deserialize: devalue.parse });\n * ```\n */\n deserialize?: Deserialize;\n}\n\n/**\n * Hydrate a fresh router from server-serialized State (#563, #596).\n *\n * Accepts either a JSON string (parsed via `JSON.parse` by default, or\n * `options.deserialize` when provided) or a State-shaped object. Extracts\n * `state.path` and delegates to `router.start(state.path)` — the canonical\n * URL is the source of truth for the router on hydration.\n *\n * The full parsed state (incl. `state.context.<namespace>` payloads) is\n * deposited into a one-shot scratchpad on `RouterInternals.hydrationState`\n * before `start()` is invoked and cleared in the matching `finally`. SSR\n * loader plugins (`@real-router/ssr-data-plugin`,\n * `@real-router/rsc-server-plugin`) read this scratchpad to skip their loader\n * call when the server-resolved namespace value is already present — avoiding\n * the post-hydration loader re-run on first paint.\n *\n * Single-shot semantics: the scratchpad is consumed during the first `start()`\n * triggered by `hydrateRouter` regardless of route mismatch; subsequent\n * `start()` calls run loaders normally.\n *\n * @example\n * ```typescript\n * // Client\n * const router = createAppRouter();\n * router.usePlugin(browserPluginFactory());\n * await hydrateRouter(router, window.__SSR_STATE__);\n * ```\n *\n * @example\n * ```typescript\n * // With non-JSON types (Date / Map / Set / RegExp / BigInt) via devalue (#606)\n * import * as devalue from \"devalue\";\n *\n * await hydrateRouter(router, window.__SSR_STATE__, {\n * deserialize: devalue.parse,\n * });\n * ```\n */\nexport async function hydrateRouter(\n router: Router,\n source: string | { path: string },\n options?: HydrateRouterOptions,\n): Promise<State> {\n const deserialize: Deserialize = options?.deserialize ?? JSON.parse;\n const parsed =\n typeof source === \"string\"\n ? (deserialize(source) as SerializedRouterState)\n : (source as SerializedRouterState);\n\n const ctx = getInternals(router);\n const previous = ctx.hydrationState;\n\n ctx.hydrationState = parsed;\n\n try {\n return await router.start(parsed.path);\n } finally {\n ctx.hydrationState = previous;\n }\n}\n"],"mappings":"mCAmEA,eAAsB,EACpB,EACA,EACA,EACgB,CAChB,IAAM,EAA2B,GAAS,aAAe,KAAK,MACxD,EACJ,OAAO,GAAW,SACb,EAAY,CAAM,EAClB,EAED,EAAMA,EAAAA,aAAa,CAAM,EACzB,EAAW,EAAI,eAErB,EAAI,eAAiB,EAErB,GAAI,CACF,OAAO,MAAM,EAAO,MAAM,EAAO,IAAI,CACvC,QAAU,CACR,EAAI,eAAiB,CACvB,CACF"}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { Params, State } from "../types/base.js";
|
|
2
|
-
import { Serialize } from "./serializeState.js";
|
|
3
|
-
//#region src/utils/serializeRouterState.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* Parsed shape produced by {@link serializeRouterState} (after `JSON.parse`).
|
|
6
|
-
*
|
|
7
|
-
* Identical to {@link State} minus `transition` (per-navigation `TransitionMeta`
|
|
8
|
-
* is meaningless after hydration; the client builds its own on commit). Used as
|
|
9
|
-
* the input shape for {@link hydrateRouter} and as the type of the one-shot
|
|
10
|
-
* hydration scratchpad consumed by SSR loader plugins.
|
|
11
|
-
*/
|
|
12
|
-
type SerializedRouterState<P extends Params = Params> = Omit<State<P>, "transition">;
|
|
13
|
-
interface SerializeRouterStateOptions {
|
|
14
|
-
/**
|
|
15
|
-
* Plugin context namespaces to strip from the serialized output.
|
|
16
|
-
* Use when a plugin populates `state.context.<ns>` with non-JSON-serializable
|
|
17
|
-
* values (e.g., RSC payload: ReactNode trees containing functions/symbols).
|
|
18
|
-
*
|
|
19
|
-
* @default []
|
|
20
|
-
*/
|
|
21
|
-
excludeContext?: readonly string[];
|
|
22
|
-
/**
|
|
23
|
-
* Custom serializer (e.g., `devalue.stringify` / `superjson.stringify`) to
|
|
24
|
-
* support non-JSON types in `state.params` and `state.context.<ns>` payloads
|
|
25
|
-
* (Date / Map / Set / RegExp / BigInt). Defaults to `JSON.stringify`.
|
|
26
|
-
*
|
|
27
|
-
* Pair with the matching `deserialize` on `hydrateRouter` to round-trip the
|
|
28
|
-
* extended types on the client.
|
|
29
|
-
*
|
|
30
|
-
* @default JSON.stringify
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* ```typescript
|
|
34
|
-
* import * as devalue from "devalue";
|
|
35
|
-
*
|
|
36
|
-
* const json = serializeRouterState(state, { serialize: devalue.stringify });
|
|
37
|
-
* ```
|
|
38
|
-
*/
|
|
39
|
-
serialize?: Serialize;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* XSS-safe JSON serialization of router State for SSR → client transport (#563).
|
|
43
|
-
*
|
|
44
|
-
* Strips `state.transition` (per-navigation `TransitionMeta` — meaningless after
|
|
45
|
-
* hydration; the client's hydration commit produces its own `transition`).
|
|
46
|
-
* Keeps `name`, `params`, `path`, and `context` (plugin context namespaces are
|
|
47
|
-
* preserved as-is — server's `state.context.data` from `ssr-data-plugin` and
|
|
48
|
-
* any other plugin claims travel to the client untouched).
|
|
49
|
-
*
|
|
50
|
-
* Pass `options.excludeContext` to strip specific namespaces from the output —
|
|
51
|
-
* required for plugins that publish non-JSON-serializable values (e.g., RSC
|
|
52
|
-
* `ReactNode` trees from `@real-router/rsc-server-plugin`).
|
|
53
|
-
*
|
|
54
|
-
* @example
|
|
55
|
-
* ```typescript
|
|
56
|
-
* // Server
|
|
57
|
-
* const state = await router.start(req.url);
|
|
58
|
-
* const html = `<script>window.__SSR_STATE__=${serializeRouterState(state)}</script>`;
|
|
59
|
-
*
|
|
60
|
-
* // Client
|
|
61
|
-
* await hydrateRouter(router, window.__SSR_STATE__);
|
|
62
|
-
* ```
|
|
63
|
-
*
|
|
64
|
-
* @example
|
|
65
|
-
* ```typescript
|
|
66
|
-
* // With RSC plugin: strip the "rsc" namespace before transport
|
|
67
|
-
* const state = await router.start(url);
|
|
68
|
-
* const json = serializeRouterState(state, { excludeContext: ["rsc"] });
|
|
69
|
-
* ```
|
|
70
|
-
*
|
|
71
|
-
* @example
|
|
72
|
-
* ```typescript
|
|
73
|
-
* // Non-JSON types (Date / Map / Set / RegExp / BigInt) via devalue (#606)
|
|
74
|
-
* import * as devalue from "devalue";
|
|
75
|
-
*
|
|
76
|
-
* const json = serializeRouterState(state, { serialize: devalue.stringify });
|
|
77
|
-
* // On the client:
|
|
78
|
-
* await hydrateRouter(router, json, { deserialize: devalue.parse });
|
|
79
|
-
* ```
|
|
80
|
-
*/
|
|
81
|
-
declare function serializeRouterState(state: State, options?: SerializeRouterStateOptions): string;
|
|
82
|
-
//#endregion
|
|
83
|
-
export { SerializeRouterStateOptions, SerializedRouterState, serializeRouterState };
|
|
84
|
-
//# sourceMappingURL=serializeRouterState.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"serializeRouterState.d.ts","names":[],"sources":["../../../src/utils/serializeRouterState.ts"],"mappings":";;;;;;;;;;;KAaY,sBAAsB,UAAU,SAAS,UAAU,KAC7D,MAAM;UAIS;;;;;;;;EAQf;;;;;;;;;;;;;;;;;;EAmBA,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2CE,qBACd,OAAO,OACP,UAAU"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("./serializeState.js");function t(t,n){let r=n?.excludeContext,i=t.context;if(r?.length){let e=Object.create(null),n=t.context;for(let[t,i]of Object.entries(n))r.includes(t)||(e[t]=i);i=e}let a={name:t.name,params:t.params,path:t.path,context:i};return n?.serialize?e.serializeState(a,{serialize:n.serialize}):e.serializeState(a)}exports.serializeRouterState=t;
|
|
2
|
-
//# sourceMappingURL=serializeRouterState.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"serializeRouterState.js","names":["serializeState"],"sources":["../../../src/utils/serializeRouterState.ts"],"sourcesContent":["import { serializeState } from \"./serializeState\";\n\nimport type { Serialize } from \"./serializeState\";\nimport type { Params, State } from \"../types\";\n\n/**\n * Parsed shape produced by {@link serializeRouterState} (after `JSON.parse`).\n *\n * Identical to {@link State} minus `transition` (per-navigation `TransitionMeta`\n * is meaningless after hydration; the client builds its own on commit). Used as\n * the input shape for {@link hydrateRouter} and as the type of the one-shot\n * hydration scratchpad consumed by SSR loader plugins.\n */\nexport type SerializedRouterState<P extends Params = Params> = Omit<\n State<P>,\n \"transition\"\n>;\n\nexport interface SerializeRouterStateOptions {\n /**\n * Plugin context namespaces to strip from the serialized output.\n * Use when a plugin populates `state.context.<ns>` with non-JSON-serializable\n * values (e.g., RSC payload: ReactNode trees containing functions/symbols).\n *\n * @default []\n */\n excludeContext?: readonly string[];\n\n /**\n * Custom serializer (e.g., `devalue.stringify` / `superjson.stringify`) to\n * support non-JSON types in `state.params` and `state.context.<ns>` payloads\n * (Date / Map / Set / RegExp / BigInt). Defaults to `JSON.stringify`.\n *\n * Pair with the matching `deserialize` on `hydrateRouter` to round-trip the\n * extended types on the client.\n *\n * @default JSON.stringify\n *\n * @example\n * ```typescript\n * import * as devalue from \"devalue\";\n *\n * const json = serializeRouterState(state, { serialize: devalue.stringify });\n * ```\n */\n serialize?: Serialize;\n}\n\n/**\n * XSS-safe JSON serialization of router State for SSR → client transport (#563).\n *\n * Strips `state.transition` (per-navigation `TransitionMeta` — meaningless after\n * hydration; the client's hydration commit produces its own `transition`).\n * Keeps `name`, `params`, `path`, and `context` (plugin context namespaces are\n * preserved as-is — server's `state.context.data` from `ssr-data-plugin` and\n * any other plugin claims travel to the client untouched).\n *\n * Pass `options.excludeContext` to strip specific namespaces from the output —\n * required for plugins that publish non-JSON-serializable values (e.g., RSC\n * `ReactNode` trees from `@real-router/rsc-server-plugin`).\n *\n * @example\n * ```typescript\n * // Server\n * const state = await router.start(req.url);\n * const html = `<script>window.__SSR_STATE__=${serializeRouterState(state)}</script>`;\n *\n * // Client\n * await hydrateRouter(router, window.__SSR_STATE__);\n * ```\n *\n * @example\n * ```typescript\n * // With RSC plugin: strip the \"rsc\" namespace before transport\n * const state = await router.start(url);\n * const json = serializeRouterState(state, { excludeContext: [\"rsc\"] });\n * ```\n *\n * @example\n * ```typescript\n * // Non-JSON types (Date / Map / Set / RegExp / BigInt) via devalue (#606)\n * import * as devalue from \"devalue\";\n *\n * const json = serializeRouterState(state, { serialize: devalue.stringify });\n * // On the client:\n * await hydrateRouter(router, json, { deserialize: devalue.parse });\n * ```\n */\nexport function serializeRouterState(\n state: State,\n options?: SerializeRouterStateOptions,\n): string {\n const exclude = options?.excludeContext;\n\n let context = state.context;\n\n if (exclude?.length) {\n // Null-proto target so a literal \"__proto__\" namespace (a real own key on\n // state.context, e.g. written by claimContextNamespace) is copied as a\n // genuine own entry by the plain assignment below — a `{}` target would\n // instead dispatch into the inherited Object.prototype.__proto__ setter and\n // silently drop the data (#1191, same hazard the write path guards).\n const filtered: Record<string, unknown> = Object.create(null) as Record<\n string,\n unknown\n >;\n const source = state.context;\n\n for (const [key, value] of Object.entries(source)) {\n if (!exclude.includes(key)) {\n filtered[key] = value;\n }\n }\n\n context = filtered;\n }\n\n const payload = {\n name: state.name,\n params: state.params,\n path: state.path,\n context,\n };\n\n return options?.serialize\n ? serializeState(payload, { serialize: options.serialize })\n : serializeState(payload);\n}\n"],"mappings":"uCAwFA,SAAgB,EACd,EACA,EACQ,CACR,IAAM,EAAU,GAAS,eAErB,EAAU,EAAM,QAEpB,GAAI,GAAS,OAAQ,CAMnB,IAAM,EAAoC,OAAO,OAAO,IAAI,EAItD,EAAS,EAAM,QAErB,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,CAAM,EACzC,EAAQ,SAAS,CAAG,IACvB,EAAS,GAAO,GAIpB,EAAU,CACZ,CAEA,IAAM,EAAU,CACd,KAAM,EAAM,KACZ,OAAQ,EAAM,OACd,KAAM,EAAM,KACZ,SACF,EAEA,OAAO,GAAS,UACZA,EAAAA,eAAe,EAAS,CAAE,UAAW,EAAQ,SAAU,CAAC,EACxDA,EAAAA,eAAe,CAAO,CAC5B"}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
//#region src/utils/serializeState.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Custom serializer signature for {@link serializeState} (#606).
|
|
4
|
-
*
|
|
5
|
-
* Compatible with `JSON.stringify` (default) as well as `devalue.stringify`,
|
|
6
|
-
* `superjson.stringify`, or any user-supplied function that returns a JSON
|
|
7
|
-
* string. The output is run through XSS-safe character escapes regardless
|
|
8
|
-
* of which serializer produced it.
|
|
9
|
-
*/
|
|
10
|
-
type Serialize = (data: unknown) => string;
|
|
11
|
-
interface SerializeStateOptions {
|
|
12
|
-
/**
|
|
13
|
-
* Custom serializer (e.g., `devalue.stringify` / `superjson.stringify`) to
|
|
14
|
-
* support non-JSON types (Date / Map / Set / RegExp / BigInt). Defaults to
|
|
15
|
-
* `JSON.stringify`. Output is still XSS-escaped.
|
|
16
|
-
*
|
|
17
|
-
* @default JSON.stringify
|
|
18
|
-
*/
|
|
19
|
-
serialize?: Serialize;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* XSS-safe JSON serialization for embedding data in HTML `<script>` tags.
|
|
23
|
-
*
|
|
24
|
-
* Escapes `<`, `>`, and `&` to their Unicode equivalents to prevent
|
|
25
|
-
* injection via `</script>` or HTML entities inside inline scripts.
|
|
26
|
-
*
|
|
27
|
-
* Pass `options.serialize` to use a custom serializer such as `devalue` or
|
|
28
|
-
* `superjson` for non-JSON types (Date/Map/Set/RegExp/BigInt). The serializer
|
|
29
|
-
* must return a string; XSS-escape is applied to its output.
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* ```typescript
|
|
33
|
-
* const json = serializeState({ name: "home", path: "/" });
|
|
34
|
-
* const html = `<script>window.__STATE__=${json}</script>`;
|
|
35
|
-
* ```
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* ```typescript
|
|
39
|
-
* import * as devalue from "devalue";
|
|
40
|
-
*
|
|
41
|
-
* const json = serializeState(
|
|
42
|
-
* { date: new Date(), tags: new Set(["a", "b"]) },
|
|
43
|
-
* { serialize: devalue.stringify },
|
|
44
|
-
* );
|
|
45
|
-
* ```
|
|
46
|
-
*/
|
|
47
|
-
declare function serializeState(data: unknown, options?: SerializeStateOptions): string;
|
|
48
|
-
//#endregion
|
|
49
|
-
export { Serialize, SerializeStateOptions, serializeState };
|
|
50
|
-
//# sourceMappingURL=serializeState.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"serializeState.d.ts","names":[],"sources":["../../../src/utils/serializeState.ts"],"mappings":";;;;;;;;;KAQY,aAAa;UAER;;;;;;;;EAQf,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6BE,eACd,eACA,UAAU"}
|