@real-router/core 0.47.0 → 0.48.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 (52) hide show
  1. package/dist/cjs/{Router-BtWR1xO-.js → Router-But6ZKYG.js} +3 -3
  2. package/dist/cjs/Router-But6ZKYG.js.map +1 -0
  3. package/dist/cjs/Router-Dh1xgFLI.d.ts.map +1 -1
  4. package/dist/cjs/RouterError-CWIGEyPI.js +2 -0
  5. package/dist/cjs/RouterError-CWIGEyPI.js.map +1 -0
  6. package/dist/cjs/api.d.ts.map +1 -1
  7. package/dist/cjs/api.js +1 -1
  8. package/dist/cjs/api.js.map +1 -1
  9. package/dist/cjs/{getPluginApi-1VcDVGjf.js → getPluginApi-DMO1jdNK.js} +2 -2
  10. package/dist/cjs/getPluginApi-DMO1jdNK.js.map +1 -0
  11. package/dist/cjs/index.d.ts.map +1 -1
  12. package/dist/cjs/index.js +1 -1
  13. package/dist/cjs/internals-na15rxo_.js.map +1 -1
  14. package/dist/cjs/utils.js +1 -1
  15. package/dist/cjs/validation.d.ts +1 -0
  16. package/dist/cjs/validation.d.ts.map +1 -1
  17. package/dist/esm/Router-BPkXwb1J.d.mts.map +1 -1
  18. package/dist/esm/Router-BThyTcCs.mjs +6 -0
  19. package/dist/esm/Router-BThyTcCs.mjs.map +1 -0
  20. package/dist/esm/RouterError-2JY9OfZc.mjs +2 -0
  21. package/dist/esm/RouterError-2JY9OfZc.mjs.map +1 -0
  22. package/dist/esm/api.d.mts.map +1 -1
  23. package/dist/esm/api.mjs +1 -1
  24. package/dist/esm/api.mjs.map +1 -1
  25. package/dist/esm/{getPluginApi-BvOUPp3g.mjs → getPluginApi-Bwp0MNW9.mjs} +2 -2
  26. package/dist/esm/getPluginApi-Bwp0MNW9.mjs.map +1 -0
  27. package/dist/esm/index.d.mts.map +1 -1
  28. package/dist/esm/index.mjs +1 -1
  29. package/dist/esm/internals-CCymabFj.mjs.map +1 -1
  30. package/dist/esm/utils.mjs +1 -1
  31. package/dist/esm/validation.d.mts +1 -0
  32. package/dist/esm/validation.d.mts.map +1 -1
  33. package/package.json +2 -2
  34. package/src/Router.ts +4 -0
  35. package/src/api/getPluginApi.ts +27 -1
  36. package/src/api/getRoutesApi.ts +16 -4
  37. package/src/constants.ts +14 -0
  38. package/src/helpers.ts +14 -49
  39. package/src/internals.ts +1 -0
  40. package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +3 -2
  41. package/src/namespaces/NavigationNamespace/transition/completeTransition.ts +12 -8
  42. package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +4 -2
  43. package/src/namespaces/StateNamespace/StateNamespace.ts +19 -6
  44. package/dist/cjs/Router-BtWR1xO-.js.map +0 -1
  45. package/dist/cjs/RouterError-AQUx-VLW.js +0 -2
  46. package/dist/cjs/RouterError-AQUx-VLW.js.map +0 -1
  47. package/dist/cjs/getPluginApi-1VcDVGjf.js.map +0 -1
  48. package/dist/esm/Router-DmPynezS.mjs +0 -6
  49. package/dist/esm/Router-DmPynezS.mjs.map +0 -1
  50. package/dist/esm/RouterError-BOUkCIgf.mjs +0 -2
  51. package/dist/esm/RouterError-BOUkCIgf.mjs.map +0 -1
  52. package/dist/esm/getPluginApi-BvOUPp3g.mjs.map +0 -1
package/src/helpers.ts CHANGED
@@ -109,65 +109,30 @@ export function deepFreezeState<T extends State>(state: T): T {
109
109
  return clonedState;
110
110
  }
111
111
 
112
- // WeakSet to track already frozen root objects for O(1) re-freeze check
113
- const frozenRoots = new WeakSet<object>();
114
-
115
- // Module-scope recursive freeze function - better JIT optimization, no allocation per call
116
- function freezeRecursive(obj: unknown): void {
117
- // Skip primitives, null
118
- if (obj === null || typeof obj !== "object") {
119
- return;
120
- }
121
-
122
- // Skip already frozen objects (handles potential shared refs)
123
- if (Object.isFrozen(obj)) {
124
- return;
125
- }
126
-
127
- // Freeze the object/array
128
- Object.freeze(obj);
129
-
130
- // Iterate without Object.values() allocation
131
- if (Array.isArray(obj)) {
132
- for (const item of obj) {
133
- freezeRecursive(item);
134
- }
135
- } else {
136
- for (const key in obj) {
137
- freezeRecursive((obj as Record<string, unknown>)[key]);
138
- }
139
- }
140
- }
141
-
142
112
  /**
143
- * Freezes State object in-place without cloning.
144
- * Optimized for hot paths where state is known to be a fresh object.
113
+ * Shallow-freezes a State object in place.
114
+ *
115
+ * Freezes only the top-level State object (blocks reassignment of `name`,
116
+ * `params`, `path`, `transition`, `context`). Nested objects (`params`,
117
+ * `transition`, `transition.segments`, `transition.segments.{deactivated,activated}`)
118
+ * are expected to be **already frozen at creation time** by their producers:
119
+ *
120
+ * - `params` frozen in `makeState()` / `navigateToNotFound()`
121
+ * - `transition`, `segments`, `deactivated`, `activated` frozen in
122
+ * `buildTransitionMeta()` (or inline in `navigateToNotFound()`)
145
123
  *
146
- * IMPORTANT: Only use this when you know the state is a fresh object
147
- * that hasn't been exposed to external code yet (e.g., from makeState()).
124
+ * `state.context` is **intentionally not frozen** plugins write to it via
125
+ * `claim.write(state, value)` after state creation.
148
126
  *
149
- * @param state - The State object to freeze (must be a fresh object)
150
- * @returns The same state object, now frozen
151
127
  * @internal
152
128
  */
153
129
  export function freezeStateInPlace<T extends State>(state: T): T {
154
- // Early return for null/undefined - state from makeState() is never null
155
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
130
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive guard against external misuse
156
131
  if (!state) {
157
132
  return state;
158
133
  }
159
134
 
160
- // Fast path: already processed root object - O(1) check
161
- if (frozenRoots.has(state)) {
162
- return state;
163
- }
164
-
165
- freezeRecursive(state);
166
-
167
- // Mark root as processed for future calls
168
- frozenRoots.add(state);
169
-
170
- return state;
135
+ return Object.freeze(state);
171
136
  }
172
137
 
173
138
  /**
package/src/internals.ts CHANGED
@@ -90,6 +90,7 @@ export interface RouterInternals<
90
90
  readonly clearState: () => void;
91
91
  readonly setState: (state: State) => void;
92
92
  readonly routerExtensions: { keys: string[] }[];
93
+ readonly contextClaimRecords: Set<string>;
93
94
  }
94
95
 
95
96
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- existential type: stores RouterInternals for all Dependencies types
@@ -10,7 +10,7 @@ import {
10
10
  import { completeTransition } from "./transition/completeTransition";
11
11
  import { routeTransitionError } from "./transition/errorHandling";
12
12
  import { executeGuardPipeline } from "./transition/guardPhase";
13
- import { errorCodes, constants } from "../../constants";
13
+ import { EMPTY_PARAMS, errorCodes, constants } from "../../constants";
14
14
  import { RouterError } from "../../RouterError";
15
15
  import { getTransitionPath, nameToIDs } from "../../transitionPath";
16
16
 
@@ -310,9 +310,10 @@ export class NavigationNamespace {
310
310
 
311
311
  const state: State = {
312
312
  name: constants.UNKNOWN_ROUTE,
313
- params: {} as Params,
313
+ params: EMPTY_PARAMS as Params,
314
314
  path,
315
315
  transition: transitionMeta,
316
+ context: {},
316
317
  };
317
318
 
318
319
  Object.freeze(state);
@@ -1,5 +1,4 @@
1
1
  import { errorCodes, constants } from "../../../constants";
2
- import { freezeStateInPlace } from "../../../helpers";
3
2
  import { RouterError } from "../../../RouterError";
4
3
 
5
4
  import type { NavigationDependencies, NavigationContext } from "../types";
@@ -20,14 +19,19 @@ function buildTransitionMeta(
20
19
  toActivate: string[],
21
20
  intersection: string,
22
21
  ): TransitionMeta {
22
+ Object.freeze(toDeactivate);
23
+ Object.freeze(toActivate);
24
+
25
+ const segments = Object.freeze({
26
+ deactivated: toDeactivate,
27
+ activated: toActivate,
28
+ intersection,
29
+ });
30
+
23
31
  const meta: MutableTransitionMeta = {
24
32
  phase: "activating",
25
33
  reason: "success",
26
- segments: {
27
- deactivated: toDeactivate,
28
- activated: toActivate,
29
- intersection,
30
- },
34
+ segments,
31
35
  };
32
36
 
33
37
  if (fromState?.name !== undefined) {
@@ -42,7 +46,7 @@ function buildTransitionMeta(
42
46
  meta.redirected = opts.redirected;
43
47
  }
44
48
 
45
- return meta;
49
+ return Object.freeze(meta);
46
50
  }
47
51
 
48
52
  function stripSignal({
@@ -88,7 +92,7 @@ export function completeTransition(
88
92
  intersection,
89
93
  );
90
94
 
91
- const finalState = freezeStateInPlace(toState);
95
+ const finalState = Object.freeze(toState);
92
96
 
93
97
  deps.setState(finalState);
94
98
 
@@ -7,7 +7,7 @@ import {
7
7
  rebuildTreeInPlace,
8
8
  resetStore,
9
9
  } from "./routesStore";
10
- import { constants } from "../../constants";
10
+ import { constants, DEFAULT_TRANSITION } from "../../constants";
11
11
  import { getTransitionPath } from "../../transitionPath";
12
12
 
13
13
  import type { RoutesStore } from "./routesStore";
@@ -107,7 +107,7 @@ export class RoutesNamespace<
107
107
  );
108
108
  }
109
109
 
110
- if (toState.transition?.reload) {
110
+ if (toState.transition.reload) {
111
111
  return true;
112
112
  }
113
113
 
@@ -403,6 +403,8 @@ export class RoutesNamespace<
403
403
  name,
404
404
  params: effectiveParams,
405
405
  path: "",
406
+ transition: DEFAULT_TRANSITION,
407
+ context: {},
406
408
  };
407
409
 
408
410
  return this.#deps.areStatesEqual(
@@ -1,7 +1,7 @@
1
1
  // packages/core/src/namespaces/StateNamespace/StateNamespace.ts
2
2
 
3
3
  import { areParamValuesEqual } from "./helpers";
4
- import { EMPTY_PARAMS } from "../../constants";
4
+ import { DEFAULT_TRANSITION, EMPTY_PARAMS } from "../../constants";
5
5
  import { freezeStateInPlace } from "../../helpers";
6
6
  import { setStateMetaParams } from "../../stateMetaStore";
7
7
 
@@ -97,7 +97,15 @@ export class StateNamespace {
97
97
  // =========================================================================
98
98
 
99
99
  /**
100
- * Creates a frozen state object for a route.
100
+ * Creates a state object for a route.
101
+ *
102
+ * `params` is frozen at creation so it is always immutable, even when
103
+ * `skipFreeze=true` is passed to defer the outer `Object.freeze(state)` call.
104
+ * This keeps params-freezing invariants independent of transition-pipeline
105
+ * mutation (e.g. `completeTransition` attaching `state.transition`).
106
+ *
107
+ * `context` is initialized as a fresh empty object — intentionally NOT frozen
108
+ * so plugins can publish data via `claim.write(state, value)` after creation.
101
109
  */
102
110
  makeState<P extends Params = Params>(
103
111
  name: string,
@@ -114,18 +122,23 @@ export class StateNamespace {
114
122
  let mergedParams: P;
115
123
 
116
124
  if (hasDefaultParams) {
117
- mergedParams = { ...defaultParamsConfig[name], ...params } as P;
125
+ mergedParams = Object.freeze({
126
+ ...defaultParamsConfig[name],
127
+ ...params,
128
+ }) as P;
118
129
  } else if (!params || params === EMPTY_PARAMS) {
119
130
  mergedParams = EMPTY_PARAMS as P;
120
131
  } else {
121
- mergedParams = { ...params };
132
+ mergedParams = Object.freeze({ ...params }) as P;
122
133
  }
123
134
 
124
- const state: State<P> = {
135
+ const state = {
125
136
  name,
126
137
  params: mergedParams,
127
138
  path: path ?? this.#deps.buildPath(name, params),
128
- };
139
+ context: {},
140
+ ...(!skipFreeze && { transition: DEFAULT_TRANSITION }),
141
+ } as State<P>;
129
142
 
130
143
  if (meta) {
131
144
  setStateMetaParams(state, meta as unknown as Params);