@vobs/router 0.1.0 → 0.3.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/dist/history.d.ts CHANGED
@@ -21,5 +21,5 @@ export interface ScrollRestoration {
21
21
  settle(action: 'none' | 'push' | 'replace', historyEntryKey: string | undefined): void;
22
22
  destroy(): void;
23
23
  }
24
- export declare function createScrollRestoration(window: RouterWindow | undefined): ScrollRestoration;
24
+ export declare function createScrollRestoration(window: RouterWindow | undefined, maxEntries?: number): ScrollRestoration;
25
25
  export {};
package/dist/history.js CHANGED
@@ -85,12 +85,16 @@ export function resolveHistoryWindow(options) {
85
85
  ? (options.window ?? defaultRouterWindow())
86
86
  : defaultRouterWindow();
87
87
  }
88
- export function createScrollRestoration(window) {
88
+ export function createScrollRestoration(window, maxEntries = 100) {
89
89
  const positions = new Map();
90
+ const positionLimit = Number.isFinite(maxEntries) ? Math.max(0, Math.trunc(maxEntries)) : 100;
90
91
  let currentKey;
91
- if (window?.history?.scrollRestoration !== undefined) {
92
+ const previousScrollRestoration = window?.history?.scrollRestoration;
93
+ let changedScrollRestoration = false;
94
+ if (previousScrollRestoration !== undefined && window !== undefined) {
92
95
  try {
93
96
  window.history.scrollRestoration = 'manual';
97
+ changedScrollRestoration = true;
94
98
  }
95
99
  catch {
96
100
  // ignore read-only scrollRestoration (e.g. sandboxed iframes)
@@ -107,7 +111,16 @@ export function createScrollRestoration(window) {
107
111
  saveCurrent() {
108
112
  if (window === undefined || currentKey === undefined)
109
113
  return;
114
+ if (positionLimit === 0)
115
+ return;
116
+ positions.delete(currentKey);
110
117
  positions.set(currentKey, readPosition());
118
+ while (positions.size > positionLimit) {
119
+ const oldestKey = positions.keys().next().value;
120
+ if (oldestKey === undefined)
121
+ break;
122
+ positions.delete(oldestKey);
123
+ }
111
124
  },
112
125
  settle(action, historyEntryKey) {
113
126
  if (window === undefined)
@@ -121,6 +134,14 @@ export function createScrollRestoration(window) {
121
134
  destroy() {
122
135
  positions.clear();
123
136
  currentKey = undefined;
137
+ if (changedScrollRestoration && window !== undefined) {
138
+ try {
139
+ window.history.scrollRestoration = previousScrollRestoration;
140
+ }
141
+ catch {
142
+ // ignore read-only scrollRestoration (e.g. sandboxed iframes)
143
+ }
144
+ }
124
145
  },
125
146
  };
126
147
  }
package/dist/resolve.js CHANGED
@@ -8,6 +8,7 @@ import { resolveRouteMatch } from './match.js';
8
8
  import { createRouteLocation } from './location.js';
9
9
  export const defaultRedirectLimit = 8;
10
10
  const routeComponentCache = new WeakMap();
11
+ const pendingLoaderSignals = new WeakMap();
11
12
  export function resolveRoute(options) {
12
13
  const match = resolveRouteMatch(options.routes, options.path, options.redirectLimit ?? defaultRedirectLimit, 0);
13
14
  const component = match?.component ?? options.notFound;
@@ -44,7 +45,16 @@ export function resolveRouteInitialState(route, options = {}) {
44
45
  const context = createRouteLoaderContext(route.route, options);
45
46
  const cacheKey = readLoaderCacheKey(route.loaderCache, route.route, options);
46
47
  if (cacheKey !== undefined && options.loaderCache?.has(cacheKey) === true) {
47
- return options.loaderCache.get(cacheKey);
48
+ const cached = options.loaderCache.get(cacheKey);
49
+ if (!isPromiseLike(cached))
50
+ return cached;
51
+ // A pending loader may belong to a superseded navigation. Keep prefetch deduplication,
52
+ // but do not hand a cancelled navigation's request to a new consumer.
53
+ if (pendingLoaderSignals.get(cached) === undefined ||
54
+ !pendingLoaderSignals.get(cached)?.aborted) {
55
+ return cached;
56
+ }
57
+ options.loaderCache.delete(cacheKey);
48
58
  }
49
59
  const loaded = runWithInjectionScope(context.injections, () => route.loader(context));
50
60
  return cacheLoaderResult(loaded, cacheKey, context.signal, options.loaderCache);
@@ -184,6 +194,7 @@ function cacheLoaderResult(loaded, cacheKey, signal, loaderCache) {
184
194
  }
185
195
  throw error;
186
196
  });
197
+ pendingLoaderSignals.set(pending, signal);
187
198
  writeLoaderCache(cacheKey, pending, signal, loaderCache);
188
199
  return pending;
189
200
  }
package/dist/router.d.ts CHANGED
@@ -2,5 +2,7 @@
2
2
  * Copyright (c) 2026 vobsjs
3
3
  * @vobs/router
4
4
  */
5
- import { type Router, type RouterOptions } from './types.js';
6
- export declare function createRouter(options: RouterOptions): Router;
5
+ import { type RouteRecord, type ValidatedRouteRecords, type Router, type RouterOptions } from './types.js';
6
+ export declare function createRouter<const Routes extends readonly RouteRecord[]>(options: RouterOptions<Routes> & {
7
+ readonly routes: Routes & ValidatedRouteRecords<Routes>;
8
+ }): Router;
package/dist/router.js CHANGED
@@ -44,7 +44,7 @@ export function createRouter(options) {
44
44
  historyBinding = createHistoryBinding(options.history);
45
45
  scrollRestoration =
46
46
  options.scrollRestoration === true
47
- ? createScrollRestoration(resolveHistoryWindow(options.history) ?? defaultRouterWindow())
47
+ ? createScrollRestoration(resolveHistoryWindow(options.history) ?? defaultRouterWindow(), options.scrollRestorationMaxEntries)
48
48
  : undefined;
49
49
  try {
50
50
  await commitNavigation(options.initialPath ?? historyBinding?.readPath() ?? '/', historyBinding === undefined ? 'none' : 'replace', 0, options.hydrate === true ? 'hydrate' : 'mount', undefined);
@@ -339,18 +339,47 @@ export function createRouter(options) {
339
339
  mounted = false;
340
340
  container = undefined;
341
341
  environment = undefined;
342
- historyBinding?.destroy();
342
+ const errors = [];
343
+ try {
344
+ historyBinding?.destroy();
345
+ }
346
+ catch (error) {
347
+ errors.push(error);
348
+ }
343
349
  historyBinding = undefined;
344
350
  const page = currentPage;
345
351
  currentPage = undefined;
346
352
  current.value = null;
347
353
  pending.value = null;
348
- page?.destroy();
349
- keepAliveCache.destroyAll();
350
- scrollRestoration?.destroy();
354
+ try {
355
+ page?.destroy();
356
+ }
357
+ catch (error) {
358
+ errors.push(error);
359
+ }
360
+ try {
361
+ keepAliveCache.destroyAll();
362
+ }
363
+ catch (error) {
364
+ errors.push(error);
365
+ }
366
+ try {
367
+ scrollRestoration?.destroy();
368
+ }
369
+ catch (error) {
370
+ errors.push(error);
371
+ }
351
372
  scrollRestoration = undefined;
352
- routerInjectionScope?.dispose();
373
+ try {
374
+ routerInjectionScope?.dispose();
375
+ }
376
+ catch (error) {
377
+ errors.push(error);
378
+ }
353
379
  routerInjectionScope = undefined;
380
+ if (errors.length > 0) {
381
+ throw new AggregateError(errors, 'Router destroy failed');
382
+ }
354
383
  }
355
384
  return {
356
385
  current,
package/dist/types.d.ts CHANGED
@@ -59,6 +59,18 @@ export interface RouteLoaderCache {
59
59
  clear(): void;
60
60
  }
61
61
  export type RouteRecord<Component extends RouteComponent = RouteComponent> = PageRouteRecord<Component> | NestedRouteRecord<Component> | RedirectRouteRecord;
62
+ type RouteRecordForValue<Route> = Route extends RouteRecord ? Route extends {
63
+ readonly children: infer Children extends readonly RouteRecord[];
64
+ } ? Omit<Route, 'children'> & {
65
+ readonly children: {
66
+ readonly [Key in keyof Children]: RouteRecordForValue<Children[Key]>;
67
+ };
68
+ } : Route extends {
69
+ readonly component: infer Component extends RouteComponent;
70
+ } ? Route & PageRouteRecord<Component> : Route : never;
71
+ export type ValidatedRouteRecords<Routes extends readonly RouteRecord[]> = {
72
+ readonly [Key in keyof Routes]: RouteRecordForValue<Routes[Key]>;
73
+ };
62
74
  export type PageRouteRecord<Component extends RouteComponent = RouteComponent> = PageRouteRecordWithoutInitialState<Component> | PageRouteRecordWithInitialState<Component> | PageRouteRecordWithLoader<Component>;
63
75
  export interface PageRouteRecordWithoutInitialState<Component extends RouteComponent = RouteComponent> {
64
76
  readonly path: string;
@@ -102,7 +114,7 @@ export interface PageRouteRecordWithLoader<Component extends RouteComponent = Ro
102
114
  export interface NestedRouteRecord<Component extends RouteComponent = RouteComponent> {
103
115
  readonly path: string;
104
116
  readonly component?: Component;
105
- readonly children: readonly RouteRecord[];
117
+ readonly children: readonly RouteRecord<Component>[];
106
118
  readonly layouts?: readonly RouteComponent[];
107
119
  readonly error?: RouteComponent;
108
120
  readonly meta?: RouteMeta;
@@ -159,8 +171,8 @@ export interface RouteNavigationScope {
159
171
  readonly description: ReadonlySignal<string>;
160
172
  readonly status: ReadonlySignal<string>;
161
173
  }
162
- export interface RouterOptions {
163
- readonly routes: readonly RouteRecord[];
174
+ export interface RouterOptions<Routes extends readonly RouteRecord[] = readonly RouteRecord[]> {
175
+ readonly routes: Routes;
164
176
  readonly notFound?: RouteComponent | undefined;
165
177
  readonly error?: RouteComponent | undefined;
166
178
  readonly initialPath?: string;
@@ -168,6 +180,8 @@ export interface RouterOptions {
168
180
  readonly hydrate?: boolean;
169
181
  readonly navigation?: RouterNavigationOptions;
170
182
  readonly scrollRestoration?: boolean;
183
+ /** Maximum number of saved history-entry scroll positions. Defaults to 100. */
184
+ readonly scrollRestorationMaxEntries?: number;
171
185
  readonly keepAliveMax?: number;
172
186
  }
173
187
  export interface RouterNavigationOptions {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vobs/router",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Client and server route resolution for vobs pages.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -18,8 +18,8 @@
18
18
  },
19
19
  "homepage": "https://github.com/vobsjs/vobs#readme",
20
20
  "dependencies": {
21
- "@vobs/reactivity": "0.1.0",
22
- "@vobs/runtime-core": "0.1.0"
21
+ "@vobs/reactivity": "0.3.0",
22
+ "@vobs/runtime-core": "0.3.0"
23
23
  },
24
24
  "files": [
25
25
  "dist"
@@ -36,9 +36,9 @@
36
36
  "main": "./dist/index.js",
37
37
  "sideEffects": false,
38
38
  "devDependencies": {
39
- "@vobs/resource": "0.1.0"
39
+ "@vobs/resource": "0.3.0"
40
40
  },
41
41
  "engines": {
42
- "node": ">=20.19.0"
42
+ "node": ">=22.12.0"
43
43
  }
44
44
  }