@real-router/core 0.42.0 → 0.44.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 +13 -4
- package/dist/cjs/Router-BAks0PFv.js +6 -0
- package/dist/cjs/Router-BAks0PFv.js.map +1 -0
- package/dist/cjs/{Router-B2tmtNiM.d.ts → Router-Dh1xgFLI.d.ts} +67 -9
- package/dist/cjs/RouterError-AQUx-VLW.js +2 -0
- package/dist/cjs/RouterError-AQUx-VLW.js.map +1 -0
- package/dist/cjs/api.d.ts +1 -1
- package/dist/cjs/api.js +1 -1
- package/dist/cjs/{getPluginApi-BXrM_Nwv.js → getPluginApi-1VcDVGjf.js} +2 -2
- package/dist/cjs/{getPluginApi-BXrM_Nwv.js.map → getPluginApi-1VcDVGjf.js.map} +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/utils.js +1 -1
- package/dist/cjs/validation.d.ts +1 -1
- package/dist/esm/{Router-4JRfQLMM.d.mts → Router-BPkXwb1J.d.mts} +67 -9
- package/dist/esm/Router-C3BXH7z8.mjs +6 -0
- package/dist/esm/Router-C3BXH7z8.mjs.map +1 -0
- package/dist/esm/RouterError-BOUkCIgf.mjs +2 -0
- package/dist/esm/RouterError-BOUkCIgf.mjs.map +1 -0
- package/dist/esm/api.d.mts +1 -1
- package/dist/esm/api.mjs +1 -1
- package/dist/esm/{getPluginApi-D0bBPuLp.mjs → getPluginApi-BvOUPp3g.mjs} +2 -2
- package/dist/esm/{getPluginApi-D0bBPuLp.mjs.map → getPluginApi-BvOUPp3g.mjs.map} +1 -1
- package/dist/esm/index.d.mts +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/utils.mjs +1 -1
- package/dist/esm/validation.d.mts +1 -1
- package/package.json +2 -2
- package/src/Router.ts +16 -2
- package/src/constants.ts +2 -0
- package/src/fsm/routerFSM.ts +17 -7
- package/src/getNavigator.ts +3 -1
- package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +61 -7
- package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +28 -10
- package/src/namespaces/NavigationNamespace/transition/guardPhase.ts +7 -0
- package/src/namespaces/NavigationNamespace/types.ts +3 -0
- package/src/namespaces/OptionsNamespace/constants.ts +3 -6
- package/src/namespaces/PluginsNamespace/constants.ts +2 -0
- package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +1 -1
- package/src/types.ts +2 -1
- package/src/wiring/RouterWiringBuilder.ts +4 -1
- package/dist/cjs/Router-CVNR-dug.js +0 -6
- package/dist/cjs/Router-CVNR-dug.js.map +0 -1
- package/dist/cjs/RouterError-BkgjTHQg.js +0 -2
- package/dist/cjs/RouterError-BkgjTHQg.js.map +0 -1
- package/dist/esm/Router-C_k70gHi.mjs +0 -6
- package/dist/esm/Router-C_k70gHi.mjs.map +0 -1
- package/dist/esm/RouterError-D0RlQE_5.mjs +0 -2
- package/dist/esm/RouterError-D0RlQE_5.mjs.map +0 -1
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=Object.freeze({ROUTER_NOT_STARTED:`NOT_STARTED`,NO_START_PATH_OR_STATE:`NO_START_PATH_OR_STATE`,ROUTER_ALREADY_STARTED:`ALREADY_STARTED`,ROUTE_NOT_FOUND:`ROUTE_NOT_FOUND`,SAME_STATES:`SAME_STATES`,CANNOT_DEACTIVATE:`CANNOT_DEACTIVATE`,CANNOT_ACTIVATE:`CANNOT_ACTIVATE`,TRANSITION_ERR:`TRANSITION_ERR`,TRANSITION_CANCELLED:`CANCELLED`,ROUTER_DISPOSED:`DISPOSED`,PLUGIN_CONFLICT:`PLUGIN_CONFLICT`}),t=`@@router/UNKNOWN_ROUTE`,n={UNKNOWN_ROUTE:t},r={ROUTER_START:`onStart`,ROUTER_STOP:`onStop`,TRANSITION_START:`onTransitionStart`,TRANSITION_LEAVE_APPROVE:`onTransitionLeaveApprove`,TRANSITION_CANCEL:`onTransitionCancel`,TRANSITION_SUCCESS:`onTransitionSuccess`,TRANSITION_ERROR:`onTransitionError`},i={ROUTER_START:`$start`,ROUTER_STOP:`$stop`,TRANSITION_START:`$$start`,TRANSITION_LEAVE_APPROVE:`$$leaveApprove`,TRANSITION_CANCEL:`$$cancel`,TRANSITION_SUCCESS:`$$success`,TRANSITION_ERROR:`$$error`},a={maxDependencies:100,maxPlugins:50,maxListeners:1e4,warnListeners:1e3,maxEventDepth:5,maxLifecycleHandlers:200},o=Object.freeze({});function s(e){if(typeof e!=`object`||!e)return!1;let t=e;return typeof t.name==`string`&&typeof t.path==`string`&&typeof t.params==`object`&&t.params!==null}function c(e){if(!e)return e;if(!s(e))throw TypeError(`[deepFreezeState] Expected valid State object, got: ${typeof e}`);let t=structuredClone(e),n=new WeakSet;function r(e){if(!(typeof e!=`object`||!e)&&!n.has(e))if(n.add(e),Object.freeze(e),Array.isArray(e))for(let t of e)r(t);else for(let t in e)r(e[t])}return r(t),t}const l=new WeakSet;function u(e){if(!(typeof e!=`object`||!e)&&!Object.isFrozen(e))if(Object.freeze(e),Array.isArray(e))for(let t of e)u(t);else for(let t in e)u(e[t])}function d(e){return!e||l.has(e)?e:(u(e),l.add(e),e)}function f(e={}){return{...a,...e}}const p=new Set(Object.values(e)),m=new Set([`code`,`segment`,`path`,`redirect`]),h=new Set([`setCode`,`setErrorInstance`,`setAdditionalFields`,`hasField`,`getField`,`toJSON`]);var g=class extends Error{segment;path;redirect;code;constructor(e,{message:t,segment:n,path:r,redirect:i,...a}={}){super(t??e),this.code=e,this.segment=n,this.path=r,this.redirect=i?c(i):void 0;for(let[e,t]of Object.entries(a)){if(m.has(e))throw TypeError(`[RouterError] Cannot set reserved property "${e}"`);h.has(e)||(this[e]=t)}}setCode(e){this.code=e,p.has(this.message)&&(this.message=e)}setErrorInstance(e){if(!e)throw TypeError(`[RouterError.setErrorInstance] err parameter is required and must be an Error instance`);this.message=e.message,this.cause=e.cause,this.stack=e.stack??``}setAdditionalFields(e){for(let[t,n]of Object.entries(e)){if(m.has(t))throw TypeError(`[RouterError.setAdditionalFields] Cannot set reserved property "${t}"`);h.has(t)||(this[t]=n)}}hasField(e){return e in this}getField(e){return this[e]}toJSON(){let e={code:this.code,message:this.message};this.segment!==void 0&&(e.segment=this.segment),this.path!==void 0&&(e.path=this.path),this.redirect!==void 0&&(e.redirect=this.redirect);let t=new Set([`code`,`message`,`segment`,`path`,`redirect`,`stack`]);for(let n in this)Object.hasOwn(this,n)&&!t.has(n)&&(e[n]=this[n]);return e}};export{o as a,e as c,a as i,i as l,f as n,t as o,d as r,n as s,g as t,r as u};
|
|
2
|
+
//# sourceMappingURL=RouterError-BOUkCIgf.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RouterError-BOUkCIgf.mjs","names":[],"sources":["../../src/constants.ts","../../src/helpers.ts","../../src/RouterError.ts"],"sourcesContent":["// packages/core/src/constants.ts\n\nimport type {\n EventToNameMap,\n EventToPluginMap,\n ErrorCodeToValueMap,\n ErrorCodeKeys,\n ErrorCodeValues,\n} from \"@real-router/types\";\n\nexport type ConstantsKeys = \"UNKNOWN_ROUTE\";\n\nexport type Constants = Record<ConstantsKeys, string>;\n\n// =============================================================================\n// Error Codes (migrated from router-error)\n// =============================================================================\n\nexport type ErrorCodes = Record<ErrorCodeKeys, ErrorCodeValues>;\n\n/**\n * Error codes for router operations.\n * Used to identify specific failure scenarios in navigation and lifecycle.\n * Frozen to prevent accidental modifications.\n */\nexport const errorCodes: ErrorCodeToValueMap = Object.freeze({\n ROUTER_NOT_STARTED: \"NOT_STARTED\", // navigate() called before start()\n NO_START_PATH_OR_STATE: \"NO_START_PATH_OR_STATE\", // start() without initial route\n ROUTER_ALREADY_STARTED: \"ALREADY_STARTED\", // start() called twice\n ROUTE_NOT_FOUND: \"ROUTE_NOT_FOUND\", // Navigation to non-existent route\n SAME_STATES: \"SAME_STATES\", // Navigate to current route without reload\n CANNOT_DEACTIVATE: \"CANNOT_DEACTIVATE\", // canDeactivate guard blocked navigation\n CANNOT_ACTIVATE: \"CANNOT_ACTIVATE\", // canActivate guard blocked navigation\n TRANSITION_ERR: \"TRANSITION_ERR\", // Generic transition failure\n TRANSITION_CANCELLED: \"CANCELLED\", // Navigation cancelled by user or new navigation\n ROUTER_DISPOSED: \"DISPOSED\", // Router has been disposed\n PLUGIN_CONFLICT: \"PLUGIN_CONFLICT\", // Plugin tried to extend router with already-existing property\n});\n\n/**\n * General router constants.\n * Special route names and identifiers.\n */\nexport const UNKNOWN_ROUTE = \"@@router/UNKNOWN_ROUTE\";\n\nexport const constants: Constants = {\n UNKNOWN_ROUTE,\n};\n\n/**\n * Plugin method names.\n * Maps to methods that plugins can implement to hook into router lifecycle.\n */\nexport const plugins: EventToPluginMap = {\n ROUTER_START: \"onStart\", // Plugin method called when router starts\n ROUTER_STOP: \"onStop\", // Plugin method called when router stops\n TRANSITION_START: \"onTransitionStart\", // Plugin method called when navigation begins\n TRANSITION_LEAVE_APPROVE: \"onTransitionLeaveApprove\", // Plugin method called when deactivation guards pass\n TRANSITION_CANCEL: \"onTransitionCancel\", // Plugin method called when navigation cancelled\n TRANSITION_SUCCESS: \"onTransitionSuccess\", // Plugin method called when navigation succeeds\n TRANSITION_ERROR: \"onTransitionError\", // Plugin method called when navigation fails\n};\n\n/**\n * Event names for router event system.\n * Used with addEventListener/removeEventListener for reactive subscriptions.\n */\nexport const events: EventToNameMap = {\n ROUTER_START: \"$start\", // Emitted when router.start() succeeds\n ROUTER_STOP: \"$stop\", // Emitted when router.stop() is called\n TRANSITION_START: \"$$start\", // Emitted when navigation begins\n TRANSITION_LEAVE_APPROVE: \"$$leaveApprove\", // Emitted when deactivation guards pass\n TRANSITION_CANCEL: \"$$cancel\", // Emitted when navigation is cancelled\n TRANSITION_SUCCESS: \"$$success\", // Emitted when navigation completes successfully\n TRANSITION_ERROR: \"$$error\", // Emitted when navigation fails\n};\n\nexport const DEFAULT_LIMITS = {\n maxDependencies: 100,\n maxPlugins: 50,\n maxListeners: 10_000,\n warnListeners: 1000,\n maxEventDepth: 5,\n maxLifecycleHandlers: 200,\n} as const;\n\nexport const EMPTY_PARAMS: Readonly<Record<string, never>> = Object.freeze({});\n","// packages/core/src/helpers.ts\n\nimport { DEFAULT_LIMITS } from \"./constants\";\n\nimport type { Limits } from \"./types\";\nimport type { State, LimitsConfig } from \"@real-router/types\";\n\n// =============================================================================\n// State Helpers\n// =============================================================================\n\n/**\n * Structural type guard for State object.\n * Only checks required fields exist with correct types.\n * Does NOT validate params serializability (allows circular refs).\n *\n * Use `isState` from type-guards for full validation (serializable params).\n * Use this for internal operations like deepFreezeState that handle any object structure.\n *\n * @param value - Value to check\n * @returns true if value has State structure\n * @internal\n */\nfunction isStateStructural(value: unknown): value is State {\n if (value === null || typeof value !== \"object\") {\n return false;\n }\n\n const obj = value as Record<string, unknown>;\n\n return (\n typeof obj.name === \"string\" &&\n typeof obj.path === \"string\" &&\n typeof obj.params === \"object\" &&\n obj.params !== null\n );\n}\n\n/**\n * Deep freezes State object to prevent mutations.\n * Creates a deep clone first, then recursively freezes the clone and all nested objects.\n * Uses simple recursive freezing after cloning (no need for WeakSet since clone has no circular refs).\n *\n * @param state - The State object to freeze\n * @returns A frozen deep clone of the state\n * @throws {TypeError} If state is not a valid State object\n *\n * @example\n * const state = { name: 'home', params: {}, path: '/' };\n * const frozen = deepFreezeState(state);\n * // frozen.params is now immutable\n * // original state is unchanged\n */\nexport function deepFreezeState<T extends State>(state: T): T {\n // Early return for null/undefined\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!state) {\n return state;\n }\n\n // Validate State structure (structural check, allows circular refs)\n if (!isStateStructural(state)) {\n throw new TypeError(\n `[deepFreezeState] Expected valid State object, got: ${typeof state}`,\n );\n }\n\n // Create a deep clone to avoid mutating the original\n // structuredClone preserves circular references, so we need to track visited objects\n const clonedState = structuredClone(state);\n\n // WeakSet to track visited objects (prevent infinite recursion with circular refs)\n const visited = new WeakSet<object>();\n\n // Recursive freeze function with circular reference protection\n function freezeClonedRecursive(obj: unknown): void {\n // Skip primitives, null, undefined\n // Note: typeof undefined === \"undefined\" !== \"object\", so checking undefined is redundant\n if (obj === null || typeof obj !== \"object\") {\n return;\n }\n\n // Skip already visited objects (circular reference protection)\n if (visited.has(obj)) {\n return;\n }\n\n // Mark as visited\n visited.add(obj);\n\n // Freeze the object/array itself\n Object.freeze(obj);\n\n // Iterate without Object.values() allocation\n if (Array.isArray(obj)) {\n for (const item of obj) {\n freezeClonedRecursive(item);\n }\n } else {\n for (const key in obj) {\n freezeClonedRecursive((obj as Record<string, unknown>)[key]);\n }\n }\n }\n\n // Freeze the entire cloned state tree\n freezeClonedRecursive(clonedState);\n\n return clonedState;\n}\n\n// WeakSet to track already frozen root objects for O(1) re-freeze check\nconst frozenRoots = new WeakSet<object>();\n\n// Module-scope recursive freeze function - better JIT optimization, no allocation per call\nfunction freezeRecursive(obj: unknown): void {\n // Skip primitives, null\n if (obj === null || typeof obj !== \"object\") {\n return;\n }\n\n // Skip already frozen objects (handles potential shared refs)\n if (Object.isFrozen(obj)) {\n return;\n }\n\n // Freeze the object/array\n Object.freeze(obj);\n\n // Iterate without Object.values() allocation\n if (Array.isArray(obj)) {\n for (const item of obj) {\n freezeRecursive(item);\n }\n } else {\n for (const key in obj) {\n freezeRecursive((obj as Record<string, unknown>)[key]);\n }\n }\n}\n\n/**\n * Freezes State object in-place without cloning.\n * Optimized for hot paths where state is known to be a fresh object.\n *\n * IMPORTANT: Only use this when you know the state is a fresh object\n * that hasn't been exposed to external code yet (e.g., from makeState()).\n *\n * @param state - The State object to freeze (must be a fresh object)\n * @returns The same state object, now frozen\n * @internal\n */\nexport function freezeStateInPlace<T extends State>(state: T): T {\n // Early return for null/undefined - state from makeState() is never null\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!state) {\n return state;\n }\n\n // Fast path: already processed root object - O(1) check\n if (frozenRoots.has(state)) {\n return state;\n }\n\n freezeRecursive(state);\n\n // Mark root as processed for future calls\n frozenRoots.add(state);\n\n return state;\n}\n\n/**\n * Merges user limits with defaults.\n * Returns frozen object for immutability.\n */\nexport function createLimits(userLimits: Partial<LimitsConfig> = {}): Limits {\n return { ...DEFAULT_LIMITS, ...userLimits };\n}\n","// packages/core/src/RouterError.ts\n\nimport { errorCodes } from \"./constants\";\nimport { deepFreezeState } from \"./helpers\";\n\nimport type { State } from \"@real-router/types\";\n\n// Pre-compute Set of error code values for O(1) lookup in setCode()\n// This avoids creating array and doing linear search on every setCode() call\nconst errorCodeValues = new Set(Object.values(errorCodes));\n\n// Reserved built-in properties - throw error if user tries to set these\nconst reservedProperties = new Set([\"code\", \"segment\", \"path\", \"redirect\"]);\n\n// Reserved method names - silently ignore attempts to overwrite these\nconst reservedMethods = new Set([\n \"setCode\",\n \"setErrorInstance\",\n \"setAdditionalFields\",\n \"hasField\",\n \"getField\",\n \"toJSON\",\n]);\n\nexport class RouterError extends Error {\n [key: string]: unknown;\n\n // Using public properties to ensure structural compatibility\n // with RouterError interface in core-types\n readonly segment: string | undefined;\n readonly path: string | undefined;\n readonly redirect: State | undefined;\n\n // Note: code appears to be writable but setCode() should be used\n // to properly update both code and message together\n code: string;\n\n /**\n * Creates a new RouterError instance.\n *\n * The options object accepts built-in fields (message, segment, path, redirect)\n * and any additional custom fields, which will all be attached to the error instance.\n *\n * @param code - The error code (e.g., \"ROUTE_NOT_FOUND\", \"CANNOT_ACTIVATE\")\n * @param options - Optional configuration object\n * @param options.message - Custom error message (defaults to code if not provided)\n * @param options.segment - The route segment where the error occurred\n * @param options.path - The full path where the error occurred\n * @param options.redirect - Optional redirect state for navigation errors\n *\n * @example\n * ```typescript\n * // Basic error\n * const err1 = new RouterError(\"ROUTE_NOT_FOUND\");\n *\n * // Error with custom message\n * const err2 = new RouterError(\"ERR\", { message: \"Something went wrong\" });\n *\n * // Error with context and custom fields\n * const err3 = new RouterError(\"CANNOT_ACTIVATE\", {\n * message: \"Insufficient permissions\",\n * segment: \"admin\",\n * path: \"/admin/users\",\n * userId: \"123\" // custom field\n * });\n *\n * // Error with redirect\n * const err4 = new RouterError(\"TRANSITION_ERR\", {\n * redirect: { name: \"home\", path: \"/\", params: {} }\n * });\n * ```\n */\n constructor(\n code: string,\n {\n message,\n segment,\n path,\n redirect,\n ...rest\n }: {\n [key: string]: unknown;\n message?: string | undefined;\n segment?: string | undefined;\n path?: string | undefined;\n redirect?: State | undefined;\n } = {},\n ) {\n super(message ?? code);\n\n this.code = code;\n this.segment = segment;\n this.path = path;\n // Deep freeze redirect to prevent mutations (creates a frozen clone)\n this.redirect = redirect ? deepFreezeState(redirect) : undefined;\n\n // Assign custom fields, checking reserved properties and filtering out reserved method names\n // Issue #39: Throw for reserved properties to match setAdditionalFields behavior\n for (const [key, value] of Object.entries(rest)) {\n if (reservedProperties.has(key)) {\n throw new TypeError(\n `[RouterError] Cannot set reserved property \"${key}\"`,\n );\n }\n\n if (!reservedMethods.has(key)) {\n this[key] = value;\n }\n }\n }\n\n /**\n * Updates the error code and conditionally updates the message.\n *\n * If the current message is one of the standard error code values\n * (e.g., \"ROUTE_NOT_FOUND\", \"SAME_STATES\"), it will be replaced with the new code.\n * This allows keeping error messages in sync with codes when using standard error codes.\n *\n * If the message is custom (not a standard error code), it will be preserved.\n *\n * @param newCode - The new error code to set\n *\n * @example\n * // Message follows code (standard error code as message)\n * const err = new RouterError(\"ROUTE_NOT_FOUND\", { message: \"ROUTE_NOT_FOUND\" });\n * err.setCode(\"CUSTOM_ERROR\"); // message becomes \"CUSTOM_ERROR\"\n *\n * @example\n * // Custom message is preserved\n * const err = new RouterError(\"ERR\", { message: \"Custom error message\" });\n * err.setCode(\"NEW_CODE\"); // message stays \"Custom error message\"\n */\n setCode(newCode: string): void {\n this.code = newCode;\n\n // Only update message if it's a standard error code value (not a custom message)\n if (errorCodeValues.has(this.message)) {\n this.message = newCode;\n }\n }\n\n /**\n * Copies properties from another Error instance to this RouterError.\n *\n * This method updates the message, cause, and stack trace from the provided error.\n * Useful for wrapping native errors while preserving error context.\n *\n * @param err - The Error instance to copy properties from\n * @throws {TypeError} If err is null or undefined\n *\n * @example\n * ```typescript\n * const routerErr = new RouterError(\"TRANSITION_ERR\");\n * try {\n * // some operation that might fail\n * } catch (nativeErr) {\n * routerErr.setErrorInstance(nativeErr);\n * throw routerErr;\n * }\n * ```\n */\n setErrorInstance(err: Error): void {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!err) {\n throw new TypeError(\n \"[RouterError.setErrorInstance] err parameter is required and must be an Error instance\",\n );\n }\n\n this.message = err.message;\n this.cause = err.cause;\n this.stack = err.stack ?? \"\";\n }\n\n /**\n * Adds custom fields to the error object.\n *\n * This method allows attaching arbitrary data to the error for debugging or logging purposes.\n * All fields become accessible as properties on the error instance and are included in JSON serialization.\n *\n * Reserved method names (setCode, setErrorInstance, setAdditionalFields, hasField, getField, toJSON)\n * are automatically filtered out to prevent accidental overwriting of class methods.\n *\n * @param fields - Object containing custom fields to add to the error\n *\n * @example\n * ```typescript\n * const err = new RouterError(\"CANNOT_ACTIVATE\");\n * err.setAdditionalFields({\n * userId: \"123\",\n * attemptedRoute: \"/admin\",\n * reason: \"insufficient permissions\"\n * });\n *\n * console.log(err.userId); // \"123\"\n * console.log(JSON.stringify(err)); // includes all custom fields\n * ```\n */\n setAdditionalFields(fields: Record<string, unknown>): void {\n // Assign fields, throwing for reserved properties, silently ignoring methods\n for (const [key, value] of Object.entries(fields)) {\n if (reservedProperties.has(key)) {\n throw new TypeError(\n `[RouterError.setAdditionalFields] Cannot set reserved property \"${key}\"`,\n );\n }\n\n if (!reservedMethods.has(key)) {\n this[key] = value;\n }\n }\n }\n\n /**\n * Checks if a custom field exists on the error object.\n *\n * This method checks for both custom fields added via setAdditionalFields()\n * and built-in fields (code, message, segment, etc.).\n *\n * @param key - The field name to check\n * @returns `true` if the field exists, `false` otherwise\n *\n * @example\n * ```typescript\n * const err = new RouterError(\"ERR\", { segment: \"users\" });\n * err.setAdditionalFields({ userId: \"123\" });\n *\n * err.hasField(\"userId\"); // true\n * err.hasField(\"segment\"); // true\n * err.hasField(\"unknown\"); // false\n * ```\n */\n hasField(key: string): boolean {\n return key in this;\n }\n\n /**\n * Retrieves a custom field value from the error object.\n *\n * This method can access both custom fields and built-in fields.\n * Returns `undefined` if the field doesn't exist.\n *\n * @param key - The field name to retrieve\n * @returns The field value, or `undefined` if it doesn't exist\n *\n * @example\n * ```typescript\n * const err = new RouterError(\"ERR\");\n * err.setAdditionalFields({ userId: \"123\", role: \"admin\" });\n *\n * err.getField(\"userId\"); // \"123\"\n * err.getField(\"role\"); // \"admin\"\n * err.getField(\"code\"); // \"ERR\" (built-in field)\n * err.getField(\"unknown\"); // undefined\n * ```\n */\n getField(key: string): unknown {\n return this[key];\n }\n\n /**\n * Serializes the error to a JSON-compatible object.\n *\n * This method is automatically called by JSON.stringify() and includes:\n * - Built-in fields: code, message, segment (if set), path (if set), redirect (if set)\n * - All custom fields added via setAdditionalFields() or constructor\n * - Excludes: stack trace (for security/cleanliness)\n *\n * @returns A plain object representation of the error, suitable for JSON serialization\n *\n * @example\n * ```typescript\n * const err = new RouterError(\"ROUTE_NOT_FOUND\", {\n * message: \"Route not found\",\n * path: \"/admin/users/123\"\n * });\n * err.setAdditionalFields({ userId: \"123\" });\n *\n * JSON.stringify(err);\n * // {\n * // \"code\": \"ROUTE_NOT_FOUND\",\n * // \"message\": \"Route not found\",\n * // \"path\": \"/admin/users/123\",\n * // \"userId\": \"123\"\n * // }\n * ```\n */\n toJSON(): Record<string, unknown> {\n const result: Record<string, unknown> = {\n code: this.code,\n message: this.message,\n };\n\n if (this.segment !== undefined) {\n result.segment = this.segment;\n }\n if (this.path !== undefined) {\n result.path = this.path;\n }\n if (this.redirect !== undefined) {\n result.redirect = this.redirect;\n }\n\n // add all public fields\n // Using Set.has() for O(1) lookup instead of Array.includes() O(n)\n // Overall complexity: O(n) instead of O(n*m)\n const excludeKeys = new Set([\n \"code\",\n \"message\",\n \"segment\",\n \"path\",\n \"redirect\",\n \"stack\",\n ]);\n\n for (const key in this) {\n if (Object.hasOwn(this, key) && !excludeKeys.has(key)) {\n result[key] = this[key];\n }\n }\n\n return result;\n }\n}\n"],"mappings":"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"}
|
package/dist/esm/api.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { f as RouteTree, t as Router$1 } from "./Router-
|
|
1
|
+
import { f as RouteTree, t as Router$1 } from "./Router-BPkXwb1J.mjs";
|
|
2
2
|
import { DefaultDependencies, DependenciesApi, LifecycleApi, PluginApi as PluginApi$1, Router, RoutesApi } from "@real-router/types";
|
|
3
3
|
|
|
4
4
|
//#region src/api/types.d.ts
|
package/dist/esm/api.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{c as e,i as t,l as n,n as r,o as i,r as a,s as o,t as s,u as c}from"./Router-
|
|
1
|
+
import{c as e,i as t,l as n,n as r,o as i,r as a,s as o,t as s,u as c}from"./Router-C3BXH7z8.mjs";import{c as l,t as u}from"./RouterError-BOUkCIgf.mjs";import{r as d}from"./internals-CCymabFj.mjs";import{n as f,t as p}from"./getPluginApi-BvOUPp3g.mjs";import{logger as m}from"@real-router/logger";function h(e,t,n){if(t){let n=t===e,r=t.startsWith(`${e}.`);if(n||r){let r=n?``:` (current: "${t}")`;return m.warn(`router.removeRoute`,`Cannot remove route "${e}" — it is currently active${r}. Navigate away first.`),!1}}return n&&m.warn(`router.removeRoute`,`Route "${e}" removed while navigation is in progress. This may cause unexpected behavior.`),!0}function g(e){return e?(m.error(`router.clearRoutes`,`Cannot clear routes while navigation is in progress. Wait for navigation to complete.`),!1):!0}function _(e,t,n=``){for(let r of e){let e=n?`${n}.${r.name}`:r.name;if(e===t)return r;if(r.children&&t.startsWith(`${e}.`))return _(r.children,t,e)}}function v(e,t,n,r){let a=t=>t===e||t.startsWith(`${e}.`);i(t.decoders,a),i(t.encoders,a),i(t.defaultParams,a),i(t.forwardMap,a),i(t.forwardFnMap,a),i(n,a),i(t.forwardMap,e=>a(t.forwardMap[e]));let[o,s]=r.getFactories();for(let e of Object.keys(s))a(e)&&r.clearCanActivate(e);for(let e of Object.keys(o))a(e)&&r.clearCanDeactivate(e)}function y(e,t,n,r){return t===null?(delete n.forwardMap[e],delete n.forwardFnMap[e]):typeof t==`string`?(delete n.forwardFnMap[e],n.forwardMap[e]=t):(delete n.forwardMap[e],n.forwardFnMap[e]=t),r(n)}function b(e,t,n,r){let i={name:e.name,path:e.path},a=n.forwardFnMap[t],o=n.forwardMap[t];a===void 0?o!==void 0&&(i.forwardTo=o):i.forwardTo=a,t in n.defaultParams&&(i.defaultParams=n.defaultParams[t]),t in n.decoders&&(i.decodeParams=n.decoders[t]),t in n.encoders&&(i.encodeParams=n.encoders[t]);let[s,c]=r;return t in c&&(i.canActivate=c[t]),t in s&&(i.canDeactivate=s[t]),e.children&&(i.children=e.children.map(e=>b(e,`${t}.${e.name}`,n,r))),i}function x(n,r,i){if(i){let t=_(n.definitions,i);t.children??=[];for(let n of r)t.children.push(e(n))}else for(let t of r)n.definitions.push(e(t));t(r,n.config,n.routeCustomFields,n.pendingCanActivate,n.pendingCanDeactivate,n.depsStore,i??``),n.treeOperations.commitTreeChanges(n)}function S(n,i,a,o){r(n),n.lifecycleNamespace.clearDefinitionGuards();for(let t of i)n.definitions.push(e(t));if(t(i,n.config,n.routeCustomFields,n.pendingCanActivate,n.pendingCanDeactivate,n.depsStore,``),n.treeOperations.commitTreeChanges(n),o!==void 0){let e=a.matchPath(o,a.getOptions());e?a.setState(e):a.clearState()}}function C(e,t){return o(e.definitions,t)?(v(t,e.config,e.routeCustomFields,e.lifecycleNamespace),e.treeOperations.commitTreeChanges(e),!0):!1}function w(e,t,n){if(n.forwardTo!==void 0&&(e.resolvedForwardMap=y(t,n.forwardTo,e.config,e=>a(e))),n.defaultParams!==void 0&&(n.defaultParams===null?delete e.config.defaultParams[t]:e.config.defaultParams[t]=n.defaultParams),n.decodeParams!==void 0)if(n.decodeParams===null)delete e.config.decoders[t];else{let r=n.decodeParams;e.config.decoders[t]=e=>r(e)??e}if(n.encodeParams!==void 0)if(n.encodeParams===null)delete e.config.encoders[t];else{let r=n.encodeParams;e.config.encoders[t]=e=>r(e)??e}}function T(e,t){let n=e.matcher.getSegmentsByName(t);if(!n)return;let r=n.at(-1),i=e.treeOperations.nodeToDefinition(r),a=e.lifecycleNamespace.getFactories();return b(i,t,e.config,a)}function E(e){let t=d(e),n=t.routeGetStore();return{add:(e,r)=>{f(t.isDisposed);let i=Array.isArray(e)?e:[e],a=r?.parent;c(i,t.validator),a!==void 0&&t.validator?.routes.validateParentOption(a,n.tree),t.validator?.routes.throwIfInternalRouteInArray(i,`addRoute`),t.validator?.routes.validateAddRouteArgs(i),t.validator?.routes.validateRoutes(i,n),x(n,i,a)},remove:e=>{f(t.isDisposed),t.validator?.routes.validateRemoveRouteArgs(e),t.validator?.routes.throwIfInternalRoute(e,`removeRoute`),h(e,t.getStateName(),t.isTransitioning())&&(C(n,e)||m.warn(`router.removeRoute`,`Route "${e}" not found. No changes made.`))},update:(e,r)=>{f(t.isDisposed),t.validator?.routes.validateUpdateRouteBasicArgs(e,r),t.validator?.routes.throwIfInternalRoute(e,`updateRoute`);let{forwardTo:i,defaultParams:a,decodeParams:o,encodeParams:s,canActivate:c,canDeactivate:l}=r;t.validator?.routes.validateUpdateRoutePropertyTypes(e,r),t.isTransitioning()&&m.error(`router.updateRoute`,`Updating route "${e}" while navigation is in progress. This may cause unexpected behavior.`),t.validator?.routes.validateUpdateRoute(e,r,n),w(n,e,{forwardTo:i,defaultParams:a,decodeParams:o,encodeParams:s}),c!==void 0&&(c===null?n.lifecycleNamespace.clearCanActivate(e):n.lifecycleNamespace.addCanActivate(e,c,!0)),l!==void 0&&(l===null?n.lifecycleNamespace.clearCanDeactivate(e):n.lifecycleNamespace.addCanDeactivate(e,l,!0))},clear:()=>{f(t.isDisposed),g(t.isTransitioning())&&(n.treeOperations.resetStore(n),n.lifecycleNamespace.clearAll(),t.clearState())},has:e=>(t.validator?.routes.validateRouteName(e,`hasRoute`),n.matcher.hasRoute(e)),get:e=>(t.validator?.routes.validateRouteName(e,`getRoute`),T(n,e)),replace:r=>{f(t.isDisposed);let i=Array.isArray(r)?r:[r];if(!g(t.isTransitioning()))return;c(i,t.validator),t.validator?.routes.throwIfInternalRouteInArray(i,`replaceRoutes`),t.validator?.routes.validateAddRouteArgs(i),t.validator?.routes.validateRoutes(i,n);let a=e.getState()?.path;S(n,i,t,a)}}}function D(e,t,n,r){if(n===void 0)return!1;if(!Object.hasOwn(e.dependencies,t))r?.dependencies.validateDependencyCount(e,`setDependency`);else{let i=e.dependencies[t];i!==n&&!(Number.isNaN(i)&&Number.isNaN(n))&&r?.dependencies.warnOverwrite(t,`setDependency`)}return e.dependencies[t]=n,!0}function O(e,t,n){let r=[];for(let i in t)t[i]!==void 0&&(Object.hasOwn(e.dependencies,i)?r.push(i):n?.dependencies.validateDependencyCount(e,`setDependencies`),e.dependencies[i]=t[i]);r.length>0&&n?.dependencies.warnBatchOverwrite(r,`setDependencies`)}function k(e){let t=d(e);return{get:e=>{t.validator?.dependencies.validateDependencyName(e,`getDependency`);let n=t.dependenciesGetStore(),r=n.dependencies[e];return t.validator?.dependencies.validateDependencyExists(e,n),r},getAll:()=>({...t.dependenciesGetStore().dependencies}),set:(e,n)=>{f(t.isDisposed),t.validator?.dependencies.validateSetDependencyArgs(e,n,`setDependency`),D(t.dependenciesGetStore(),e,n,t.validator)},setAll:e=>{f(t.isDisposed);let n=t.dependenciesGetStore();t.validator?.dependencies.validateDependenciesObject(e,`setDependencies`),t.validator?.dependencies.validateDependencyLimit(n,n.limits),O(n,e,t.validator)},remove:e=>{f(t.isDisposed),t.validator?.dependencies.validateDependencyName(e,`removeDependency`);let n=t.dependenciesGetStore();Object.hasOwn(n.dependencies,e)||t.validator?.dependencies.warnRemoveNonExistent(e),delete n.dependencies[e]},reset:()=>{f(t.isDisposed);let e=t.dependenciesGetStore();e.dependencies=Object.create(null)},has:e=>(t.validator?.dependencies.validateDependencyName(e,`hasDependency`),Object.hasOwn(t.dependenciesGetStore().dependencies,e))}}function A(e){let t=d(e),n=t.routeGetStore().lifecycleNamespace;return{addActivateGuard(e,r){f(t.isDisposed),t.validator?.routes.validateRouteName(e,`addActivateGuard`),t.validator?.lifecycle.validateHandler(r,`addActivateGuard`);let i=n.getHandlerCount(`activate`);t.validator?.lifecycle.validateHandlerLimit(i,t.dependenciesGetStore().limits,`canActivate`),n.addCanActivate(e,r)},addDeactivateGuard(e,r){f(t.isDisposed),t.validator?.routes.validateRouteName(e,`addDeactivateGuard`),t.validator?.lifecycle.validateHandler(r,`addDeactivateGuard`);let i=n.getHandlerCount(`deactivate`);t.validator?.lifecycle.validateHandlerLimit(i,t.dependenciesGetStore().limits,`canDeactivate`),n.addCanDeactivate(e,r)},removeActivateGuard(e){f(t.isDisposed),t.validator?.routes.validateRouteName(e,`removeActivateGuard`),n.clearCanActivate(e)},removeDeactivateGuard(e){f(t.isDisposed),t.validator?.routes.validateRouteName(e,`removeDeactivateGuard`),n.clearCanDeactivate(e)}}}function j(e,t){let r=d(e);if(r.isDisposed())throw new u(l.ROUTER_DISPOSED);r.validator?.dependencies.validateCloneArgs(t);let i=r.routeGetStore(),a=n(i.tree),o=i.config,c=i.resolvedForwardMap,f=i.routeCustomFields,p=r.cloneOptions(),m=r.cloneDependencies(),[h,g]=r.getLifecycleFactories(),_=r.getPluginFactories(),v=new s(a,p,{...m,...t}),y=A(v);for(let[e,t]of Object.entries(h))y.addDeactivateGuard(e,t);for(let[e,t]of Object.entries(g))y.addActivateGuard(e,t);_.length>0&&v.usePlugin(..._);let b=d(v).routeGetStore();return Object.assign(b.config.decoders,o.decoders),Object.assign(b.config.encoders,o.encoders),Object.assign(b.config.defaultParams,o.defaultParams),Object.assign(b.config.forwardMap,o.forwardMap),Object.assign(b.config.forwardFnMap,o.forwardFnMap),Object.assign(b.resolvedForwardMap,c),Object.assign(b.routeCustomFields,f),v}export{j as cloneRouter,k as getDependenciesApi,A as getLifecycleApi,p as getPluginApi,E as getRoutesApi};
|
|
2
2
|
//# sourceMappingURL=api.mjs.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{c as e,t}from"./RouterError-
|
|
2
|
-
//# sourceMappingURL=getPluginApi-
|
|
1
|
+
import{c as e,t}from"./RouterError-BOUkCIgf.mjs";import{r as n}from"./internals-CCymabFj.mjs";function r(n){if(n())throw new t(e.ROUTER_DISPOSED)}function i(i){let a=n(i);return{makeState:(e,t,n,r)=>(a.validator?.state.validateMakeStateArgs(e,t,n),a.makeState(e,t,n,r?.params)),buildState:(e,t)=>{a.validator?.routes.validateStateBuilderArgs(e,t,`buildState`);let{name:n,params:r}=a.forwardState(e,t);return a.buildStateResolved(n,r)},forwardState:(e,t)=>(a.validator?.routes.validateStateBuilderArgs(e,t,`forwardState`),a.forwardState(e,t)),matchPath:e=>(a.validator?.routes.validateMatchPathArgs(e),a.matchPath(e,a.getOptions())),setRootPath:e=>{r(a.isDisposed),a.validator?.routes.validateSetRootPathArgs(e),a.setRootPath(e)},getRootPath:a.getRootPath,addEventListener:(e,t)=>(r(a.isDisposed),a.validator?.eventBus.validateListenerArgs(e,t),a.addEventListener(e,t)),buildNavigationState:(e,t={})=>{a.validator?.routes.validateStateBuilderArgs(e,t,`buildNavigationState`);let{name:n,params:r}=a.forwardState(e,t),i=a.buildStateResolved(n,r);if(i)return a.makeState(i.name,i.params,a.buildPath(i.name,i.params),i.meta)},getOptions:a.getOptions,getTree:a.getTree,addInterceptor:(e,t)=>{r(a.isDisposed),a.validator?.plugins.validateAddInterceptorArgs(e,t);let n=a.interceptors.get(e);return n||(n=[],a.interceptors.set(e,n)),n.push(t),()=>{let e=n.indexOf(t);e!==-1&&n.splice(e,1)}},getRouteConfig:e=>{let t=a.routeGetStore();if(t.matcher.hasRoute(e))return t.routeCustomFields[e]},extendRouter:n=>{r(a.isDisposed);let o=Object.keys(n);for(let n of o)if(n in i)throw new t(e.PLUGIN_CONFLICT,{message:`Cannot extend router: property "${n}" already exists`});for(let e of o)i[e]=n[e];let s={keys:o};a.routerExtensions.push(s);let c=!1;return()=>{if(c)return;c=!0;for(let e of s.keys)delete i[e];let e=a.routerExtensions.indexOf(s);e!==-1&&a.routerExtensions.splice(e,1)}}}}export{r as n,i as t};
|
|
2
|
+
//# sourceMappingURL=getPluginApi-BvOUPp3g.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getPluginApi-
|
|
1
|
+
{"version":3,"file":"getPluginApi-BvOUPp3g.mjs","names":[],"sources":["../../src/api/helpers.ts","../../src/api/getPluginApi.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","import { throwIfDisposed } from \"./helpers\";\nimport { errorCodes } from \"../constants\";\nimport { getInternals } from \"../internals\";\nimport { RouterError } from \"../RouterError\";\n\nimport type { PluginApi } from \"./types\";\nimport type { DefaultDependencies, Params, Router } from \"@real-router/types\";\n\nexport function getPluginApi<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(router: Router<Dependencies>): PluginApi {\n const ctx = getInternals(router);\n\n return {\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\">>\n | 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 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 return () => {\n const index = list.indexOf(fn);\n\n if (index !== -1) {\n list.splice(index, 1);\n }\n };\n },\n getRouteConfig: (name) => {\n const store = ctx.routeGetStore();\n\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 if (idx !== -1) {\n ctx.routerExtensions.splice(idx, 1);\n }\n };\n },\n };\n}\n"],"mappings":"8FAKA,SAAgB,EAAgB,EAAiC,CAC/D,GAAI,GAAY,CACd,MAAM,IAAI,EAAY,EAAW,gBAAgB,CCCrD,SAAgB,EAEd,EAAyC,CACzC,IAAM,EAAM,EAAa,EAAO,CAEhC,MAAO,CACL,WAAY,EAAM,EAAQ,EAAM,KAC9B,EAAI,WAAW,MAAM,sBAAsB,EAAM,EAAQ,EAAK,CAEvD,EAAI,UACT,EACA,EACA,EACA,GAAM,OAGP,EAEH,YAAa,EAAW,IAAgB,CACtC,EAAI,WAAW,OAAO,yBACpB,EACA,EACA,aACD,CAED,GAAM,CAAE,OAAM,UAAW,EAAI,aAAa,EAAW,EAAY,CAEjE,OAAO,EAAI,mBAAmB,EAAM,EAAO,EAE7C,cACE,EACA,KAEA,EAAI,WAAW,OAAO,yBACpB,EACA,EACA,eACD,CAEM,EAAI,aAAa,EAAW,EAAY,EAEjD,UAAY,IACV,EAAI,WAAW,OAAO,sBAAsB,EAAK,CAE1C,EAAI,UAAU,EAAM,EAAI,YAAY,CAAC,EAE9C,YAAc,GAAa,CACzB,EAAgB,EAAI,WAAW,CAE/B,EAAI,WAAW,OAAO,wBAAwB,EAAS,CAEvD,EAAI,YAAY,EAAS,EAE3B,YAAa,EAAI,YACjB,kBAAmB,EAAW,KAC5B,EAAgB,EAAI,WAAW,CAE/B,EAAI,WAAW,SAAS,qBAAqB,EAAW,EAAG,CAEpD,EAAI,iBAAiB,EAAW,EAAG,EAE5C,sBAAuB,EAAM,EAAS,EAAE,GAAK,CAC3C,EAAI,WAAW,OAAO,yBACpB,EACA,EACA,uBACD,CAED,GAAM,CAAE,KAAM,EAAc,OAAQ,GAAmB,EAAI,aACzD,EACA,EACD,CACK,EAAY,EAAI,mBAAmB,EAAc,EAAe,CAEjE,KAIL,OAAO,EAAI,UACT,EAAU,KACV,EAAU,OACV,EAAI,UAAU,EAAU,KAAM,EAAU,OAAO,CAC/C,EAAU,KACX,EAEH,WAAY,EAAI,WAChB,QAAS,EAAI,QACb,gBAAiB,EAAQ,IAAO,CAC9B,EAAgB,EAAI,WAAW,CAC/B,EAAI,WAAW,QAAQ,2BAA2B,EAAQ,EAAG,CAC7D,IAAI,EAAO,EAAI,aAAa,IAAI,EAAO,CASvC,OAPK,IACH,EAAO,EAAE,CACT,EAAI,aAAa,IAAI,EAAQ,EAAK,EAGpC,EAAK,KAAK,EAAG,KAEA,CACX,IAAM,EAAQ,EAAK,QAAQ,EAAG,CAE1B,IAAU,IACZ,EAAK,OAAO,EAAO,EAAE,GAI3B,eAAiB,GAAS,CACxB,IAAM,EAAQ,EAAI,eAAe,CAE5B,KAAM,QAAQ,SAAS,EAAK,CAIjC,OAAO,EAAM,kBAAkB,IAEjC,aAAe,GAAwC,CACrD,EAAgB,EAAI,WAAW,CAE/B,IAAM,EAAO,OAAO,KAAK,EAAW,CAEpC,IAAK,IAAM,KAAO,EAChB,GAAI,KAAO,EACT,MAAM,IAAI,EAAY,EAAW,gBAAiB,CAChD,QAAS,mCAAmC,EAAI,kBACjD,CAAC,CAIN,IAAK,IAAM,KAAO,EACf,EAAmC,GAAO,EAAW,GAGxD,IAAM,EAAkB,CAAE,OAAM,CAEhC,EAAI,iBAAiB,KAAK,EAAgB,CAE1C,IAAI,EAAU,GAEd,UAAa,CACX,GAAI,EACF,OAGF,EAAU,GAEV,IAAK,IAAM,KAAO,EAAgB,KAChC,OAAQ,EAAmC,GAG7C,IAAM,EAAM,EAAI,iBAAiB,QAAQ,EAAgB,CAErD,IAAQ,IACV,EAAI,iBAAiB,OAAO,EAAK,EAAE,GAI1C"}
|
package/dist/esm/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as RouteConfigUpdate, f as RouteTree, i as GuardFnFactory, n as BuildStateResultWithSegments, o as PluginFactory, s as Route, t as Router } from "./Router-
|
|
1
|
+
import { c as RouteConfigUpdate, f as RouteTree, i as GuardFnFactory, n as BuildStateResultWithSegments, o as PluginFactory, s as Route, t as Router } from "./Router-BPkXwb1J.mjs";
|
|
2
2
|
import { t as RouterValidator } from "./RouterValidator-DphcVMEp.mjs";
|
|
3
3
|
import { Config, DefaultDependencies, DefaultDependencies as DefaultDependencies$1, ErrorCodeKeys, ErrorCodeToValueMap, ErrorCodeValues, EventToNameMap, GuardFn, Listener, NavigationOptions, Navigator, Navigator as Navigator$1, Options, Options as Options$1, Params, Plugin, Router as Router$1, SimpleState, State, State as State$1, SubscribeFn, SubscribeState, Subscription, Unsubscribe } from "@real-router/types";
|
|
4
4
|
|
package/dist/esm/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as e,t}from"./Router-
|
|
1
|
+
import{a as e,t}from"./Router-C3BXH7z8.mjs";import{c as n,l as r,o as i,s as a,t as o}from"./RouterError-BOUkCIgf.mjs";const s=(e=[],n={},r={})=>new t(e,n,r),c=new WeakMap,l=e=>{let t=c.get(e);return t||(t=Object.freeze({navigate:e.navigate,getState:e.getState,isActiveRoute:e.isActiveRoute,canNavigateTo:e.canNavigateTo,subscribe:e.subscribe,subscribeLeave:e.subscribeLeave,isLeaveApproved:e.isLeaveApproved}),c.set(e,t)),t};export{t as Router,o as RouterError,i as UNKNOWN_ROUTE,a as constants,s as createRouter,n as errorCodes,r as events,l as getNavigator,e as resolveForwardChain};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/esm/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/createRouter.ts","../../src/getNavigator.ts"],"sourcesContent":["// packages/core/src/createRouter.ts\n\nimport { Router } from \"./Router\";\n\nimport type { Route } from \"./types\";\nimport type { DefaultDependencies, Options } from \"@real-router/types\";\n\n/**\n * Creates a new router instance.\n *\n * @param routes - Array of route definitions\n * @param options - Router configuration options\n * @param dependencies - Dependencies to inject into the router\n * @returns A new Router instance\n *\n * @example\n * const router = createRouter([\n * { name: 'home', path: '/' },\n * { name: 'users', path: '/users' },\n * ]);\n *\n * router.start('/');\n */\nexport const createRouter = <\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n routes: Route<Dependencies>[] = [],\n options: Partial<Options> = {},\n dependencies: Dependencies = {} as Dependencies,\n): Router<Dependencies> => {\n return new Router<Dependencies>(routes, options, dependencies);\n};\n","import type {\n Navigator,\n DefaultDependencies,\n Router,\n} from \"@real-router/types\";\n\nconst cache = new WeakMap<Router, Navigator>();\n\nexport const getNavigator = <\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n router: Router<Dependencies>,\n): Navigator => {\n let nav = cache.get(router);\n\n if (!nav) {\n nav = Object.freeze({\n navigate: router.navigate,\n getState: router.getState,\n isActiveRoute: router.isActiveRoute,\n canNavigateTo: router.canNavigateTo,\n subscribe: router.subscribe,\n });\n cache.set(router, nav);\n }\n\n return nav;\n};\n"],"mappings":"uHAuBA,MAAa,GAGX,EAAgC,EAAE,CAClC,EAA4B,EAAE,CAC9B,EAA6B,EAAE,GAExB,IAAI,EAAqB,EAAQ,EAAS,EAAa,CCxB1D,EAAQ,IAAI,QAEL,EAGX,GACc,CACd,IAAI,EAAM,EAAM,IAAI,EAAO,
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/createRouter.ts","../../src/getNavigator.ts"],"sourcesContent":["// packages/core/src/createRouter.ts\n\nimport { Router } from \"./Router\";\n\nimport type { Route } from \"./types\";\nimport type { DefaultDependencies, Options } from \"@real-router/types\";\n\n/**\n * Creates a new router instance.\n *\n * @param routes - Array of route definitions\n * @param options - Router configuration options\n * @param dependencies - Dependencies to inject into the router\n * @returns A new Router instance\n *\n * @example\n * const router = createRouter([\n * { name: 'home', path: '/' },\n * { name: 'users', path: '/users' },\n * ]);\n *\n * router.start('/');\n */\nexport const createRouter = <\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n routes: Route<Dependencies>[] = [],\n options: Partial<Options> = {},\n dependencies: Dependencies = {} as Dependencies,\n): Router<Dependencies> => {\n return new Router<Dependencies>(routes, options, dependencies);\n};\n","import type {\n Navigator,\n DefaultDependencies,\n Router,\n} from \"@real-router/types\";\n\nconst cache = new WeakMap<Router, Navigator>();\n\nexport const getNavigator = <\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n router: Router<Dependencies>,\n): Navigator => {\n let nav = cache.get(router);\n\n if (!nav) {\n nav = Object.freeze({\n navigate: router.navigate,\n getState: router.getState,\n isActiveRoute: router.isActiveRoute,\n canNavigateTo: router.canNavigateTo,\n subscribe: router.subscribe,\n subscribeLeave: router.subscribeLeave,\n isLeaveApproved: router.isLeaveApproved,\n } as Navigator);\n cache.set(router, nav);\n }\n\n return nav;\n};\n"],"mappings":"uHAuBA,MAAa,GAGX,EAAgC,EAAE,CAClC,EAA4B,EAAE,CAC9B,EAA6B,EAAE,GAExB,IAAI,EAAqB,EAAQ,EAAS,EAAa,CCxB1D,EAAQ,IAAI,QAEL,EAGX,GACc,CACd,IAAI,EAAM,EAAM,IAAI,EAAO,CAe3B,OAbK,IACH,EAAM,OAAO,OAAO,CAClB,SAAU,EAAO,SACjB,SAAU,EAAO,SACjB,cAAe,EAAO,cACtB,cAAe,EAAO,cACtB,UAAW,EAAO,UAClB,eAAgB,EAAO,eACvB,gBAAiB,EAAO,gBACzB,CAAc,CACf,EAAM,IAAI,EAAQ,EAAI,EAGjB"}
|
package/dist/esm/utils.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./getPluginApi-
|
|
1
|
+
import{t as e}from"./getPluginApi-BvOUPp3g.mjs";function t(e){let n=[];for(let r of e.children.values())r.children.size===0?n.push(r.fullName):n.push(...t(r));return n}async function n(n,r){let i=t(e(n).getTree()),a=[];for(let e of i){let t=r?.[e];if(t){let r=await t();for(let t of r)a.push(n.buildPath(e,t))}else a.push(n.buildPath(e,{}))}return a}function r(e){return e===void 0?`null`:JSON.stringify(e).replaceAll(`<`,String.raw`\u003c`).replaceAll(`>`,String.raw`\u003e`).replaceAll(`&`,String.raw`\u0026`)}export{n as getStaticPaths,r as serializeState};
|
|
2
2
|
//# sourceMappingURL=utils.mjs.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as Limits, d as RouteDefinition, f as RouteTree, i as GuardFnFactory, l as CreateMatcherOptions, o as PluginFactory, r as EventMethodMap, u as Matcher } from "./Router-
|
|
1
|
+
import { a as Limits, d as RouteDefinition, f as RouteTree, i as GuardFnFactory, l as CreateMatcherOptions, o as PluginFactory, r as EventMethodMap, u as Matcher } from "./Router-BPkXwb1J.mjs";
|
|
2
2
|
import { t as RouterValidator } from "./RouterValidator-DphcVMEp.mjs";
|
|
3
3
|
import { DefaultDependencies, EventName, ForwardToCallback, GuardFn, Options, Params, Plugin, RouteTreeState, Router, SimpleState, State, Unsubscribe } from "@real-router/types";
|
|
4
4
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@real-router/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"description": "A simple, powerful, view-agnostic, modular and extensible router",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"dependencies": {
|
|
90
90
|
"@real-router/fsm": "^0.2.2",
|
|
91
91
|
"@real-router/logger": "^0.2.1",
|
|
92
|
-
"@real-router/types": "^0.
|
|
92
|
+
"@real-router/types": "^0.30.0"
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
95
|
"event-emitter": "^0.1.2",
|
package/src/Router.ts
CHANGED
|
@@ -41,6 +41,7 @@ import type { DependenciesStore } from "./namespaces";
|
|
|
41
41
|
import type { Limits, PluginFactory, Route, RouterEventMap } from "./types";
|
|
42
42
|
import type {
|
|
43
43
|
DefaultDependencies,
|
|
44
|
+
LeaveFn,
|
|
44
45
|
NavigationOptions,
|
|
45
46
|
Options,
|
|
46
47
|
Params,
|
|
@@ -290,6 +291,8 @@ export class Router<
|
|
|
290
291
|
|
|
291
292
|
// Subscription
|
|
292
293
|
this.subscribe = this.subscribe.bind(this);
|
|
294
|
+
this.subscribeLeave = this.subscribeLeave.bind(this);
|
|
295
|
+
this.isLeaveApproved = this.isLeaveApproved.bind(this);
|
|
293
296
|
}
|
|
294
297
|
|
|
295
298
|
// ============================================================================
|
|
@@ -410,7 +413,7 @@ export class Router<
|
|
|
410
413
|
|
|
411
414
|
stop(): this {
|
|
412
415
|
this.#navigation.abortCurrentNavigation();
|
|
413
|
-
this.#eventBus.
|
|
416
|
+
this.#eventBus.sendCancelIfPossible(this.#state.get());
|
|
414
417
|
|
|
415
418
|
if (!this.#eventBus.isReady() && !this.#eventBus.isTransitioning()) {
|
|
416
419
|
return this;
|
|
@@ -428,7 +431,7 @@ export class Router<
|
|
|
428
431
|
}
|
|
429
432
|
|
|
430
433
|
this.#navigation.abortCurrentNavigation();
|
|
431
|
-
this.#eventBus.
|
|
434
|
+
this.#eventBus.sendCancelIfPossible(this.#state.get());
|
|
432
435
|
|
|
433
436
|
if (this.#eventBus.isReady() || this.#eventBus.isTransitioning()) {
|
|
434
437
|
this.#lifecycle.stop();
|
|
@@ -531,6 +534,16 @@ export class Router<
|
|
|
531
534
|
return this.#eventBus.subscribe(listener);
|
|
532
535
|
}
|
|
533
536
|
|
|
537
|
+
subscribeLeave(listener: LeaveFn): Unsubscribe {
|
|
538
|
+
EventBusNamespace.validateSubscribeLeaveListener(listener);
|
|
539
|
+
|
|
540
|
+
return this.#eventBus.subscribeLeave(listener);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
isLeaveApproved(): boolean {
|
|
544
|
+
return this.#eventBus.isLeaveApproved();
|
|
545
|
+
}
|
|
546
|
+
|
|
534
547
|
// ============================================================================
|
|
535
548
|
// Navigation
|
|
536
549
|
// ============================================================================
|
|
@@ -645,6 +658,7 @@ export class Router<
|
|
|
645
658
|
this.usePlugin = throwDisposed as never;
|
|
646
659
|
|
|
647
660
|
this.subscribe = throwDisposed as never;
|
|
661
|
+
this.subscribeLeave = throwDisposed as never;
|
|
648
662
|
this.canNavigateTo = throwDisposed as never;
|
|
649
663
|
}
|
|
650
664
|
}
|
package/src/constants.ts
CHANGED
|
@@ -55,6 +55,7 @@ export const plugins: EventToPluginMap = {
|
|
|
55
55
|
ROUTER_START: "onStart", // Plugin method called when router starts
|
|
56
56
|
ROUTER_STOP: "onStop", // Plugin method called when router stops
|
|
57
57
|
TRANSITION_START: "onTransitionStart", // Plugin method called when navigation begins
|
|
58
|
+
TRANSITION_LEAVE_APPROVE: "onTransitionLeaveApprove", // Plugin method called when deactivation guards pass
|
|
58
59
|
TRANSITION_CANCEL: "onTransitionCancel", // Plugin method called when navigation cancelled
|
|
59
60
|
TRANSITION_SUCCESS: "onTransitionSuccess", // Plugin method called when navigation succeeds
|
|
60
61
|
TRANSITION_ERROR: "onTransitionError", // Plugin method called when navigation fails
|
|
@@ -68,6 +69,7 @@ export const events: EventToNameMap = {
|
|
|
68
69
|
ROUTER_START: "$start", // Emitted when router.start() succeeds
|
|
69
70
|
ROUTER_STOP: "$stop", // Emitted when router.stop() is called
|
|
70
71
|
TRANSITION_START: "$$start", // Emitted when navigation begins
|
|
72
|
+
TRANSITION_LEAVE_APPROVE: "$$leaveApprove", // Emitted when deactivation guards pass
|
|
71
73
|
TRANSITION_CANCEL: "$$cancel", // Emitted when navigation is cancelled
|
|
72
74
|
TRANSITION_SUCCESS: "$$success", // Emitted when navigation completes successfully
|
|
73
75
|
TRANSITION_ERROR: "$$error", // Emitted when navigation fails
|
package/src/fsm/routerFSM.ts
CHANGED
|
@@ -10,14 +10,16 @@ import type { FSMConfig } from "@real-router/fsm";
|
|
|
10
10
|
* - IDLE: Router not started or stopped
|
|
11
11
|
* - STARTING: Router is initializing
|
|
12
12
|
* - READY: Router is ready for navigation
|
|
13
|
-
* -
|
|
13
|
+
* - TRANSITION_STARTED: Navigation in progress (before deactivation guards)
|
|
14
|
+
* - LEAVE_APPROVED: Deactivation guards passed, activation guards pending
|
|
14
15
|
* - DISPOSED: Router has been disposed (R2+)
|
|
15
16
|
*/
|
|
16
17
|
export const routerStates = {
|
|
17
18
|
IDLE: "IDLE",
|
|
18
19
|
STARTING: "STARTING",
|
|
19
20
|
READY: "READY",
|
|
20
|
-
|
|
21
|
+
TRANSITION_STARTED: "TRANSITION_STARTED",
|
|
22
|
+
LEAVE_APPROVED: "LEAVE_APPROVED",
|
|
21
23
|
DISPOSED: "DISPOSED",
|
|
22
24
|
} as const;
|
|
23
25
|
|
|
@@ -39,6 +41,7 @@ export const routerEvents = {
|
|
|
39
41
|
START: "START",
|
|
40
42
|
STARTED: "STARTED",
|
|
41
43
|
NAVIGATE: "NAVIGATE",
|
|
44
|
+
LEAVE_APPROVE: "LEAVE_APPROVE",
|
|
42
45
|
COMPLETE: "COMPLETE",
|
|
43
46
|
FAIL: "FAIL",
|
|
44
47
|
CANCEL: "CANCEL",
|
|
@@ -62,8 +65,9 @@ export interface RouterPayloads {}
|
|
|
62
65
|
* Transitions:
|
|
63
66
|
* - IDLE → STARTING (START), DISPOSED (DISPOSE)
|
|
64
67
|
* - STARTING → READY (STARTED), IDLE (FAIL)
|
|
65
|
-
* - READY →
|
|
66
|
-
* -
|
|
68
|
+
* - READY → TRANSITION_STARTED (NAVIGATE), READY (FAIL, self-loop for early validation errors), IDLE (STOP)
|
|
69
|
+
* - TRANSITION_STARTED → LEAVE_APPROVED (LEAVE_APPROVE), TRANSITION_STARTED (NAVIGATE, self-loop), READY (CANCEL, FAIL)
|
|
70
|
+
* - LEAVE_APPROVED → READY (COMPLETE, CANCEL, FAIL), TRANSITION_STARTED (NAVIGATE)
|
|
67
71
|
* - DISPOSED → (no transitions)
|
|
68
72
|
*/
|
|
69
73
|
const routerFSMConfig: FSMConfig<RouterState, RouterEvent, null> = {
|
|
@@ -79,12 +83,18 @@ const routerFSMConfig: FSMConfig<RouterState, RouterEvent, null> = {
|
|
|
79
83
|
[routerEvents.FAIL]: routerStates.IDLE,
|
|
80
84
|
},
|
|
81
85
|
[routerStates.READY]: {
|
|
82
|
-
[routerEvents.NAVIGATE]: routerStates.
|
|
86
|
+
[routerEvents.NAVIGATE]: routerStates.TRANSITION_STARTED,
|
|
83
87
|
[routerEvents.FAIL]: routerStates.READY,
|
|
84
88
|
[routerEvents.STOP]: routerStates.IDLE,
|
|
85
89
|
},
|
|
86
|
-
[routerStates.
|
|
87
|
-
[routerEvents.NAVIGATE]: routerStates.
|
|
90
|
+
[routerStates.TRANSITION_STARTED]: {
|
|
91
|
+
[routerEvents.NAVIGATE]: routerStates.TRANSITION_STARTED,
|
|
92
|
+
[routerEvents.LEAVE_APPROVE]: routerStates.LEAVE_APPROVED,
|
|
93
|
+
[routerEvents.CANCEL]: routerStates.READY,
|
|
94
|
+
[routerEvents.FAIL]: routerStates.READY,
|
|
95
|
+
},
|
|
96
|
+
[routerStates.LEAVE_APPROVED]: {
|
|
97
|
+
[routerEvents.NAVIGATE]: routerStates.TRANSITION_STARTED,
|
|
88
98
|
[routerEvents.COMPLETE]: routerStates.READY,
|
|
89
99
|
[routerEvents.CANCEL]: routerStates.READY,
|
|
90
100
|
[routerEvents.FAIL]: routerStates.READY,
|
package/src/getNavigator.ts
CHANGED
|
@@ -20,7 +20,9 @@ export const getNavigator = <
|
|
|
20
20
|
isActiveRoute: router.isActiveRoute,
|
|
21
21
|
canNavigateTo: router.canNavigateTo,
|
|
22
22
|
subscribe: router.subscribe,
|
|
23
|
-
|
|
23
|
+
subscribeLeave: router.subscribeLeave,
|
|
24
|
+
isLeaveApproved: router.isLeaveApproved,
|
|
25
|
+
} as Navigator);
|
|
24
26
|
cache.set(router, nav);
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -10,6 +10,7 @@ import type { EventMethodMap, RouterEventMap } from "../../types";
|
|
|
10
10
|
import type { FSM } from "@real-router/fsm";
|
|
11
11
|
import type {
|
|
12
12
|
EventName,
|
|
13
|
+
LeaveFn,
|
|
13
14
|
NavigationOptions,
|
|
14
15
|
Plugin,
|
|
15
16
|
State,
|
|
@@ -43,6 +44,12 @@ export class EventBusNamespace {
|
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
static validateSubscribeLeaveListener(listener: unknown): void {
|
|
48
|
+
if (typeof listener !== "function") {
|
|
49
|
+
throw new TypeError("[router.subscribeLeave] Expected a function");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
emitRouterStart(): void {
|
|
47
54
|
this.#emitter.emit(events.ROUTER_START);
|
|
48
55
|
}
|
|
@@ -75,6 +82,10 @@ export class EventBusNamespace {
|
|
|
75
82
|
this.#emitter.emit(events.TRANSITION_CANCEL, toState, fromState);
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
emitTransitionLeaveApprove(toState: State, fromState?: State): void {
|
|
86
|
+
this.#emitter.emit(events.TRANSITION_LEAVE_APPROVE, toState, fromState);
|
|
87
|
+
}
|
|
88
|
+
|
|
78
89
|
sendStart(): void {
|
|
79
90
|
this.#fsm.send(routerEvents.START);
|
|
80
91
|
}
|
|
@@ -94,7 +105,7 @@ export class EventBusNamespace {
|
|
|
94
105
|
sendNavigate(toState: State, fromState?: State): void {
|
|
95
106
|
this.#currentToState = toState;
|
|
96
107
|
// Bypass FSM dispatch — forceState + direct emit (no action lookup, no rest params)
|
|
97
|
-
this.#fsm.forceState(routerStates.
|
|
108
|
+
this.#fsm.forceState(routerStates.TRANSITION_STARTED);
|
|
98
109
|
this.emitTransitionStart(toState, fromState);
|
|
99
110
|
}
|
|
100
111
|
|
|
@@ -112,6 +123,12 @@ export class EventBusNamespace {
|
|
|
112
123
|
}
|
|
113
124
|
}
|
|
114
125
|
|
|
126
|
+
sendLeaveApprove(toState: State, fromState?: State): void {
|
|
127
|
+
// Bypass FSM dispatch — forceState + direct emit (no action lookup, no rest params)
|
|
128
|
+
this.#fsm.forceState(routerStates.LEAVE_APPROVED);
|
|
129
|
+
this.emitTransitionLeaveApprove(toState, fromState);
|
|
130
|
+
}
|
|
131
|
+
|
|
115
132
|
sendFail(toState?: State, fromState?: State, error?: unknown): void {
|
|
116
133
|
const prev = this.#currentToState;
|
|
117
134
|
|
|
@@ -168,7 +185,16 @@ export class EventBusNamespace {
|
|
|
168
185
|
}
|
|
169
186
|
|
|
170
187
|
isTransitioning(): boolean {
|
|
171
|
-
|
|
188
|
+
const state = this.#fsm.getState();
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
state === routerStates.TRANSITION_STARTED ||
|
|
192
|
+
state === routerStates.LEAVE_APPROVED
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
isLeaveApproved(): boolean {
|
|
197
|
+
return this.#fsm.getState() === routerStates.LEAVE_APPROVED;
|
|
172
198
|
}
|
|
173
199
|
|
|
174
200
|
isReady(): boolean {
|
|
@@ -198,6 +224,17 @@ export class EventBusNamespace {
|
|
|
198
224
|
);
|
|
199
225
|
}
|
|
200
226
|
|
|
227
|
+
subscribeLeave(listener: LeaveFn): Unsubscribe {
|
|
228
|
+
return this.#emitter.on(
|
|
229
|
+
events.TRANSITION_LEAVE_APPROVE,
|
|
230
|
+
(toState: State, fromState?: State) => {
|
|
231
|
+
if (fromState !== undefined) {
|
|
232
|
+
listener({ route: fromState, nextRoute: toState });
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
201
238
|
clearAll(): void {
|
|
202
239
|
this.#emitter.clearAll();
|
|
203
240
|
}
|
|
@@ -210,12 +247,14 @@ export class EventBusNamespace {
|
|
|
210
247
|
this.#emitter.setLimits(limits);
|
|
211
248
|
}
|
|
212
249
|
|
|
213
|
-
|
|
214
|
-
|
|
250
|
+
sendCancelIfPossible(fromState: State | undefined): void {
|
|
251
|
+
const toState = this.#currentToState;
|
|
252
|
+
|
|
253
|
+
if (!this.canCancel() || toState === undefined) {
|
|
215
254
|
return;
|
|
216
255
|
}
|
|
217
256
|
|
|
218
|
-
this.sendCancel(
|
|
257
|
+
this.sendCancel(toState, fromState);
|
|
219
258
|
}
|
|
220
259
|
|
|
221
260
|
#emitPendingError(): void {
|
|
@@ -239,7 +278,18 @@ export class EventBusNamespace {
|
|
|
239
278
|
|
|
240
279
|
// NAVIGATE and COMPLETE actions bypassed — sendNavigate/sendComplete
|
|
241
280
|
// use fsm.forceState() + direct emit for zero-allocation hot path.
|
|
242
|
-
fsm.on(routerStates.
|
|
281
|
+
fsm.on(routerStates.TRANSITION_STARTED, routerEvents.CANCEL, () => {
|
|
282
|
+
const toState = this.#pendingToState;
|
|
283
|
+
|
|
284
|
+
/* v8 ignore next -- @preserve: #pendingToState guaranteed set by sendCancel before send() */
|
|
285
|
+
if (toState === undefined) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
this.emitTransitionCancel(toState, this.#pendingFromState);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
fsm.on(routerStates.LEAVE_APPROVED, routerEvents.CANCEL, () => {
|
|
243
293
|
const toState = this.#pendingToState;
|
|
244
294
|
|
|
245
295
|
/* v8 ignore next -- @preserve: #pendingToState guaranteed set by sendCancel before send() */
|
|
@@ -250,6 +300,10 @@ export class EventBusNamespace {
|
|
|
250
300
|
this.emitTransitionCancel(toState, this.#pendingFromState);
|
|
251
301
|
});
|
|
252
302
|
|
|
303
|
+
fsm.on(routerStates.LEAVE_APPROVED, routerEvents.FAIL, () => {
|
|
304
|
+
this.#emitPendingError();
|
|
305
|
+
});
|
|
306
|
+
|
|
253
307
|
fsm.on(routerStates.STARTING, routerEvents.FAIL, () => {
|
|
254
308
|
this.#emitPendingError();
|
|
255
309
|
});
|
|
@@ -258,7 +312,7 @@ export class EventBusNamespace {
|
|
|
258
312
|
this.#emitPendingError();
|
|
259
313
|
});
|
|
260
314
|
|
|
261
|
-
fsm.on(routerStates.
|
|
315
|
+
fsm.on(routerStates.TRANSITION_STARTED, routerEvents.FAIL, () => {
|
|
262
316
|
this.#emitPendingError();
|
|
263
317
|
});
|
|
264
318
|
}
|
|
@@ -124,13 +124,7 @@ export class NavigationNamespace {
|
|
|
124
124
|
return CACHED_SAME_STATES_REJECTION;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
this.#abortPreviousNavigation();
|
|
128
|
-
|
|
129
|
-
if (opts.signal?.aborted) {
|
|
130
|
-
throw new RouterError(errorCodes.TRANSITION_CANCELLED, {
|
|
131
|
-
reason: opts.signal.reason,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
127
|
+
this.#abortPreviousNavigation(opts.signal);
|
|
134
128
|
|
|
135
129
|
const myId = ++this.#navigationId;
|
|
136
130
|
|
|
@@ -157,13 +151,30 @@ export class NavigationNamespace {
|
|
|
157
151
|
const hasGuards =
|
|
158
152
|
canDeactivateFunctions.size > 0 || canActivateFunctions.size > 0;
|
|
159
153
|
|
|
154
|
+
const confirmedToState = toState;
|
|
155
|
+
const emitLeaveApproveCallback = () => {
|
|
156
|
+
deps.sendLeaveApprove(confirmedToState, fromState);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const isCurrentNav = () => this.#navigationId === myId && deps.isActive();
|
|
160
|
+
|
|
161
|
+
if (!hasGuards) {
|
|
162
|
+
// No guards — emitLeaveApprove directly before completeTransition
|
|
163
|
+
emitLeaveApproveCallback();
|
|
164
|
+
|
|
165
|
+
// Reentrant check: subscribeLeave() listener may have called router.navigate()
|
|
166
|
+
// Same pattern as reentrant check after emitTransitionStart (line 141-143)
|
|
167
|
+
/* v8 ignore next 3 -- @preserve: reentrant navigate from TRANSITION_LEAVE_APPROVE listener; tested but V8 cannot track the branch through the synchronous callback chain */
|
|
168
|
+
if (this.#navigationId !== myId) {
|
|
169
|
+
throw new RouterError(errorCodes.TRANSITION_CANCELLED);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
160
173
|
if (hasGuards) {
|
|
161
174
|
controller = new AbortController();
|
|
162
175
|
this.#currentController = controller;
|
|
163
176
|
|
|
164
177
|
const signal = controller.signal;
|
|
165
|
-
const isCurrentNav = () =>
|
|
166
|
-
this.#navigationId === myId && deps.isActive();
|
|
167
178
|
|
|
168
179
|
const guardCompletion = executeGuardPipeline(
|
|
169
180
|
canDeactivateFunctions,
|
|
@@ -176,6 +187,7 @@ export class NavigationNamespace {
|
|
|
176
187
|
fromState,
|
|
177
188
|
signal,
|
|
178
189
|
isCurrentNav,
|
|
190
|
+
emitLeaveApproveCallback,
|
|
179
191
|
);
|
|
180
192
|
|
|
181
193
|
if (guardCompletion !== undefined) {
|
|
@@ -371,7 +383,7 @@ export class NavigationNamespace {
|
|
|
371
383
|
}
|
|
372
384
|
}
|
|
373
385
|
|
|
374
|
-
#abortPreviousNavigation(): void {
|
|
386
|
+
#abortPreviousNavigation(externalSignal?: AbortSignal): void {
|
|
375
387
|
if (this.#deps.isTransitioning()) {
|
|
376
388
|
logger.warn(
|
|
377
389
|
"router.navigate",
|
|
@@ -383,5 +395,11 @@ export class NavigationNamespace {
|
|
|
383
395
|
);
|
|
384
396
|
this.#deps.cancelNavigation();
|
|
385
397
|
}
|
|
398
|
+
|
|
399
|
+
if (externalSignal?.aborted) {
|
|
400
|
+
throw new RouterError(errorCodes.TRANSITION_CANCELLED, {
|
|
401
|
+
reason: externalSignal.reason,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
386
404
|
}
|
|
387
405
|
}
|