pulse-js-framework 1.5.1 → 1.5.3

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.
@@ -15,13 +15,21 @@ export class LRUCache {
15
15
  /**
16
16
  * Create an LRU cache
17
17
  * @param {number} capacity - Maximum number of items to store
18
+ * @param {Object} [options] - Configuration options
19
+ * @param {boolean} [options.trackMetrics=false] - Enable hit/miss/eviction tracking
18
20
  */
19
- constructor(capacity) {
21
+ constructor(capacity, options = {}) {
20
22
  if (capacity <= 0) {
21
23
  throw new Error('LRU cache capacity must be greater than 0');
22
24
  }
23
25
  this._capacity = capacity;
24
26
  this._cache = new Map();
27
+
28
+ // Metrics tracking
29
+ this._trackMetrics = options.trackMetrics || false;
30
+ this._hits = 0;
31
+ this._misses = 0;
32
+ this._evictions = 0;
25
33
  }
26
34
 
27
35
  /**
@@ -32,9 +40,12 @@ export class LRUCache {
32
40
  */
33
41
  get(key) {
34
42
  if (!this._cache.has(key)) {
43
+ if (this._trackMetrics) this._misses++;
35
44
  return undefined;
36
45
  }
37
46
 
47
+ if (this._trackMetrics) this._hits++;
48
+
38
49
  // Move to end (most recently used) by re-inserting
39
50
  const value = this._cache.get(key);
40
51
  this._cache.delete(key);
@@ -57,6 +68,7 @@ export class LRUCache {
57
68
  // Remove oldest (first item in Map)
58
69
  const oldest = this._cache.keys().next().value;
59
70
  this._cache.delete(oldest);
71
+ if (this._trackMetrics) this._evictions++;
60
72
  }
61
73
 
62
74
  this._cache.set(key, value);
@@ -136,6 +148,46 @@ export class LRUCache {
136
148
  forEach(callback) {
137
149
  this._cache.forEach((value, key) => callback(value, key, this));
138
150
  }
151
+
152
+ /**
153
+ * Get cache performance metrics
154
+ * Only available if trackMetrics option was enabled
155
+ * @returns {{hits: number, misses: number, evictions: number, hitRate: number, size: number, capacity: number}}
156
+ * @example
157
+ * const cache = new LRUCache(100, { trackMetrics: true });
158
+ * // ... use cache ...
159
+ * const stats = cache.getMetrics();
160
+ * console.log(`Hit rate: ${(stats.hitRate * 100).toFixed(1)}%`);
161
+ */
162
+ getMetrics() {
163
+ const total = this._hits + this._misses;
164
+ return {
165
+ hits: this._hits,
166
+ misses: this._misses,
167
+ evictions: this._evictions,
168
+ hitRate: total > 0 ? this._hits / total : 0,
169
+ size: this._cache.size,
170
+ capacity: this._capacity
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Reset all metrics counters to zero
176
+ * Useful for measuring metrics over specific time periods
177
+ */
178
+ resetMetrics() {
179
+ this._hits = 0;
180
+ this._misses = 0;
181
+ this._evictions = 0;
182
+ }
183
+
184
+ /**
185
+ * Enable or disable metrics tracking
186
+ * @param {boolean} enabled - Whether to track metrics
187
+ */
188
+ setMetricsTracking(enabled) {
189
+ this._trackMetrics = enabled;
190
+ }
139
191
  }
140
192
 
141
193
  export default LRUCache;
package/runtime/pulse.js CHANGED
@@ -69,6 +69,14 @@ const log = loggers.pulse;
69
69
  * @property {Map<string, Set<EffectFn>>} effectRegistry - Module ID to effects mapping for HMR
70
70
  */
71
71
 
72
+ /**
73
+ * Maximum number of effect re-run iterations before aborting.
74
+ * Prevents infinite loops when effects trigger each other cyclically.
75
+ * Set to 100 to allow deep chain reactions while catching most real loops.
76
+ * @type {number}
77
+ */
78
+ const MAX_EFFECT_ITERATIONS = 100;
79
+
72
80
  /**
73
81
  * Global reactive context - holds all tracking state.
74
82
  * Exported for testing purposes (use resetContext() to reset).
@@ -102,6 +110,63 @@ export function resetContext() {
102
110
  context.effectRegistry.clear();
103
111
  }
104
112
 
113
+ /**
114
+ * Counter for generating unique effect IDs
115
+ * @type {number}
116
+ */
117
+ let effectIdCounter = 0;
118
+
119
+ /**
120
+ * Global effect error handler
121
+ * @type {Function|null}
122
+ */
123
+ let globalEffectErrorHandler = null;
124
+
125
+ /**
126
+ * Custom error class for effect-related errors with context information.
127
+ * Provides details about which effect failed, in what phase, and its dependencies.
128
+ */
129
+ export class EffectError extends Error {
130
+ /**
131
+ * Create an EffectError with context information
132
+ * @param {string} message - Error message
133
+ * @param {Object} options - Error context
134
+ * @param {string} [options.effectId] - Effect identifier
135
+ * @param {string} [options.phase] - Phase when error occurred ('cleanup' | 'execution')
136
+ * @param {number} [options.dependencyCount] - Number of dependencies
137
+ * @param {Error} [options.cause] - Original error that caused this
138
+ */
139
+ constructor(message, options = {}) {
140
+ super(message);
141
+ this.name = 'EffectError';
142
+ this.effectId = options.effectId || null;
143
+ this.phase = options.phase || 'unknown';
144
+ this.dependencyCount = options.dependencyCount ?? 0;
145
+ this.cause = options.cause || null;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Set a global error handler for effect errors.
151
+ * The handler receives an EffectError with full context about the failure.
152
+ * @param {Function|null} handler - Error handler (effectError) => void, or null to clear
153
+ * @returns {Function|null} Previous handler (for restoration)
154
+ * @example
155
+ * // Set up global error tracking
156
+ * const prevHandler = onEffectError((err) => {
157
+ * console.error(`Effect ${err.effectId} failed during ${err.phase}:`, err.cause);
158
+ * reportToErrorService(err);
159
+ * });
160
+ *
161
+ * // Later, restore previous handler
162
+ * onEffectError(prevHandler);
163
+ */
164
+ export function onEffectError(handler) {
165
+ const prev = globalEffectErrorHandler;
166
+ globalEffectErrorHandler = handler;
167
+ return prev;
168
+ }
169
+
105
170
  /**
106
171
  * Set the current module ID for HMR effect tracking.
107
172
  * Effects created while a module ID is set will be registered for cleanup.
@@ -386,6 +451,52 @@ export class Pulse {
386
451
  }
387
452
  }
388
453
 
454
+ /**
455
+ * Handle an effect error with full context information.
456
+ * Tries effect-specific handler, then global handler, then logs.
457
+ * @private
458
+ * @param {Error} error - The original error
459
+ * @param {EffectFn} effectFn - The effect that errored
460
+ * @param {string} phase - Phase when error occurred ('cleanup' | 'execution')
461
+ */
462
+ function handleEffectError(error, effectFn, phase) {
463
+ const effectError = new EffectError(
464
+ `Effect [${effectFn.id}] error during ${phase}: ${error.message}`,
465
+ {
466
+ effectId: effectFn.id,
467
+ phase,
468
+ dependencyCount: effectFn.dependencies?.size ?? 0,
469
+ cause: error
470
+ }
471
+ );
472
+
473
+ // Try effect-specific handler first
474
+ if (effectFn.onError) {
475
+ try {
476
+ effectFn.onError(effectError);
477
+ return;
478
+ } catch (handlerError) {
479
+ log.error('Effect onError handler threw:', handlerError);
480
+ }
481
+ }
482
+
483
+ // Try global handler
484
+ if (globalEffectErrorHandler) {
485
+ try {
486
+ globalEffectErrorHandler(effectError);
487
+ return;
488
+ } catch (handlerError) {
489
+ log.error('Global effect error handler threw:', handlerError);
490
+ }
491
+ }
492
+
493
+ // Default: log with context
494
+ log.error(`[${effectError.effectId}] ${effectError.message}`, {
495
+ phase: effectError.phase,
496
+ dependencies: effectError.dependencyCount
497
+ });
498
+ }
499
+
389
500
  /**
390
501
  * Run a single effect safely
391
502
  * @private
@@ -398,7 +509,7 @@ function runEffect(effectFn) {
398
509
  try {
399
510
  effectFn.run();
400
511
  } catch (error) {
401
- log.error('Effect error:', error);
512
+ handleEffectError(error, effectFn, 'execution');
402
513
  }
403
514
  }
404
515
 
@@ -412,10 +523,9 @@ function flushEffects() {
412
523
 
413
524
  context.isRunningEffects = true;
414
525
  let iterations = 0;
415
- const maxIterations = 100; // Prevent infinite loops
416
526
 
417
527
  try {
418
- while (context.pendingEffects.size > 0 && iterations < maxIterations) {
528
+ while (context.pendingEffects.size > 0 && iterations < MAX_EFFECT_ITERATIONS) {
419
529
  iterations++;
420
530
  const effects = [...context.pendingEffects];
421
531
  context.pendingEffects.clear();
@@ -425,8 +535,8 @@ function flushEffects() {
425
535
  }
426
536
  }
427
537
 
428
- if (iterations >= maxIterations) {
429
- log.warn('Maximum effect iterations reached. Possible infinite loop.');
538
+ if (iterations >= MAX_EFFECT_ITERATIONS) {
539
+ log.warn(`Maximum effect iterations (${MAX_EFFECT_ITERATIONS}) reached. Possible infinite loop.`);
430
540
  context.pendingEffects.clear();
431
541
  }
432
542
  } finally {
@@ -483,6 +593,8 @@ export function computed(fn, options = {}) {
483
593
 
484
594
  // Track which pulses this depends on
485
595
  let trackedDeps = new Set();
596
+ // Track subscription cleanup functions to prevent memory leaks
597
+ let subscriptionCleanups = [];
486
598
 
487
599
  p.get = function() {
488
600
  if (dirty) {
@@ -500,18 +612,21 @@ export function computed(fn, options = {}) {
500
612
  dirty = false;
501
613
 
502
614
  // Cleanup old subscriptions
503
- for (const dep of trackedDeps) {
504
- dep._unsubscribe(markDirty);
615
+ for (const unsubscribe of subscriptionCleanups) {
616
+ unsubscribe();
505
617
  }
618
+ subscriptionCleanups = [];
619
+ trackedDeps.clear();
506
620
 
507
621
  // Set up new subscriptions
508
622
  trackedDeps = tempEffect.dependencies;
509
623
  for (const dep of trackedDeps) {
510
- dep.subscribe(() => {
624
+ const unsubscribe = dep.subscribe(() => {
511
625
  dirty = true;
512
626
  // Notify our own subscribers
513
627
  p._triggerNotify();
514
628
  });
629
+ subscriptionCleanups.push(unsubscribe);
515
630
  }
516
631
 
517
632
  p._init(cachedValue);
@@ -529,7 +644,14 @@ export function computed(fn, options = {}) {
529
644
  return cachedValue;
530
645
  };
531
646
 
532
- const markDirty = { run: () => { dirty = true; }, dependencies: new Set(), cleanups: [] };
647
+ // Cleanup function for lazy computed
648
+ cleanup = () => {
649
+ for (const unsubscribe of subscriptionCleanups) {
650
+ unsubscribe();
651
+ }
652
+ subscriptionCleanups = [];
653
+ trackedDeps.clear();
654
+ };
533
655
  } else {
534
656
  // Eager computed - updates immediately when dependencies change
535
657
  cleanup = effect(() => {
@@ -568,9 +690,16 @@ export function computed(fn, options = {}) {
568
690
  return p;
569
691
  }
570
692
 
693
+ /**
694
+ * @typedef {Object} EffectOptions
695
+ * @property {string} [id] - Custom effect identifier for debugging
696
+ * @property {function(EffectError): void} [onError] - Error handler for this effect
697
+ */
698
+
571
699
  /**
572
700
  * Create an effect that runs when its dependencies change
573
701
  * @param {function(): void|function(): void} fn - Effect function, may return a cleanup function
702
+ * @param {EffectOptions} [options={}] - Effect configuration options
574
703
  * @returns {function(): void} Dispose function to stop the effect
575
704
  * @example
576
705
  * const count = pulse(0);
@@ -588,19 +717,32 @@ export function computed(fn, options = {}) {
588
717
  * const timer = setInterval(() => tick(), 1000);
589
718
  * return () => clearInterval(timer); // Cleanup on re-run or dispose
590
719
  * });
720
+ *
721
+ * // With custom ID and error handler
722
+ * effect(() => {
723
+ * // Effect logic that might fail
724
+ * }, {
725
+ * id: 'data-sync',
726
+ * onError: (err) => console.error('Data sync failed:', err.cause)
727
+ * });
591
728
  */
592
- export function effect(fn) {
729
+ export function effect(fn, options = {}) {
730
+ const { id: customId, onError } = options;
731
+ const effectId = customId || `effect_${++effectIdCounter}`;
732
+
593
733
  // Capture module ID at creation time for HMR tracking
594
734
  const moduleId = context.currentModuleId;
595
735
 
596
736
  const effectFn = {
737
+ id: effectId,
738
+ onError,
597
739
  run: () => {
598
740
  // Run cleanup functions from previous run
599
741
  for (const cleanup of effectFn.cleanups) {
600
742
  try {
601
743
  cleanup();
602
744
  } catch (e) {
603
- log.error('Cleanup error:', e);
745
+ handleEffectError(e, effectFn, 'cleanup');
604
746
  }
605
747
  }
606
748
  effectFn.cleanups = [];
@@ -618,7 +760,7 @@ export function effect(fn) {
618
760
  try {
619
761
  fn();
620
762
  } catch (error) {
621
- log.error('Effect execution error:', error);
763
+ handleEffectError(error, effectFn, 'execution');
622
764
  } finally {
623
765
  context.currentEffect = prevEffect;
624
766
  }
@@ -645,7 +787,7 @@ export function effect(fn) {
645
787
  try {
646
788
  cleanup();
647
789
  } catch (e) {
648
- log.error('Cleanup error:', e);
790
+ handleEffectError(e, effectFn, 'cleanup');
649
791
  }
650
792
  }
651
793
  effectFn.cleanups = [];
@@ -994,6 +1136,9 @@ export default {
994
1136
  memoComputed,
995
1137
  context,
996
1138
  resetContext,
1139
+ // Error handling
1140
+ EffectError,
1141
+ onEffectError,
997
1142
  // HMR support
998
1143
  setCurrentModule,
999
1144
  clearCurrentModule,
package/runtime/router.js CHANGED
@@ -225,8 +225,14 @@ function createMiddlewareRunner(middlewares) {
225
225
  if (aborted || redirectPath) return;
226
226
  if (index >= middlewares.length) return;
227
227
 
228
+ const middlewareIndex = index;
228
229
  const middleware = middlewares[index++];
229
- await middleware(ctx, next);
230
+ try {
231
+ await middleware(ctx, next);
232
+ } catch (error) {
233
+ log.error(`Middleware error at index ${middlewareIndex}:`, error);
234
+ throw error; // Re-throw to halt navigation
235
+ }
230
236
  }
231
237
 
232
238
  await next();
@@ -417,23 +423,58 @@ function matchRoute(pattern, path) {
417
423
  return params;
418
424
  }
419
425
 
426
+ // Query string validation limits
427
+ const QUERY_LIMITS = {
428
+ maxTotalLength: 2048, // 2KB max for entire query string
429
+ maxValueLength: 1024, // 1KB max per individual value
430
+ maxParams: 50 // Maximum number of query parameters
431
+ };
432
+
420
433
  /**
421
- * Parse query string into object
434
+ * Parse query string into object with validation
435
+ * @param {string} search - Query string (with or without leading ?)
436
+ * @returns {Object} Parsed query parameters
422
437
  */
423
438
  function parseQuery(search) {
424
- const params = new URLSearchParams(search);
439
+ if (!search) return {};
440
+
441
+ // Remove leading ? if present
442
+ const queryStr = search.startsWith('?') ? search.slice(1) : search;
443
+
444
+ // Validate total length
445
+ if (queryStr.length > QUERY_LIMITS.maxTotalLength) {
446
+ log.warn(`Query string exceeds maximum length (${QUERY_LIMITS.maxTotalLength} chars). Truncating.`);
447
+ }
448
+
449
+ const params = new URLSearchParams(queryStr.slice(0, QUERY_LIMITS.maxTotalLength));
425
450
  const query = {};
451
+ let paramCount = 0;
452
+
426
453
  for (const [key, value] of params) {
454
+ // Check parameter count limit
455
+ if (paramCount >= QUERY_LIMITS.maxParams) {
456
+ log.warn(`Query string exceeds maximum parameters (${QUERY_LIMITS.maxParams}). Ignoring excess.`);
457
+ break;
458
+ }
459
+
460
+ // Validate and potentially truncate value length
461
+ let safeValue = value;
462
+ if (value.length > QUERY_LIMITS.maxValueLength) {
463
+ log.warn(`Query parameter "${key}" exceeds maximum length. Truncating.`);
464
+ safeValue = value.slice(0, QUERY_LIMITS.maxValueLength);
465
+ }
466
+
427
467
  if (key in query) {
428
468
  // Multiple values for same key
429
469
  if (Array.isArray(query[key])) {
430
- query[key].push(value);
470
+ query[key].push(safeValue);
431
471
  } else {
432
- query[key] = [query[key], value];
472
+ query[key] = [query[key], safeValue];
433
473
  }
434
474
  } else {
435
- query[key] = value;
475
+ query[key] = safeValue;
436
476
  }
477
+ paramCount++;
437
478
  }
438
479
  return query;
439
480
  }
@@ -460,6 +501,10 @@ export function createRouter(options = {}) {
460
501
  const currentQuery = pulse({});
461
502
  const currentMeta = pulse({});
462
503
  const isLoading = pulse(false);
504
+ const routeError = pulse(null);
505
+
506
+ // Route error handler (configurable)
507
+ let onRouteError = options.onRouteError || null;
463
508
 
464
509
  // Scroll positions for history
465
510
  const scrollPositions = new Map();
@@ -793,25 +838,65 @@ export function createRouter(options = {}) {
793
838
  router
794
839
  };
795
840
 
796
- // Call handler and render result
797
- const result = typeof route.handler === 'function'
798
- ? route.handler(ctx)
799
- : route.handler;
841
+ // Helper to handle errors
842
+ const handleError = (error) => {
843
+ routeError.set(error);
844
+ log.error('Route component error:', error);
845
+
846
+ if (onRouteError) {
847
+ try {
848
+ const errorView = onRouteError(error, ctx);
849
+ if (errorView instanceof Node) {
850
+ container.replaceChildren(errorView);
851
+ currentView = errorView;
852
+ return true;
853
+ }
854
+ } catch (handlerError) {
855
+ log.error('Route error handler threw:', handlerError);
856
+ }
857
+ }
858
+
859
+ const errorEl = el('div.route-error', [
860
+ el('h2', 'Route Error'),
861
+ el('p', error.message || 'Failed to load route component')
862
+ ]);
863
+ container.replaceChildren(errorEl);
864
+ currentView = errorEl;
865
+ return true;
866
+ };
867
+
868
+ // Call handler and render result (with error handling)
869
+ let result;
870
+ try {
871
+ result = typeof route.handler === 'function'
872
+ ? route.handler(ctx)
873
+ : route.handler;
874
+ } catch (error) {
875
+ handleError(error);
876
+ return;
877
+ }
800
878
 
801
879
  if (result instanceof Node) {
802
880
  container.appendChild(result);
803
881
  currentView = result;
882
+ routeError.set(null);
804
883
  } else if (result && typeof result.then === 'function') {
805
884
  // Async component
806
885
  isLoading.set(true);
807
- result.then(component => {
808
- isLoading.set(false);
809
- const view = typeof component === 'function' ? component(ctx) : component;
810
- if (view instanceof Node) {
811
- container.appendChild(view);
812
- currentView = view;
813
- }
814
- });
886
+ routeError.set(null);
887
+ result
888
+ .then(component => {
889
+ isLoading.set(false);
890
+ const view = typeof component === 'function' ? component(ctx) : component;
891
+ if (view instanceof Node) {
892
+ container.appendChild(view);
893
+ currentView = view;
894
+ }
895
+ })
896
+ .catch(error => {
897
+ isLoading.set(false);
898
+ handleError(error);
899
+ });
815
900
  }
816
901
  }
817
902
  });
@@ -868,6 +953,17 @@ export function createRouter(options = {}) {
868
953
  /**
869
954
  * Check if a route matches the given path
870
955
  */
956
+ /**
957
+ * Check if a path matches the current route
958
+ * @param {string} path - Path to check
959
+ * @param {boolean} [exact=false] - If true, requires exact match; if false, matches prefixes
960
+ * @returns {boolean} True if path is active
961
+ * @example
962
+ * // Current path: /users/123
963
+ * router.isActive('/users'); // true (prefix match)
964
+ * router.isActive('/users', true); // false (not exact)
965
+ * router.isActive('/users/123', true); // true (exact match)
966
+ */
871
967
  function isActive(path, exact = false) {
872
968
  const current = currentPath.get();
873
969
  if (exact) {
@@ -877,7 +973,12 @@ export function createRouter(options = {}) {
877
973
  }
878
974
 
879
975
  /**
880
- * Get all matched routes (for nested routes)
976
+ * Get all routes that match a given path (useful for nested routes)
977
+ * @param {string} path - Path to match against routes
978
+ * @returns {Array<{route: Object, params: Object}>} Array of matched routes with extracted params
979
+ * @example
980
+ * const matches = router.getMatchedRoutes('/admin/users/123');
981
+ * // Returns: [{route: adminRoute, params: {}}, {route: userRoute, params: {id: '123'}}]
881
982
  */
882
983
  function getMatchedRoutes(path) {
883
984
  const matches = [];
@@ -891,53 +992,107 @@ export function createRouter(options = {}) {
891
992
  }
892
993
 
893
994
  /**
894
- * Go back in history
995
+ * Navigate back in browser history
996
+ * Equivalent to browser back button
997
+ * @returns {void}
998
+ * @example
999
+ * router.back(); // Go to previous page
895
1000
  */
896
1001
  function back() {
897
1002
  window.history.back();
898
1003
  }
899
1004
 
900
1005
  /**
901
- * Go forward in history
1006
+ * Navigate forward in browser history
1007
+ * Equivalent to browser forward button
1008
+ * @returns {void}
1009
+ * @example
1010
+ * router.forward(); // Go to next page (if available)
902
1011
  */
903
1012
  function forward() {
904
1013
  window.history.forward();
905
1014
  }
906
1015
 
907
1016
  /**
908
- * Go to specific history entry
1017
+ * Navigate to a specific position in browser history
1018
+ * @param {number} delta - Number of entries to move (negative = back, positive = forward)
1019
+ * @returns {void}
1020
+ * @example
1021
+ * router.go(-2); // Go back 2 pages
1022
+ * router.go(1); // Go forward 1 page
909
1023
  */
910
1024
  function go(delta) {
911
1025
  window.history.go(delta);
912
1026
  }
913
1027
 
1028
+ /**
1029
+ * Set route error handler
1030
+ * @param {function} handler - Error handler (error, ctx) => Node
1031
+ * @returns {function} Previous handler
1032
+ */
1033
+ function setErrorHandler(handler) {
1034
+ const prev = onRouteError;
1035
+ onRouteError = handler;
1036
+ return prev;
1037
+ }
1038
+
1039
+ /**
1040
+ * Router instance with reactive state and navigation methods.
1041
+ *
1042
+ * Reactive properties (use .get() to read value, auto-updates in effects):
1043
+ * - path: Current URL path as string
1044
+ * - route: Current matched route object or null
1045
+ * - params: Route params object, e.g., {id: '123'}
1046
+ * - query: Query params object, e.g., {page: '1'}
1047
+ * - meta: Route meta data object
1048
+ * - loading: Boolean indicating async route loading
1049
+ * - error: Current route error or null
1050
+ *
1051
+ * @example
1052
+ * // Read reactive state
1053
+ * router.path.get(); // '/users/123'
1054
+ * router.params.get(); // {id: '123'}
1055
+ *
1056
+ * // Subscribe to changes
1057
+ * effect(() => {
1058
+ * console.log('Path changed:', router.path.get());
1059
+ * });
1060
+ *
1061
+ * // Navigate
1062
+ * router.navigate('/users/456');
1063
+ * router.back();
1064
+ */
914
1065
  const router = {
915
- // Reactive state (read-only)
1066
+ // Reactive state (read-only) - use .get() to read, subscribe with effects
916
1067
  path: currentPath,
917
1068
  route: currentRoute,
918
1069
  params: currentParams,
919
1070
  query: currentQuery,
920
1071
  meta: currentMeta,
921
1072
  loading: isLoading,
1073
+ error: routeError,
922
1074
 
923
- // Methods
1075
+ // Navigation methods
924
1076
  navigate,
925
1077
  start,
926
1078
  link,
927
1079
  outlet,
1080
+ back,
1081
+ forward,
1082
+ go,
1083
+
1084
+ // Guards and middleware
928
1085
  use,
929
1086
  beforeEach,
930
1087
  beforeResolve,
931
1088
  afterEach,
932
- back,
933
- forward,
934
- go,
1089
+ setErrorHandler,
935
1090
 
936
1091
  // Route inspection
937
1092
  isActive,
938
1093
  getMatchedRoutes,
939
1094
 
940
- // Utils
1095
+ // Utility functions
941
1096
  matchRoute,
942
1097
  parseQuery
943
1098
  };