@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.
- package/README.md +1 -3
- 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/metafile-cjs.json +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/metafile-esm.json +1 -1
- package/package.json +7 -5
- package/src/Router.ts +1174 -0
- package/src/RouterError.ts +324 -0
- package/src/constants.ts +112 -0
- package/src/createRouter.ts +32 -0
- package/src/fsm/index.ts +5 -0
- package/src/fsm/routerFSM.ts +129 -0
- package/src/getNavigator.ts +15 -0
- package/src/helpers.ts +194 -0
- package/src/index.ts +46 -0
- package/src/namespaces/CloneNamespace/CloneNamespace.ts +120 -0
- package/src/namespaces/CloneNamespace/index.ts +3 -0
- package/src/namespaces/CloneNamespace/types.ts +46 -0
- package/src/namespaces/DependenciesNamespace/DependenciesNamespace.ts +250 -0
- package/src/namespaces/DependenciesNamespace/index.ts +3 -0
- package/src/namespaces/DependenciesNamespace/validators.ts +105 -0
- package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +272 -0
- package/src/namespaces/EventBusNamespace/index.ts +5 -0
- package/src/namespaces/EventBusNamespace/types.ts +11 -0
- package/src/namespaces/MiddlewareNamespace/MiddlewareNamespace.ts +206 -0
- package/src/namespaces/MiddlewareNamespace/index.ts +5 -0
- package/src/namespaces/MiddlewareNamespace/types.ts +28 -0
- package/src/namespaces/MiddlewareNamespace/validators.ts +96 -0
- package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +308 -0
- package/src/namespaces/NavigationNamespace/index.ts +5 -0
- package/src/namespaces/NavigationNamespace/transition/executeLifecycleHooks.ts +84 -0
- package/src/namespaces/NavigationNamespace/transition/executeMiddleware.ts +56 -0
- package/src/namespaces/NavigationNamespace/transition/index.ts +107 -0
- package/src/namespaces/NavigationNamespace/transition/makeError.ts +37 -0
- package/src/namespaces/NavigationNamespace/transition/mergeStates.ts +54 -0
- package/src/namespaces/NavigationNamespace/transition/processLifecycleResult.ts +81 -0
- package/src/namespaces/NavigationNamespace/transition/wrapSyncError.ts +82 -0
- package/src/namespaces/NavigationNamespace/types.ts +129 -0
- package/src/namespaces/NavigationNamespace/validators.ts +87 -0
- package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +50 -0
- package/src/namespaces/OptionsNamespace/constants.ts +41 -0
- package/src/namespaces/OptionsNamespace/helpers.ts +51 -0
- package/src/namespaces/OptionsNamespace/index.ts +11 -0
- package/src/namespaces/OptionsNamespace/validators.ts +252 -0
- package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +325 -0
- package/src/namespaces/PluginsNamespace/constants.ts +35 -0
- package/src/namespaces/PluginsNamespace/index.ts +7 -0
- package/src/namespaces/PluginsNamespace/types.ts +32 -0
- package/src/namespaces/PluginsNamespace/validators.ts +79 -0
- package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +389 -0
- package/src/namespaces/RouteLifecycleNamespace/index.ts +5 -0
- package/src/namespaces/RouteLifecycleNamespace/types.ts +17 -0
- package/src/namespaces/RouteLifecycleNamespace/validators.ts +65 -0
- package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +140 -0
- package/src/namespaces/RouterLifecycleNamespace/constants.ts +25 -0
- package/src/namespaces/RouterLifecycleNamespace/index.ts +5 -0
- package/src/namespaces/RouterLifecycleNamespace/types.ts +23 -0
- package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +1482 -0
- package/src/namespaces/RoutesNamespace/constants.ts +14 -0
- package/src/namespaces/RoutesNamespace/helpers.ts +532 -0
- package/src/namespaces/RoutesNamespace/index.ts +9 -0
- package/src/namespaces/RoutesNamespace/stateBuilder.ts +70 -0
- package/src/namespaces/RoutesNamespace/types.ts +82 -0
- package/src/namespaces/RoutesNamespace/validators.ts +331 -0
- package/src/namespaces/StateNamespace/StateNamespace.ts +317 -0
- package/src/namespaces/StateNamespace/helpers.ts +43 -0
- package/src/namespaces/StateNamespace/index.ts +5 -0
- package/src/namespaces/StateNamespace/types.ts +15 -0
- package/src/namespaces/index.ts +42 -0
- package/src/transitionPath.ts +441 -0
- package/src/typeGuards.ts +74 -0
- package/src/types.ts +194 -0
- package/src/wiring/RouterWiringBuilder.ts +235 -0
- package/src/wiring/index.ts +7 -0
- package/src/wiring/types.ts +53 -0
- package/src/wiring/wireRouter.ts +29 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// packages/real-router/modules/RouterError.ts
|
|
2
|
+
|
|
3
|
+
import { errorCodes } from "./constants";
|
|
4
|
+
import { deepFreezeState } from "./helpers";
|
|
5
|
+
|
|
6
|
+
import type { State } from "@real-router/types";
|
|
7
|
+
|
|
8
|
+
// Pre-compute Set of error code values for O(1) lookup in setCode()
|
|
9
|
+
// This avoids creating array and doing linear search on every setCode() call
|
|
10
|
+
const errorCodeValues = new Set(Object.values(errorCodes));
|
|
11
|
+
|
|
12
|
+
// Reserved built-in properties - throw error if user tries to set these
|
|
13
|
+
const reservedProperties = new Set(["code", "segment", "path", "redirect"]);
|
|
14
|
+
|
|
15
|
+
// Reserved method names - silently ignore attempts to overwrite these
|
|
16
|
+
const reservedMethods = new Set([
|
|
17
|
+
"setCode",
|
|
18
|
+
"setErrorInstance",
|
|
19
|
+
"setAdditionalFields",
|
|
20
|
+
"hasField",
|
|
21
|
+
"getField",
|
|
22
|
+
"toJSON",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export class RouterError extends Error {
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
|
|
28
|
+
// Using public properties to ensure structural compatibility
|
|
29
|
+
// with RouterError interface in core-types
|
|
30
|
+
readonly segment: string | undefined;
|
|
31
|
+
readonly path: string | undefined;
|
|
32
|
+
readonly redirect: State | undefined;
|
|
33
|
+
|
|
34
|
+
// Note: code appears to be writable but setCode() should be used
|
|
35
|
+
// to properly update both code and message together
|
|
36
|
+
code: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Creates a new RouterError instance.
|
|
40
|
+
*
|
|
41
|
+
* The options object accepts built-in fields (message, segment, path, redirect)
|
|
42
|
+
* and any additional custom fields, which will all be attached to the error instance.
|
|
43
|
+
*
|
|
44
|
+
* @param code - The error code (e.g., "ROUTE_NOT_FOUND", "CANNOT_ACTIVATE")
|
|
45
|
+
* @param options - Optional configuration object
|
|
46
|
+
* @param options.message - Custom error message (defaults to code if not provided)
|
|
47
|
+
* @param options.segment - The route segment where the error occurred
|
|
48
|
+
* @param options.path - The full path where the error occurred
|
|
49
|
+
* @param options.redirect - Optional redirect state for navigation errors
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* // Basic error
|
|
54
|
+
* const err1 = new RouterError("ROUTE_NOT_FOUND");
|
|
55
|
+
*
|
|
56
|
+
* // Error with custom message
|
|
57
|
+
* const err2 = new RouterError("ERR", { message: "Something went wrong" });
|
|
58
|
+
*
|
|
59
|
+
* // Error with context and custom fields
|
|
60
|
+
* const err3 = new RouterError("CANNOT_ACTIVATE", {
|
|
61
|
+
* message: "Insufficient permissions",
|
|
62
|
+
* segment: "admin",
|
|
63
|
+
* path: "/admin/users",
|
|
64
|
+
* userId: "123" // custom field
|
|
65
|
+
* });
|
|
66
|
+
*
|
|
67
|
+
* // Error with redirect
|
|
68
|
+
* const err4 = new RouterError("TRANSITION_ERR", {
|
|
69
|
+
* redirect: { name: "home", path: "/", params: {} }
|
|
70
|
+
* });
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
constructor(
|
|
74
|
+
code: string,
|
|
75
|
+
{
|
|
76
|
+
message,
|
|
77
|
+
segment,
|
|
78
|
+
path,
|
|
79
|
+
redirect,
|
|
80
|
+
...rest
|
|
81
|
+
}: {
|
|
82
|
+
[key: string]: unknown;
|
|
83
|
+
message?: string | undefined;
|
|
84
|
+
segment?: string | undefined;
|
|
85
|
+
path?: string | undefined;
|
|
86
|
+
redirect?: State | undefined;
|
|
87
|
+
} = {},
|
|
88
|
+
) {
|
|
89
|
+
super(message ?? code);
|
|
90
|
+
|
|
91
|
+
this.code = code;
|
|
92
|
+
this.segment = segment;
|
|
93
|
+
this.path = path;
|
|
94
|
+
// Deep freeze redirect to prevent mutations (creates a frozen clone)
|
|
95
|
+
this.redirect = redirect ? deepFreezeState(redirect) : undefined;
|
|
96
|
+
|
|
97
|
+
// Assign custom fields, checking reserved properties and filtering out reserved method names
|
|
98
|
+
// Issue #39: Throw for reserved properties to match setAdditionalFields behavior
|
|
99
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
100
|
+
if (reservedProperties.has(key)) {
|
|
101
|
+
throw new TypeError(
|
|
102
|
+
`[RouterError] Cannot set reserved property "${key}"`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!reservedMethods.has(key)) {
|
|
107
|
+
this[key] = value;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Updates the error code and conditionally updates the message.
|
|
114
|
+
*
|
|
115
|
+
* If the current message is one of the standard error code values
|
|
116
|
+
* (e.g., "ROUTE_NOT_FOUND", "SAME_STATES"), it will be replaced with the new code.
|
|
117
|
+
* This allows keeping error messages in sync with codes when using standard error codes.
|
|
118
|
+
*
|
|
119
|
+
* If the message is custom (not a standard error code), it will be preserved.
|
|
120
|
+
*
|
|
121
|
+
* @param newCode - The new error code to set
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* // Message follows code (standard error code as message)
|
|
125
|
+
* const err = new RouterError("ROUTE_NOT_FOUND", { message: "ROUTE_NOT_FOUND" });
|
|
126
|
+
* err.setCode("CUSTOM_ERROR"); // message becomes "CUSTOM_ERROR"
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* // Custom message is preserved
|
|
130
|
+
* const err = new RouterError("ERR", { message: "Custom error message" });
|
|
131
|
+
* err.setCode("NEW_CODE"); // message stays "Custom error message"
|
|
132
|
+
*/
|
|
133
|
+
setCode(newCode: string): void {
|
|
134
|
+
this.code = newCode;
|
|
135
|
+
|
|
136
|
+
// Only update message if it's a standard error code value (not a custom message)
|
|
137
|
+
if (errorCodeValues.has(this.message)) {
|
|
138
|
+
this.message = newCode;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Copies properties from another Error instance to this RouterError.
|
|
144
|
+
*
|
|
145
|
+
* This method updates the message, cause, and stack trace from the provided error.
|
|
146
|
+
* Useful for wrapping native errors while preserving error context.
|
|
147
|
+
*
|
|
148
|
+
* @param err - The Error instance to copy properties from
|
|
149
|
+
* @throws {TypeError} If err is null or undefined
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* const routerErr = new RouterError("TRANSITION_ERR");
|
|
154
|
+
* try {
|
|
155
|
+
* // some operation that might fail
|
|
156
|
+
* } catch (nativeErr) {
|
|
157
|
+
* routerErr.setErrorInstance(nativeErr);
|
|
158
|
+
* throw routerErr;
|
|
159
|
+
* }
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
setErrorInstance(err: Error): void {
|
|
163
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
164
|
+
if (!err) {
|
|
165
|
+
throw new TypeError(
|
|
166
|
+
"[RouterError.setErrorInstance] err parameter is required and must be an Error instance",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.message = err.message;
|
|
171
|
+
this.cause = err.cause;
|
|
172
|
+
this.stack = err.stack ?? "";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Adds custom fields to the error object.
|
|
177
|
+
*
|
|
178
|
+
* This method allows attaching arbitrary data to the error for debugging or logging purposes.
|
|
179
|
+
* All fields become accessible as properties on the error instance and are included in JSON serialization.
|
|
180
|
+
*
|
|
181
|
+
* Reserved method names (setCode, setErrorInstance, setAdditionalFields, hasField, getField, toJSON)
|
|
182
|
+
* are automatically filtered out to prevent accidental overwriting of class methods.
|
|
183
|
+
*
|
|
184
|
+
* @param fields - Object containing custom fields to add to the error
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```typescript
|
|
188
|
+
* const err = new RouterError("CANNOT_ACTIVATE");
|
|
189
|
+
* err.setAdditionalFields({
|
|
190
|
+
* userId: "123",
|
|
191
|
+
* attemptedRoute: "/admin",
|
|
192
|
+
* reason: "insufficient permissions"
|
|
193
|
+
* });
|
|
194
|
+
*
|
|
195
|
+
* console.log(err.userId); // "123"
|
|
196
|
+
* console.log(JSON.stringify(err)); // includes all custom fields
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
setAdditionalFields(fields: Record<string, unknown>): void {
|
|
200
|
+
// Assign fields, throwing for reserved properties, silently ignoring methods
|
|
201
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
202
|
+
if (reservedProperties.has(key)) {
|
|
203
|
+
throw new TypeError(
|
|
204
|
+
`[RouterError.setAdditionalFields] Cannot set reserved property "${key}"`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!reservedMethods.has(key)) {
|
|
209
|
+
this[key] = value;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Checks if a custom field exists on the error object.
|
|
216
|
+
*
|
|
217
|
+
* This method checks for both custom fields added via setAdditionalFields()
|
|
218
|
+
* and built-in fields (code, message, segment, etc.).
|
|
219
|
+
*
|
|
220
|
+
* @param key - The field name to check
|
|
221
|
+
* @returns `true` if the field exists, `false` otherwise
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```typescript
|
|
225
|
+
* const err = new RouterError("ERR", { segment: "users" });
|
|
226
|
+
* err.setAdditionalFields({ userId: "123" });
|
|
227
|
+
*
|
|
228
|
+
* err.hasField("userId"); // true
|
|
229
|
+
* err.hasField("segment"); // true
|
|
230
|
+
* err.hasField("unknown"); // false
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
hasField(key: string): boolean {
|
|
234
|
+
return key in this;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Retrieves a custom field value from the error object.
|
|
239
|
+
*
|
|
240
|
+
* This method can access both custom fields and built-in fields.
|
|
241
|
+
* Returns `undefined` if the field doesn't exist.
|
|
242
|
+
*
|
|
243
|
+
* @param key - The field name to retrieve
|
|
244
|
+
* @returns The field value, or `undefined` if it doesn't exist
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```typescript
|
|
248
|
+
* const err = new RouterError("ERR");
|
|
249
|
+
* err.setAdditionalFields({ userId: "123", role: "admin" });
|
|
250
|
+
*
|
|
251
|
+
* err.getField("userId"); // "123"
|
|
252
|
+
* err.getField("role"); // "admin"
|
|
253
|
+
* err.getField("code"); // "ERR" (built-in field)
|
|
254
|
+
* err.getField("unknown"); // undefined
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
getField(key: string): unknown {
|
|
258
|
+
return this[key];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Serializes the error to a JSON-compatible object.
|
|
263
|
+
*
|
|
264
|
+
* This method is automatically called by JSON.stringify() and includes:
|
|
265
|
+
* - Built-in fields: code, message, segment (if set), path (if set), redirect (if set)
|
|
266
|
+
* - All custom fields added via setAdditionalFields() or constructor
|
|
267
|
+
* - Excludes: stack trace (for security/cleanliness)
|
|
268
|
+
*
|
|
269
|
+
* @returns A plain object representation of the error, suitable for JSON serialization
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```typescript
|
|
273
|
+
* const err = new RouterError("ROUTE_NOT_FOUND", {
|
|
274
|
+
* message: "Route not found",
|
|
275
|
+
* path: "/admin/users/123"
|
|
276
|
+
* });
|
|
277
|
+
* err.setAdditionalFields({ userId: "123" });
|
|
278
|
+
*
|
|
279
|
+
* JSON.stringify(err);
|
|
280
|
+
* // {
|
|
281
|
+
* // "code": "ROUTE_NOT_FOUND",
|
|
282
|
+
* // "message": "Route not found",
|
|
283
|
+
* // "path": "/admin/users/123",
|
|
284
|
+
* // "userId": "123"
|
|
285
|
+
* // }
|
|
286
|
+
* ```
|
|
287
|
+
*/
|
|
288
|
+
toJSON(): Record<string, unknown> {
|
|
289
|
+
const result: Record<string, unknown> = {
|
|
290
|
+
code: this.code,
|
|
291
|
+
message: this.message,
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
if (this.segment !== undefined) {
|
|
295
|
+
result.segment = this.segment;
|
|
296
|
+
}
|
|
297
|
+
if (this.path !== undefined) {
|
|
298
|
+
result.path = this.path;
|
|
299
|
+
}
|
|
300
|
+
if (this.redirect !== undefined) {
|
|
301
|
+
result.redirect = this.redirect;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// add all public fields
|
|
305
|
+
// Using Set.has() for O(1) lookup instead of Array.includes() O(n)
|
|
306
|
+
// Overall complexity: O(n) instead of O(n*m)
|
|
307
|
+
const excludeKeys = new Set([
|
|
308
|
+
"code",
|
|
309
|
+
"message",
|
|
310
|
+
"segment",
|
|
311
|
+
"path",
|
|
312
|
+
"redirect",
|
|
313
|
+
"stack",
|
|
314
|
+
]);
|
|
315
|
+
|
|
316
|
+
for (const key in this) {
|
|
317
|
+
if (Object.hasOwn(this, key) && !excludeKeys.has(key)) {
|
|
318
|
+
result[key] = this[key];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// packages/real-router/modules/constants.ts
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
EventName,
|
|
5
|
+
EventToNameMap,
|
|
6
|
+
EventToPluginMap,
|
|
7
|
+
ErrorCodeToValueMap,
|
|
8
|
+
ErrorCodeKeys,
|
|
9
|
+
ErrorCodeValues,
|
|
10
|
+
} from "@real-router/types";
|
|
11
|
+
|
|
12
|
+
export type ConstantsKeys = "UNKNOWN_ROUTE";
|
|
13
|
+
|
|
14
|
+
export type Constants = Record<ConstantsKeys, string>;
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Error Codes (migrated from router-error)
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
export type ErrorCodes = Record<ErrorCodeKeys, ErrorCodeValues>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Error codes for router operations.
|
|
24
|
+
* Used to identify specific failure scenarios in navigation and lifecycle.
|
|
25
|
+
* Frozen to prevent accidental modifications.
|
|
26
|
+
*/
|
|
27
|
+
export const errorCodes: ErrorCodeToValueMap = Object.freeze({
|
|
28
|
+
ROUTER_NOT_STARTED: "NOT_STARTED", // navigate() called before start()
|
|
29
|
+
NO_START_PATH_OR_STATE: "NO_START_PATH_OR_STATE", // start() without initial route
|
|
30
|
+
ROUTER_ALREADY_STARTED: "ALREADY_STARTED", // start() called twice
|
|
31
|
+
ROUTE_NOT_FOUND: "ROUTE_NOT_FOUND", // Navigation to non-existent route
|
|
32
|
+
SAME_STATES: "SAME_STATES", // Navigate to current route without reload
|
|
33
|
+
CANNOT_DEACTIVATE: "CANNOT_DEACTIVATE", // canDeactivate guard blocked navigation
|
|
34
|
+
CANNOT_ACTIVATE: "CANNOT_ACTIVATE", // canActivate guard blocked navigation
|
|
35
|
+
TRANSITION_ERR: "TRANSITION_ERR", // Generic transition failure
|
|
36
|
+
TRANSITION_CANCELLED: "CANCELLED", // Navigation cancelled by user or new navigation
|
|
37
|
+
ROUTER_DISPOSED: "DISPOSED", // Router has been disposed
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* General router constants.
|
|
42
|
+
* Special route names and identifiers.
|
|
43
|
+
*/
|
|
44
|
+
export const constants: Constants = {
|
|
45
|
+
UNKNOWN_ROUTE: "@@router/UNKNOWN_ROUTE", // Special route for 404/not found states
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Plugin method names.
|
|
50
|
+
* Maps to methods that plugins can implement to hook into router lifecycle.
|
|
51
|
+
*/
|
|
52
|
+
export const plugins: EventToPluginMap = {
|
|
53
|
+
ROUTER_START: "onStart", // Plugin method called when router starts
|
|
54
|
+
ROUTER_STOP: "onStop", // Plugin method called when router stops
|
|
55
|
+
TRANSITION_START: "onTransitionStart", // Plugin method called when navigation begins
|
|
56
|
+
TRANSITION_CANCEL: "onTransitionCancel", // Plugin method called when navigation cancelled
|
|
57
|
+
TRANSITION_SUCCESS: "onTransitionSuccess", // Plugin method called when navigation succeeds
|
|
58
|
+
TRANSITION_ERROR: "onTransitionError", // Plugin method called when navigation fails
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Event names for router event system.
|
|
63
|
+
* Used with addEventListener/removeEventListener for reactive subscriptions.
|
|
64
|
+
*/
|
|
65
|
+
export const events: EventToNameMap = {
|
|
66
|
+
ROUTER_START: "$start", // Emitted when router.start() succeeds
|
|
67
|
+
ROUTER_STOP: "$stop", // Emitted when router.stop() is called
|
|
68
|
+
TRANSITION_START: "$$start", // Emitted when navigation begins
|
|
69
|
+
TRANSITION_CANCEL: "$$cancel", // Emitted when navigation is cancelled
|
|
70
|
+
TRANSITION_SUCCESS: "$$success", // Emitted when navigation completes successfully
|
|
71
|
+
TRANSITION_ERROR: "$$error", // Emitted when navigation fails
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Valid event names for validation.
|
|
76
|
+
*/
|
|
77
|
+
export const validEventNames = new Set<EventName>([
|
|
78
|
+
events.ROUTER_START,
|
|
79
|
+
events.TRANSITION_START,
|
|
80
|
+
events.TRANSITION_SUCCESS,
|
|
81
|
+
events.TRANSITION_ERROR,
|
|
82
|
+
events.TRANSITION_CANCEL,
|
|
83
|
+
events.ROUTER_STOP,
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Default limits configuration for the router.
|
|
88
|
+
* These values match the hardcoded constants from the current codebase.
|
|
89
|
+
*/
|
|
90
|
+
export const DEFAULT_LIMITS = {
|
|
91
|
+
maxDependencies: 100,
|
|
92
|
+
maxPlugins: 50,
|
|
93
|
+
maxMiddleware: 50,
|
|
94
|
+
maxListeners: 10_000,
|
|
95
|
+
warnListeners: 1000,
|
|
96
|
+
maxEventDepth: 5,
|
|
97
|
+
maxLifecycleHandlers: 200,
|
|
98
|
+
} as const;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Bounds for each limit - defines min and max allowed values.
|
|
102
|
+
* Used for runtime validation in setLimit/withLimits.
|
|
103
|
+
*/
|
|
104
|
+
export const LIMIT_BOUNDS = {
|
|
105
|
+
maxDependencies: { min: 0, max: 10_000 },
|
|
106
|
+
maxPlugins: { min: 0, max: 1000 },
|
|
107
|
+
maxMiddleware: { min: 0, max: 1000 },
|
|
108
|
+
maxListeners: { min: 0, max: 100_000 },
|
|
109
|
+
warnListeners: { min: 0, max: 100_000 },
|
|
110
|
+
maxEventDepth: { min: 0, max: 100 },
|
|
111
|
+
maxLifecycleHandlers: { min: 0, max: 10_000 },
|
|
112
|
+
} as const;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// packages/core/src/createRouter.ts
|
|
2
|
+
|
|
3
|
+
import { Router } from "./Router";
|
|
4
|
+
|
|
5
|
+
import type { Route } from "./types";
|
|
6
|
+
import type { DefaultDependencies, Options } from "@real-router/types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a new router instance.
|
|
10
|
+
*
|
|
11
|
+
* @param routes - Array of route definitions
|
|
12
|
+
* @param options - Router configuration options
|
|
13
|
+
* @param dependencies - Dependencies to inject into the router
|
|
14
|
+
* @returns A new Router instance
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const router = createRouter([
|
|
18
|
+
* { name: 'home', path: '/' },
|
|
19
|
+
* { name: 'users', path: '/users' },
|
|
20
|
+
* ]);
|
|
21
|
+
*
|
|
22
|
+
* router.start('/');
|
|
23
|
+
*/
|
|
24
|
+
export const createRouter = <
|
|
25
|
+
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
26
|
+
>(
|
|
27
|
+
routes: Route<Dependencies>[] = [],
|
|
28
|
+
options: Partial<Options> = {},
|
|
29
|
+
dependencies: Dependencies = {} as Dependencies,
|
|
30
|
+
): Router<Dependencies> => {
|
|
31
|
+
return new Router<Dependencies>(routes, options, dependencies);
|
|
32
|
+
};
|
package/src/fsm/index.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// packages/core/src/fsm/routerFSM.ts
|
|
2
|
+
|
|
3
|
+
import { FSM } from "@real-router/fsm";
|
|
4
|
+
|
|
5
|
+
import type { FSMConfig } from "@real-router/fsm";
|
|
6
|
+
import type { NavigationOptions, State } from "@real-router/types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Router FSM states.
|
|
10
|
+
*
|
|
11
|
+
* - IDLE: Router not started or stopped
|
|
12
|
+
* - STARTING: Router is initializing
|
|
13
|
+
* - READY: Router is ready for navigation
|
|
14
|
+
* - TRANSITIONING: Navigation in progress
|
|
15
|
+
* - DISPOSED: Router has been disposed (R2+)
|
|
16
|
+
*/
|
|
17
|
+
export const routerStates = {
|
|
18
|
+
IDLE: "IDLE",
|
|
19
|
+
STARTING: "STARTING",
|
|
20
|
+
READY: "READY",
|
|
21
|
+
TRANSITIONING: "TRANSITIONING",
|
|
22
|
+
DISPOSED: "DISPOSED",
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
export type RouterState = (typeof routerStates)[keyof typeof routerStates];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Router FSM events.
|
|
29
|
+
*
|
|
30
|
+
* - START: Begin router initialization
|
|
31
|
+
* - STARTED: Router initialization complete
|
|
32
|
+
* - NAVIGATE: Begin navigation
|
|
33
|
+
* - COMPLETE: Navigation completed successfully
|
|
34
|
+
* - FAIL: Navigation or initialization failed
|
|
35
|
+
* - CANCEL: Navigation cancelled
|
|
36
|
+
* - STOP: Stop router
|
|
37
|
+
* - DISPOSE: Dispose router (R2+)
|
|
38
|
+
*/
|
|
39
|
+
export const routerEvents = {
|
|
40
|
+
START: "START",
|
|
41
|
+
STARTED: "STARTED",
|
|
42
|
+
NAVIGATE: "NAVIGATE",
|
|
43
|
+
COMPLETE: "COMPLETE",
|
|
44
|
+
FAIL: "FAIL",
|
|
45
|
+
CANCEL: "CANCEL",
|
|
46
|
+
STOP: "STOP",
|
|
47
|
+
DISPOSE: "DISPOSE",
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
export type RouterEvent = (typeof routerEvents)[keyof typeof routerEvents];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Typed payloads for router FSM events.
|
|
54
|
+
*
|
|
55
|
+
* Events without entries have no payload.
|
|
56
|
+
*/
|
|
57
|
+
export interface RouterPayloads {
|
|
58
|
+
NAVIGATE: {
|
|
59
|
+
toState: State;
|
|
60
|
+
fromState: State | undefined;
|
|
61
|
+
};
|
|
62
|
+
COMPLETE: {
|
|
63
|
+
state: State;
|
|
64
|
+
fromState: State | undefined;
|
|
65
|
+
opts: NavigationOptions;
|
|
66
|
+
};
|
|
67
|
+
FAIL: {
|
|
68
|
+
toState?: State | undefined;
|
|
69
|
+
fromState?: State | undefined;
|
|
70
|
+
error?: unknown;
|
|
71
|
+
};
|
|
72
|
+
CANCEL: {
|
|
73
|
+
toState: State;
|
|
74
|
+
fromState: State | undefined;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Router FSM configuration.
|
|
80
|
+
*
|
|
81
|
+
* Transitions:
|
|
82
|
+
* - IDLE → STARTING (START), DISPOSED (DISPOSE)
|
|
83
|
+
* - STARTING → READY (STARTED), IDLE (FAIL)
|
|
84
|
+
* - READY → TRANSITIONING (NAVIGATE), READY (FAIL, self-loop for early validation errors), IDLE (STOP)
|
|
85
|
+
* - TRANSITIONING → TRANSITIONING (NAVIGATE, self-loop for canSend), READY (COMPLETE, CANCEL, FAIL)
|
|
86
|
+
* - DISPOSED → (no transitions)
|
|
87
|
+
*/
|
|
88
|
+
const routerFSMConfig: FSMConfig<RouterState, RouterEvent, null> = {
|
|
89
|
+
initial: routerStates.IDLE,
|
|
90
|
+
context: null,
|
|
91
|
+
transitions: {
|
|
92
|
+
[routerStates.IDLE]: {
|
|
93
|
+
[routerEvents.START]: routerStates.STARTING,
|
|
94
|
+
[routerEvents.DISPOSE]: routerStates.DISPOSED,
|
|
95
|
+
},
|
|
96
|
+
[routerStates.STARTING]: {
|
|
97
|
+
[routerEvents.STARTED]: routerStates.READY,
|
|
98
|
+
[routerEvents.FAIL]: routerStates.IDLE,
|
|
99
|
+
},
|
|
100
|
+
[routerStates.READY]: {
|
|
101
|
+
[routerEvents.NAVIGATE]: routerStates.TRANSITIONING,
|
|
102
|
+
[routerEvents.FAIL]: routerStates.READY,
|
|
103
|
+
[routerEvents.STOP]: routerStates.IDLE,
|
|
104
|
+
},
|
|
105
|
+
[routerStates.TRANSITIONING]: {
|
|
106
|
+
[routerEvents.NAVIGATE]: routerStates.TRANSITIONING,
|
|
107
|
+
[routerEvents.COMPLETE]: routerStates.READY,
|
|
108
|
+
[routerEvents.CANCEL]: routerStates.READY,
|
|
109
|
+
[routerEvents.FAIL]: routerStates.READY,
|
|
110
|
+
},
|
|
111
|
+
[routerStates.DISPOSED]: {},
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Factory function to create a router FSM instance.
|
|
117
|
+
*
|
|
118
|
+
* @returns FSM instance with initial state "IDLE"
|
|
119
|
+
*/
|
|
120
|
+
export function createRouterFSM(): FSM<
|
|
121
|
+
RouterState,
|
|
122
|
+
RouterEvent,
|
|
123
|
+
null,
|
|
124
|
+
RouterPayloads
|
|
125
|
+
> {
|
|
126
|
+
return new FSM<RouterState, RouterEvent, null, RouterPayloads>(
|
|
127
|
+
routerFSMConfig,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Router } from "./Router";
|
|
2
|
+
import type { Navigator, DefaultDependencies } from "@real-router/types";
|
|
3
|
+
|
|
4
|
+
export const getNavigator = <
|
|
5
|
+
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
6
|
+
>(
|
|
7
|
+
router: Router<Dependencies>,
|
|
8
|
+
): Navigator =>
|
|
9
|
+
Object.freeze({
|
|
10
|
+
navigate: router.navigate,
|
|
11
|
+
getState: router.getState,
|
|
12
|
+
isActiveRoute: router.isActiveRoute,
|
|
13
|
+
canNavigateTo: router.canNavigateTo,
|
|
14
|
+
subscribe: router.subscribe,
|
|
15
|
+
});
|