@real-router/core 0.22.0 → 0.23.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.
Files changed (80) hide show
  1. package/README.md +1 -3
  2. package/dist/cjs/index.d.ts +1 -1
  3. package/dist/cjs/index.js +1 -1
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/metafile-cjs.json +1 -1
  6. package/dist/esm/index.d.mts +1 -1
  7. package/dist/esm/index.mjs +1 -1
  8. package/dist/esm/index.mjs.map +1 -1
  9. package/dist/esm/metafile-esm.json +1 -1
  10. package/package.json +7 -5
  11. package/src/Router.ts +1174 -0
  12. package/src/RouterError.ts +324 -0
  13. package/src/constants.ts +112 -0
  14. package/src/createRouter.ts +32 -0
  15. package/src/fsm/index.ts +5 -0
  16. package/src/fsm/routerFSM.ts +129 -0
  17. package/src/getNavigator.ts +15 -0
  18. package/src/helpers.ts +194 -0
  19. package/src/index.ts +46 -0
  20. package/src/namespaces/CloneNamespace/CloneNamespace.ts +120 -0
  21. package/src/namespaces/CloneNamespace/index.ts +3 -0
  22. package/src/namespaces/CloneNamespace/types.ts +46 -0
  23. package/src/namespaces/DependenciesNamespace/DependenciesNamespace.ts +250 -0
  24. package/src/namespaces/DependenciesNamespace/index.ts +3 -0
  25. package/src/namespaces/DependenciesNamespace/validators.ts +105 -0
  26. package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +272 -0
  27. package/src/namespaces/EventBusNamespace/index.ts +5 -0
  28. package/src/namespaces/EventBusNamespace/types.ts +11 -0
  29. package/src/namespaces/MiddlewareNamespace/MiddlewareNamespace.ts +206 -0
  30. package/src/namespaces/MiddlewareNamespace/index.ts +5 -0
  31. package/src/namespaces/MiddlewareNamespace/types.ts +28 -0
  32. package/src/namespaces/MiddlewareNamespace/validators.ts +96 -0
  33. package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +308 -0
  34. package/src/namespaces/NavigationNamespace/index.ts +5 -0
  35. package/src/namespaces/NavigationNamespace/transition/executeLifecycleHooks.ts +84 -0
  36. package/src/namespaces/NavigationNamespace/transition/executeMiddleware.ts +56 -0
  37. package/src/namespaces/NavigationNamespace/transition/index.ts +107 -0
  38. package/src/namespaces/NavigationNamespace/transition/makeError.ts +37 -0
  39. package/src/namespaces/NavigationNamespace/transition/mergeStates.ts +54 -0
  40. package/src/namespaces/NavigationNamespace/transition/processLifecycleResult.ts +81 -0
  41. package/src/namespaces/NavigationNamespace/transition/wrapSyncError.ts +82 -0
  42. package/src/namespaces/NavigationNamespace/types.ts +129 -0
  43. package/src/namespaces/NavigationNamespace/validators.ts +87 -0
  44. package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +50 -0
  45. package/src/namespaces/OptionsNamespace/constants.ts +41 -0
  46. package/src/namespaces/OptionsNamespace/helpers.ts +51 -0
  47. package/src/namespaces/OptionsNamespace/index.ts +11 -0
  48. package/src/namespaces/OptionsNamespace/validators.ts +252 -0
  49. package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +325 -0
  50. package/src/namespaces/PluginsNamespace/constants.ts +35 -0
  51. package/src/namespaces/PluginsNamespace/index.ts +7 -0
  52. package/src/namespaces/PluginsNamespace/types.ts +32 -0
  53. package/src/namespaces/PluginsNamespace/validators.ts +79 -0
  54. package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +389 -0
  55. package/src/namespaces/RouteLifecycleNamespace/index.ts +5 -0
  56. package/src/namespaces/RouteLifecycleNamespace/types.ts +17 -0
  57. package/src/namespaces/RouteLifecycleNamespace/validators.ts +65 -0
  58. package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +140 -0
  59. package/src/namespaces/RouterLifecycleNamespace/constants.ts +25 -0
  60. package/src/namespaces/RouterLifecycleNamespace/index.ts +5 -0
  61. package/src/namespaces/RouterLifecycleNamespace/types.ts +23 -0
  62. package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +1482 -0
  63. package/src/namespaces/RoutesNamespace/constants.ts +14 -0
  64. package/src/namespaces/RoutesNamespace/helpers.ts +532 -0
  65. package/src/namespaces/RoutesNamespace/index.ts +9 -0
  66. package/src/namespaces/RoutesNamespace/stateBuilder.ts +70 -0
  67. package/src/namespaces/RoutesNamespace/types.ts +82 -0
  68. package/src/namespaces/RoutesNamespace/validators.ts +331 -0
  69. package/src/namespaces/StateNamespace/StateNamespace.ts +317 -0
  70. package/src/namespaces/StateNamespace/helpers.ts +43 -0
  71. package/src/namespaces/StateNamespace/index.ts +5 -0
  72. package/src/namespaces/StateNamespace/types.ts +15 -0
  73. package/src/namespaces/index.ts +42 -0
  74. package/src/transitionPath.ts +441 -0
  75. package/src/typeGuards.ts +74 -0
  76. package/src/types.ts +194 -0
  77. package/src/wiring/RouterWiringBuilder.ts +235 -0
  78. package/src/wiring/index.ts +7 -0
  79. package/src/wiring/types.ts +53 -0
  80. package/src/wiring/wireRouter.ts +29 -0
@@ -0,0 +1,331 @@
1
+ // packages/core/src/namespaces/RoutesNamespace/validators.ts
2
+
3
+ /**
4
+ * Static validation functions for RoutesNamespace.
5
+ * Called by Router facade before instance methods.
6
+ *
7
+ * Extracted from RoutesNamespace class for better separation of concerns.
8
+ */
9
+
10
+ import { validateRoute } from "route-tree";
11
+ import {
12
+ isString,
13
+ validateRouteName,
14
+ isParams,
15
+ getTypeDescription,
16
+ } from "type-guards";
17
+
18
+ import { validateRouteProperties, validateForwardToTargets } from "./helpers";
19
+
20
+ import type { Route, RouteConfigUpdate } from "../../types";
21
+ import type { DefaultDependencies } from "@real-router/types";
22
+ import type { RouteTree } from "route-tree";
23
+
24
+ /**
25
+ * Validates removeRoute arguments.
26
+ */
27
+ export function validateRemoveRouteArgs(name: unknown): asserts name is string {
28
+ validateRouteName(name, "removeRoute");
29
+ }
30
+
31
+ /**
32
+ * Validates setRootPath arguments.
33
+ */
34
+ export function validateSetRootPathArgs(
35
+ rootPath: unknown,
36
+ ): asserts rootPath is string {
37
+ if (typeof rootPath !== "string") {
38
+ throw new TypeError(
39
+ `[router.setRootPath] rootPath must be a string, got ${getTypeDescription(rootPath)}`,
40
+ );
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Validates addRoute arguments (route structure and properties).
46
+ * State-dependent validation (duplicates, tree) happens in instance method.
47
+ */
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Route type
49
+ export function validateAddRouteArgs(routes: readonly Route<any>[]): void {
50
+ for (const route of routes) {
51
+ // First check if route is an object (before accessing route.name)
52
+ // Runtime check for invalid types passed via `as any`
53
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime check
54
+ if (route === null || typeof route !== "object" || Array.isArray(route)) {
55
+ throw new TypeError(
56
+ `[router.addRoute] Route must be an object, got ${getTypeDescription(route)}`,
57
+ );
58
+ }
59
+
60
+ // Validate route properties (canActivate, canDeactivate, defaultParams, async checks)
61
+ // Note: validateRouteProperties handles children recursively
62
+ validateRouteProperties(route, route.name);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Validates parent option for addRoute.
68
+ */
69
+ export function validateParentOption(
70
+ parent: unknown,
71
+ ): asserts parent is string {
72
+ if (typeof parent !== "string" || parent === "") {
73
+ throw new TypeError(
74
+ `[router.addRoute] parent option must be a non-empty string, got ${getTypeDescription(parent)}`,
75
+ );
76
+ }
77
+
78
+ // Validate parent is a valid route name format (can contain dots — it's a fullName reference)
79
+ validateRouteName(parent, "addRoute");
80
+ }
81
+
82
+ /**
83
+ * Validates isActiveRoute arguments.
84
+ */
85
+ export function validateIsActiveRouteArgs(
86
+ name: unknown,
87
+ params: unknown,
88
+ strictEquality: unknown,
89
+ ignoreQueryParams: unknown,
90
+ ): void {
91
+ // Validate name - non-string throws
92
+ if (!isString(name)) {
93
+ throw new TypeError(`Route name must be a string`);
94
+ }
95
+
96
+ // Validate params if provided
97
+ if (params !== undefined && !isParams(params)) {
98
+ throw new TypeError(`[router.isActiveRoute] Invalid params structure`);
99
+ }
100
+
101
+ // Validate strictEquality if provided
102
+ if (strictEquality !== undefined && typeof strictEquality !== "boolean") {
103
+ throw new TypeError(
104
+ `[router.isActiveRoute] strictEquality must be a boolean, got ${typeof strictEquality}`,
105
+ );
106
+ }
107
+
108
+ // Validate ignoreQueryParams if provided
109
+ if (
110
+ ignoreQueryParams !== undefined &&
111
+ typeof ignoreQueryParams !== "boolean"
112
+ ) {
113
+ throw new TypeError(
114
+ `[router.isActiveRoute] ignoreQueryParams must be a boolean, got ${typeof ignoreQueryParams}`,
115
+ );
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Validates forwardState/buildState arguments.
121
+ */
122
+ export function validateStateBuilderArgs(
123
+ routeName: unknown,
124
+ routeParams: unknown,
125
+ methodName: string,
126
+ ): void {
127
+ if (!isString(routeName)) {
128
+ throw new TypeError(
129
+ `[router.${methodName}] Invalid routeName: ${getTypeDescription(routeName)}. Expected string.`,
130
+ );
131
+ }
132
+
133
+ if (!isParams(routeParams)) {
134
+ throw new TypeError(
135
+ `[router.${methodName}] Invalid routeParams: ${getTypeDescription(routeParams)}. Expected plain object.`,
136
+ );
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Validates updateRoute basic arguments (name and updates object structure).
142
+ * Does NOT read property values to allow caller to cache them first.
143
+ */
144
+ export function validateUpdateRouteBasicArgs<
145
+ Dependencies extends DefaultDependencies,
146
+ >(
147
+ name: unknown,
148
+ updates: unknown,
149
+ ): asserts updates is RouteConfigUpdate<Dependencies> {
150
+ // Validate name
151
+ validateRouteName(name, "updateRoute");
152
+
153
+ if (name === "") {
154
+ throw new ReferenceError(
155
+ `[router.updateRoute] Invalid name: empty string. Cannot update root node.`,
156
+ );
157
+ }
158
+
159
+ // Validate updates is not null
160
+
161
+ if (updates === null) {
162
+ throw new TypeError(
163
+ `[real-router] updateRoute: updates must be an object, got null`,
164
+ );
165
+ }
166
+
167
+ // Validate updates is an object (not array)
168
+ if (typeof updates !== "object" || Array.isArray(updates)) {
169
+ throw new TypeError(
170
+ `[real-router] updateRoute: updates must be an object, got ${getTypeDescription(updates)}`,
171
+ );
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Asserts that a function is not async (native or transpiled).
177
+ * Checks both constructor name and toString() for __awaiter pattern.
178
+ */
179
+ /* v8 ignore next 12 -- @preserve: transpiled async (__awaiter) branch tested in addRoute */
180
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -- needs constructor.name access
181
+ function assertNotAsync(value: Function, paramName: string): void {
182
+ if (
183
+ (value as { constructor: { name: string } }).constructor.name ===
184
+ "AsyncFunction" ||
185
+ (value as { toString: () => string }).toString().includes("__awaiter")
186
+ ) {
187
+ throw new TypeError(
188
+ `[real-router] updateRoute: ${paramName} cannot be an async function`,
189
+ );
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Validates that a value is a non-async function, if provided.
195
+ */
196
+ function validateFunctionParam(value: unknown, paramName: string): void {
197
+ if (value === undefined || value === null) {
198
+ return;
199
+ }
200
+
201
+ if (typeof value !== "function") {
202
+ throw new TypeError(
203
+ `[real-router] updateRoute: ${paramName} must be a function or null, got ${typeof value}`,
204
+ );
205
+ }
206
+
207
+ assertNotAsync(value, paramName);
208
+ }
209
+
210
+ /**
211
+ * Validates updateRoute property types using pre-cached values.
212
+ * Called AFTER properties are cached to ensure getters are called only once.
213
+ */
214
+ export function validateUpdateRoutePropertyTypes(
215
+ forwardTo: unknown,
216
+ defaultParams: unknown,
217
+ decodeParams: unknown,
218
+ encodeParams: unknown,
219
+ ): void {
220
+ // Validate forwardTo type (existence check is done by instance method)
221
+ if (forwardTo !== undefined && forwardTo !== null) {
222
+ if (typeof forwardTo !== "string" && typeof forwardTo !== "function") {
223
+ throw new TypeError(
224
+ `[real-router] updateRoute: forwardTo must be a string, function, or null, got ${getTypeDescription(forwardTo)}`,
225
+ );
226
+ }
227
+
228
+ if (typeof forwardTo === "function") {
229
+ assertNotAsync(forwardTo, "forwardTo callback");
230
+ }
231
+ }
232
+
233
+ // Validate defaultParams
234
+ if (
235
+ defaultParams !== undefined &&
236
+ defaultParams !== null &&
237
+ (typeof defaultParams !== "object" || Array.isArray(defaultParams))
238
+ ) {
239
+ throw new TypeError(
240
+ `[real-router] updateRoute: defaultParams must be an object or null, got ${getTypeDescription(defaultParams)}`,
241
+ );
242
+ }
243
+
244
+ validateFunctionParam(decodeParams, "decodeParams");
245
+ validateFunctionParam(encodeParams, "encodeParams");
246
+ }
247
+
248
+ /**
249
+ * Validates buildPath arguments.
250
+ */
251
+ export function validateBuildPathArgs(route: unknown): asserts route is string {
252
+ if (!isString(route) || route === "") {
253
+ throw new TypeError(
254
+ `[real-router] buildPath: route must be a non-empty string, got ${typeof route === "string" ? '""' : typeof route}`,
255
+ );
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Validates matchPath arguments.
261
+ */
262
+ export function validateMatchPathArgs(path: unknown): asserts path is string {
263
+ if (!isString(path)) {
264
+ throw new TypeError(
265
+ `[real-router] matchPath: path must be a string, got ${typeof path}`,
266
+ );
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Validates shouldUpdateNode arguments.
272
+ */
273
+ export function validateShouldUpdateNodeArgs(
274
+ nodeName: unknown,
275
+ ): asserts nodeName is string {
276
+ if (!isString(nodeName)) {
277
+ throw new TypeError(
278
+ `[router.shouldUpdateNode] nodeName must be a string, got ${typeof nodeName}`,
279
+ );
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Validates routes for addition to the router.
285
+ * Checks parent existence, duplicates, and forwardTo targets/cycles.
286
+ *
287
+ * @param routes - Routes to validate
288
+ * @param tree - Current route tree (optional for initial validation)
289
+ * @param forwardMap - Current forward map for cycle detection
290
+ * @param parentName - Optional parent route fullName for nesting via addRoute({ parent })
291
+ */
292
+ export function validateRoutes<Dependencies extends DefaultDependencies>(
293
+ routes: Route<Dependencies>[],
294
+ tree?: RouteTree,
295
+ forwardMap?: Record<string, string>,
296
+ parentName?: string,
297
+ ): void {
298
+ // Validate parent route exists in tree
299
+ if (parentName && tree) {
300
+ let node: RouteTree | undefined = tree;
301
+
302
+ for (const segment of parentName.split(".")) {
303
+ node = node.children.get(segment);
304
+
305
+ if (!node) {
306
+ throw new Error(
307
+ `[router.addRoute] Parent route "${parentName}" does not exist`,
308
+ );
309
+ }
310
+ }
311
+ }
312
+
313
+ // Tracking sets for duplicate detection
314
+ const seenNames = new Set<string>();
315
+ const seenPathsByParent = new Map<string, Set<string>>();
316
+
317
+ for (const route of routes) {
318
+ validateRoute(
319
+ route,
320
+ "addRoute",
321
+ tree,
322
+ parentName ?? "",
323
+ seenNames,
324
+ seenPathsByParent,
325
+ );
326
+ }
327
+
328
+ if (tree && forwardMap) {
329
+ validateForwardToTargets(routes, forwardMap, tree);
330
+ }
331
+ }
@@ -0,0 +1,317 @@
1
+ // packages/core/src/namespaces/StateNamespace/StateNamespace.ts
2
+
3
+ import {
4
+ getTypeDescription,
5
+ isParams,
6
+ isString,
7
+ validateState,
8
+ } from "type-guards";
9
+
10
+ import { areParamValuesEqual, getUrlParamsFromMeta } from "./helpers";
11
+ import { constants } from "../../constants";
12
+ import { freezeStateInPlace } from "../../helpers";
13
+
14
+ import type { StateNamespaceDependencies } from "./types";
15
+ import type {
16
+ NavigationOptions,
17
+ Params,
18
+ State,
19
+ StateMetaInput,
20
+ } from "@real-router/types";
21
+ import type { RouteTreeStateMeta } from "route-tree";
22
+
23
+ /**
24
+ * Independent namespace for managing router state storage and creation.
25
+ *
26
+ * Static methods handle validation (called by facade).
27
+ * Instance methods handle state storage, freezing, and creation.
28
+ */
29
+ export class StateNamespace {
30
+ /**
31
+ * Auto-incrementing state ID for tracking navigation history.
32
+ */
33
+ #stateId = 0;
34
+
35
+ /**
36
+ * Cached frozen state - avoids structuredClone on every getState() call.
37
+ */
38
+ #frozenState: State | undefined = undefined;
39
+
40
+ /**
41
+ * Previous state before the last setState call.
42
+ */
43
+ #previousState: State | undefined = undefined;
44
+
45
+ /**
46
+ * Dependencies injected from Router.
47
+ */
48
+ #deps!: StateNamespaceDependencies;
49
+
50
+ /**
51
+ * Cache for URL params by route name.
52
+ */
53
+ readonly #urlParamsCache = new Map<string, string[]>();
54
+
55
+ // =========================================================================
56
+ // Static validation methods (called by facade before instance methods)
57
+ // =========================================================================
58
+
59
+ /**
60
+ * Validates makeState arguments.
61
+ */
62
+ static validateMakeStateArgs(
63
+ name: unknown,
64
+ params: unknown,
65
+ path: unknown,
66
+ forceId: unknown,
67
+ ): void {
68
+ // Validate name is a string
69
+ if (!isString(name)) {
70
+ throw new TypeError(
71
+ `[router.makeState] Invalid name: ${getTypeDescription(name)}. Expected string.`,
72
+ );
73
+ }
74
+
75
+ // Validate params if provided
76
+ if (params !== undefined && !isParams(params)) {
77
+ throw new TypeError(
78
+ `[router.makeState] Invalid params: ${getTypeDescription(params)}. Expected plain object.`,
79
+ );
80
+ }
81
+
82
+ // Validate path if provided
83
+ if (path !== undefined && !isString(path)) {
84
+ throw new TypeError(
85
+ `[router.makeState] Invalid path: ${getTypeDescription(path)}. Expected string.`,
86
+ );
87
+ }
88
+
89
+ // Validate forceId if provided
90
+ if (forceId !== undefined && typeof forceId !== "number") {
91
+ throw new TypeError(
92
+ `[router.makeState] Invalid forceId: ${getTypeDescription(forceId)}. Expected number.`,
93
+ );
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Validates areStatesEqual arguments.
99
+ */
100
+ static validateAreStatesEqualArgs(
101
+ state1: unknown,
102
+ state2: unknown,
103
+ ignoreQueryParams: unknown,
104
+ ): void {
105
+ // null/undefined are valid (represent "no state")
106
+ if (state1 != null) {
107
+ validateState(state1, "areStatesEqual");
108
+ }
109
+
110
+ if (state2 != null) {
111
+ validateState(state2, "areStatesEqual");
112
+ }
113
+
114
+ if (
115
+ ignoreQueryParams !== undefined &&
116
+ typeof ignoreQueryParams !== "boolean"
117
+ ) {
118
+ throw new TypeError(
119
+ `[router.areStatesEqual] Invalid ignoreQueryParams: ${getTypeDescription(ignoreQueryParams)}. Expected boolean.`,
120
+ );
121
+ }
122
+ }
123
+
124
+ // =========================================================================
125
+ // Instance methods (trust input - already validated by facade)
126
+ // =========================================================================
127
+
128
+ /**
129
+ * Returns the current router state.
130
+ *
131
+ * The returned state is deeply frozen (immutable) for safety.
132
+ * Returns `undefined` if the router has not been started or has been stopped.
133
+ */
134
+ get<P extends Params = Params, MP extends Params = Params>():
135
+ | State<P, MP>
136
+ | undefined {
137
+ return this.#frozenState as State<P, MP> | undefined;
138
+ }
139
+
140
+ /**
141
+ * Sets the current router state.
142
+ *
143
+ * The state is deeply frozen before storage to ensure immutability.
144
+ * The previous state is preserved and accessible via `getPrevious()`.
145
+ *
146
+ * @param state - Already validated by facade, or undefined to clear
147
+ */
148
+ set(state: State | undefined): void {
149
+ // Preserve current state as previous before updating
150
+ this.#previousState = this.#frozenState;
151
+
152
+ // If state is already frozen (from makeState()), use it directly.
153
+ // For external states, freeze in place without cloning.
154
+ this.#frozenState = state ? freezeStateInPlace(state) : undefined;
155
+ }
156
+
157
+ /**
158
+ * Returns the previous router state (before the last navigation).
159
+ */
160
+ getPrevious<P extends Params = Params, MP extends Params = Params>():
161
+ | State<P, MP>
162
+ | undefined {
163
+ return this.#previousState as State<P, MP> | undefined;
164
+ }
165
+
166
+ reset(): void {
167
+ this.#frozenState = undefined;
168
+ this.#previousState = undefined;
169
+ this.#urlParamsCache.clear();
170
+ this.#stateId = 0;
171
+ }
172
+
173
+ // =========================================================================
174
+ // Dependency Injection
175
+ // =========================================================================
176
+
177
+ /**
178
+ * Sets dependencies for state creation methods.
179
+ * Must be called before using makeState, areStatesEqual, etc.
180
+ */
181
+ setDependencies(deps: StateNamespaceDependencies): void {
182
+ this.#deps = deps;
183
+ }
184
+
185
+ // =========================================================================
186
+ // State Creation Methods
187
+ // =========================================================================
188
+
189
+ /**
190
+ * Creates a frozen state object for a route.
191
+ */
192
+ makeState<P extends Params = Params, MP extends Params = Params>(
193
+ name: string,
194
+ params?: P,
195
+ path?: string,
196
+ meta?: StateMetaInput<MP>,
197
+ forceId?: number,
198
+ ): State<P, MP> {
199
+ const madeMeta = meta
200
+ ? {
201
+ ...meta,
202
+ id: forceId ?? ++this.#stateId,
203
+ params: meta.params,
204
+ options: meta.options,
205
+ }
206
+ : undefined;
207
+
208
+ // Optimization: O(1) lookup instead of O(depth) ancestor iteration
209
+ const defaultParamsConfig = this.#deps.getDefaultParams();
210
+ const hasDefaultParams = Object.hasOwn(defaultParamsConfig, name);
211
+
212
+ // Conditional allocation: avoid spreading when no defaultParams exist
213
+ let mergedParams: P;
214
+
215
+ if (hasDefaultParams) {
216
+ mergedParams = { ...defaultParamsConfig[name], ...params } as P;
217
+ } else if (params) {
218
+ mergedParams = { ...params };
219
+ } else {
220
+ mergedParams = {} as P;
221
+ }
222
+
223
+ const state: State<P, MP> = {
224
+ name,
225
+ params: mergedParams,
226
+ path: path ?? this.#deps.buildPath(name, params),
227
+ meta: madeMeta,
228
+ };
229
+
230
+ return freezeStateInPlace(state);
231
+ }
232
+
233
+ /**
234
+ * Creates a frozen state object for the "not found" route.
235
+ */
236
+ makeNotFoundState(path: string, options: NavigationOptions): State {
237
+ return this.makeState<{ path: string }>(
238
+ constants.UNKNOWN_ROUTE,
239
+ { path },
240
+ path,
241
+ {
242
+ options,
243
+ params: {},
244
+ },
245
+ );
246
+ }
247
+
248
+ // =========================================================================
249
+ // State Comparison Methods
250
+ // =========================================================================
251
+
252
+ /**
253
+ * Compares two states for equality.
254
+ * By default, ignores query params (only compares URL params).
255
+ */
256
+ areStatesEqual(
257
+ state1: State | undefined,
258
+ state2: State | undefined,
259
+ ignoreQueryParams = true,
260
+ ): boolean {
261
+ if (!state1 || !state2) {
262
+ return !!state1 === !!state2;
263
+ }
264
+
265
+ if (state1.name !== state2.name) {
266
+ return false;
267
+ }
268
+
269
+ if (ignoreQueryParams) {
270
+ const stateMeta = (state1.meta?.params ?? state2.meta?.params) as
271
+ | RouteTreeStateMeta
272
+ | undefined;
273
+
274
+ const urlParams = stateMeta
275
+ ? getUrlParamsFromMeta(stateMeta)
276
+ : this.#getUrlParams(state1.name);
277
+
278
+ return urlParams.every((param) =>
279
+ areParamValuesEqual(state1.params[param], state2.params[param]),
280
+ );
281
+ }
282
+
283
+ const state1Keys = Object.keys(state1.params);
284
+ const state2Keys = Object.keys(state2.params);
285
+
286
+ if (state1Keys.length !== state2Keys.length) {
287
+ return false;
288
+ }
289
+
290
+ return state1Keys.every(
291
+ (param) =>
292
+ param in state2.params &&
293
+ areParamValuesEqual(state1.params[param], state2.params[param]),
294
+ );
295
+ }
296
+
297
+ // =========================================================================
298
+ // Private Helpers
299
+ // =========================================================================
300
+
301
+ /**
302
+ * Gets URL params for a route name, using cache for performance.
303
+ */
304
+ #getUrlParams(name: string): string[] {
305
+ const cached = this.#urlParamsCache.get(name);
306
+
307
+ if (cached !== undefined) {
308
+ return cached;
309
+ }
310
+
311
+ const result = this.#deps.getUrlParams(name);
312
+
313
+ this.#urlParamsCache.set(name, result);
314
+
315
+ return result;
316
+ }
317
+ }
@@ -0,0 +1,43 @@
1
+ // packages/core/src/namespaces/StateNamespace/helpers.ts
2
+
3
+ import type { RouteTreeStateMeta } from "route-tree";
4
+
5
+ /**
6
+ * Extracts URL param names from RouteTreeStateMeta.
7
+ * This is an O(segments × params) operation but avoids tree traversal.
8
+ */
9
+ export function getUrlParamsFromMeta(meta: RouteTreeStateMeta): string[] {
10
+ const urlParams: string[] = [];
11
+
12
+ for (const segmentName in meta) {
13
+ const paramMap = meta[segmentName];
14
+
15
+ for (const param in paramMap) {
16
+ if (paramMap[param] === "url") {
17
+ urlParams.push(param);
18
+ }
19
+ }
20
+ }
21
+
22
+ return urlParams;
23
+ }
24
+
25
+ /**
26
+ * Compares two parameter values for equality.
27
+ * Supports deep equality for arrays (common in route params like tags, ids).
28
+ */
29
+ export function areParamValuesEqual(val1: unknown, val2: unknown): boolean {
30
+ if (val1 === val2) {
31
+ return true;
32
+ }
33
+
34
+ if (Array.isArray(val1) && Array.isArray(val2)) {
35
+ if (val1.length !== val2.length) {
36
+ return false;
37
+ }
38
+
39
+ return val1.every((v, i) => areParamValuesEqual(v, val2[i]));
40
+ }
41
+
42
+ return false;
43
+ }
@@ -0,0 +1,5 @@
1
+ // packages/core/src/namespaces/StateNamespace/index.ts
2
+
3
+ export { StateNamespace } from "./StateNamespace";
4
+
5
+ export type { StateNamespaceDependencies } from "./types";
@@ -0,0 +1,15 @@
1
+ // packages/core/src/namespaces/StateNamespace/types.ts
2
+
3
+ import type { Params } from "@real-router/types";
4
+
5
+ /**
6
+ * Dependencies injected from Router for state creation.
7
+ */
8
+ export interface StateNamespaceDependencies {
9
+ /** Get defaultParams config for a route */
10
+ getDefaultParams: () => Record<string, Params>;
11
+ /** Build URL path for a route */
12
+ buildPath: (name: string, params?: Params) => string;
13
+ /** Get URL params for a route (for areStatesEqual) */
14
+ getUrlParams: (name: string) => string[];
15
+ }