specnav-next 0.2.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.
@@ -0,0 +1,75 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { Strategy, CacheConfig, PrefetchConfig, MorphConfig, GraphConfig, CacheLayer } from 'specnav-core';
3
+
4
+ interface ProgressConfig {
5
+ enabled: boolean;
6
+ color: string;
7
+ height: number;
8
+ showSpinner: boolean;
9
+ position: "top" | "bottom";
10
+ minimum: number;
11
+ easing: string;
12
+ speed: number;
13
+ trickle: boolean;
14
+ trickleSpeed: number;
15
+ }
16
+ interface TransitionConfig {
17
+ enabled: boolean;
18
+ duration: number;
19
+ easing: string;
20
+ }
21
+ interface NavigateProviderProps {
22
+ children: React.ReactNode;
23
+ strategy?: Strategy;
24
+ cache?: Partial<CacheConfig>;
25
+ prefetch?: Partial<PrefetchConfig>;
26
+ progress?: Partial<ProgressConfig>;
27
+ morph?: Partial<MorphConfig>;
28
+ transitions?: Partial<TransitionConfig>;
29
+ graph?: Partial<GraphConfig>;
30
+ onNavigateStart?: (href: string) => void;
31
+ onNavigateEnd?: (href: string, durationMs: number) => void;
32
+ onCacheHit?: (href: string, layer: CacheLayer) => void;
33
+ }
34
+ interface NavigateOptions {
35
+ replace?: boolean;
36
+ scroll?: boolean;
37
+ morph?: boolean;
38
+ cache?: boolean;
39
+ }
40
+ interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
41
+ href: string;
42
+ prefetch?: "trajectory" | "hover" | "eager" | false;
43
+ morph?: boolean;
44
+ cache?: boolean;
45
+ replace?: boolean;
46
+ scroll?: boolean;
47
+ }
48
+ interface UseNavigateReturn {
49
+ navigate: (href: string, options?: NavigateOptions) => void;
50
+ back: () => void;
51
+ forward: () => void;
52
+ prefetch: (href: string) => Promise<void>;
53
+ clearCache: (href?: string) => void;
54
+ isNavigating: boolean;
55
+ pendingHref: string | null;
56
+ }
57
+ interface UseNavigationStateReturn {
58
+ isNavigating: boolean;
59
+ pendingHref: string | null;
60
+ previousHref: string | null;
61
+ cacheSize: number;
62
+ cacheHitRate: number;
63
+ lastNavigationMs: number;
64
+ progress: number;
65
+ }
66
+
67
+ declare function NavigateProvider({ children, strategy, cache: cacheConfig, prefetch: prefetchConfig, progress: progressConfig, morph: morphConfig, graph: graphConfig, onNavigateStart, onNavigateEnd, onCacheHit, }: NavigateProviderProps): react_jsx_runtime.JSX.Element;
68
+
69
+ declare function Link({ href, prefetch, morph, cache, replace, scroll, children, ...props }: LinkProps): react_jsx_runtime.JSX.Element;
70
+
71
+ declare function useNavigate(): UseNavigateReturn;
72
+
73
+ declare function useNavigationState(): UseNavigationStateReturn;
74
+
75
+ export { Link, type LinkProps, type NavigateOptions, NavigateProvider, type NavigateProviderProps, type ProgressConfig, type TransitionConfig, type UseNavigateReturn, type UseNavigationStateReturn, useNavigate, useNavigationState };
@@ -0,0 +1,75 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { Strategy, CacheConfig, PrefetchConfig, MorphConfig, GraphConfig, CacheLayer } from 'specnav-core';
3
+
4
+ interface ProgressConfig {
5
+ enabled: boolean;
6
+ color: string;
7
+ height: number;
8
+ showSpinner: boolean;
9
+ position: "top" | "bottom";
10
+ minimum: number;
11
+ easing: string;
12
+ speed: number;
13
+ trickle: boolean;
14
+ trickleSpeed: number;
15
+ }
16
+ interface TransitionConfig {
17
+ enabled: boolean;
18
+ duration: number;
19
+ easing: string;
20
+ }
21
+ interface NavigateProviderProps {
22
+ children: React.ReactNode;
23
+ strategy?: Strategy;
24
+ cache?: Partial<CacheConfig>;
25
+ prefetch?: Partial<PrefetchConfig>;
26
+ progress?: Partial<ProgressConfig>;
27
+ morph?: Partial<MorphConfig>;
28
+ transitions?: Partial<TransitionConfig>;
29
+ graph?: Partial<GraphConfig>;
30
+ onNavigateStart?: (href: string) => void;
31
+ onNavigateEnd?: (href: string, durationMs: number) => void;
32
+ onCacheHit?: (href: string, layer: CacheLayer) => void;
33
+ }
34
+ interface NavigateOptions {
35
+ replace?: boolean;
36
+ scroll?: boolean;
37
+ morph?: boolean;
38
+ cache?: boolean;
39
+ }
40
+ interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
41
+ href: string;
42
+ prefetch?: "trajectory" | "hover" | "eager" | false;
43
+ morph?: boolean;
44
+ cache?: boolean;
45
+ replace?: boolean;
46
+ scroll?: boolean;
47
+ }
48
+ interface UseNavigateReturn {
49
+ navigate: (href: string, options?: NavigateOptions) => void;
50
+ back: () => void;
51
+ forward: () => void;
52
+ prefetch: (href: string) => Promise<void>;
53
+ clearCache: (href?: string) => void;
54
+ isNavigating: boolean;
55
+ pendingHref: string | null;
56
+ }
57
+ interface UseNavigationStateReturn {
58
+ isNavigating: boolean;
59
+ pendingHref: string | null;
60
+ previousHref: string | null;
61
+ cacheSize: number;
62
+ cacheHitRate: number;
63
+ lastNavigationMs: number;
64
+ progress: number;
65
+ }
66
+
67
+ declare function NavigateProvider({ children, strategy, cache: cacheConfig, prefetch: prefetchConfig, progress: progressConfig, morph: morphConfig, graph: graphConfig, onNavigateStart, onNavigateEnd, onCacheHit, }: NavigateProviderProps): react_jsx_runtime.JSX.Element;
68
+
69
+ declare function Link({ href, prefetch, morph, cache, replace, scroll, children, ...props }: LinkProps): react_jsx_runtime.JSX.Element;
70
+
71
+ declare function useNavigate(): UseNavigateReturn;
72
+
73
+ declare function useNavigationState(): UseNavigationStateReturn;
74
+
75
+ export { Link, type LinkProps, type NavigateOptions, NavigateProvider, type NavigateProviderProps, type ProgressConfig, type TransitionConfig, type UseNavigateReturn, type UseNavigationStateReturn, useNavigate, useNavigationState };
package/dist/index.js ADDED
@@ -0,0 +1,547 @@
1
+ import { createContext, useState, useRef, useEffect, useContext } from 'react';
2
+ import { createAdaptiveMode, createCacheManager, createMorpher, createSpeculativeRenderer, createNavigationGraphLearner, createTrajectoryEngine } from 'specnav-core';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __esm = (fn, res) => function __init() {
8
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
9
+ };
10
+ var __export = (target, all) => {
11
+ for (var name in all)
12
+ __defProp(target, name, { get: all[name], enumerable: true });
13
+ };
14
+
15
+ // src/prefetch.ts
16
+ var prefetch_exports = {};
17
+ __export(prefetch_exports, {
18
+ inflightRequests: () => inflightRequests,
19
+ prefetchPage: () => prefetchPage
20
+ });
21
+ async function prefetchPage(href, cache, speculator) {
22
+ if (inflightRequests.has(href)) {
23
+ await inflightRequests.get(href);
24
+ return;
25
+ }
26
+ const controller = new AbortController();
27
+ const fetchPromise = fetch(href, {
28
+ headers: { "x-specnav": "1" },
29
+ signal: controller.signal
30
+ }).then((res) => {
31
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
32
+ return res.text();
33
+ }).then((html) => {
34
+ if (cache) cache.set(href, html);
35
+ if (speculator) speculator.speculate(href, html);
36
+ return html;
37
+ }).finally(() => {
38
+ inflightRequests.delete(href);
39
+ });
40
+ inflightRequests.set(href, fetchPromise);
41
+ await fetchPromise;
42
+ }
43
+ var inflightRequests;
44
+ var init_prefetch = __esm({
45
+ "src/prefetch.ts"() {
46
+ inflightRequests = /* @__PURE__ */ new Map();
47
+ }
48
+ });
49
+ var NavigationContext = createContext(null);
50
+ function useNavigationContext() {
51
+ const ctx = useContext(NavigationContext);
52
+ if (!ctx) throw new Error("useNavigationContext must be used within NavigateProvider");
53
+ return ctx;
54
+ }
55
+ function NavigateProvider({
56
+ children,
57
+ strategy = "auto",
58
+ cache: cacheConfig,
59
+ prefetch: prefetchConfig,
60
+ progress: progressConfig,
61
+ morph: morphConfig,
62
+ graph: graphConfig,
63
+ onNavigateStart,
64
+ onNavigateEnd,
65
+ onCacheHit
66
+ }) {
67
+ const [isNavigating, setIsNavigating] = useState(false);
68
+ const [pendingHref, setPendingHref] = useState(null);
69
+ const [progress, setProgress] = useState(0);
70
+ const [cacheHits, setCacheHits] = useState(0);
71
+ const [cacheMisses, setCacheMisses] = useState(0);
72
+ const navigationStartTime = useRef(null);
73
+ const [trajectory, setTrajectory] = useState(null);
74
+ const [cache, setCache] = useState(null);
75
+ const [morpher, setMorpher] = useState(null);
76
+ const [speculator, setSpeculator] = useState(null);
77
+ const [graph, setGraph] = useState(null);
78
+ const [adaptive, setAdaptive] = useState(null);
79
+ const wrappedSetNavigating = (href) => {
80
+ if (href) {
81
+ navigationStartTime.current = Date.now();
82
+ onNavigateStart?.(href);
83
+ } else if (navigationStartTime.current) {
84
+ const duration = Date.now() - navigationStartTime.current;
85
+ if (pendingHref) {
86
+ onNavigateEnd?.(pendingHref, duration);
87
+ }
88
+ navigationStartTime.current = null;
89
+ }
90
+ setIsNavigating(!!href);
91
+ setPendingHref(href);
92
+ };
93
+ useEffect(() => {
94
+ const adaptiveEngine = createAdaptiveMode();
95
+ const cacheEngine = createCacheManager(cacheConfig, {
96
+ onCacheHit: (href, layer) => {
97
+ setCacheHits((prev) => prev + 1);
98
+ onCacheHit?.(href, layer);
99
+ }
100
+ });
101
+ const morpherEngine = createMorpher(morphConfig);
102
+ const speculatorEngine = createSpeculativeRenderer(3);
103
+ const graphEngine = createNavigationGraphLearner(graphConfig);
104
+ adaptiveEngine.waitForInit().then(() => {
105
+ setAdaptive(adaptiveEngine);
106
+ setCache(cacheEngine);
107
+ setMorpher(morpherEngine);
108
+ setSpeculator(speculatorEngine);
109
+ setGraph(graphEngine);
110
+ const effectiveStrategy = adaptiveEngine.getStrategy(strategy);
111
+ if (effectiveStrategy !== "off" && prefetchConfig?.mode === "trajectory") {
112
+ const trajectoryEngine = createTrajectoryEngine(prefetchConfig.trajectory, {
113
+ onPrediction: async (href) => {
114
+ if (!adaptiveEngine?.shouldPrefetch()) return;
115
+ const { prefetchPage: prefetchPage2 } = await Promise.resolve().then(() => (init_prefetch(), prefetch_exports));
116
+ await prefetchPage2(href, cacheEngine, speculatorEngine);
117
+ },
118
+ onCancel: (href) => {
119
+ speculatorEngine?.cancel(href);
120
+ }
121
+ });
122
+ trajectoryEngine.start();
123
+ setTrajectory(trajectoryEngine);
124
+ }
125
+ });
126
+ const handlePopState = async (e) => {
127
+ if (!e.state?.specnav) return;
128
+ const href = window.location.pathname;
129
+ wrappedSetNavigating(href);
130
+ setProgress(0.3);
131
+ try {
132
+ const html = await cache?.get(href);
133
+ if (html && morpher) {
134
+ setProgress(0.7);
135
+ const parser = new DOMParser();
136
+ const doc = parser.parseFromString(html, "text/html");
137
+ const newContent = doc.body;
138
+ morpher.morph(document.body, newContent);
139
+ setProgress(1);
140
+ setTimeout(() => wrappedSetNavigating(null), 100);
141
+ } else {
142
+ window.location.reload();
143
+ }
144
+ } catch (error) {
145
+ console.error("Popstate navigation failed:", error);
146
+ window.location.reload();
147
+ }
148
+ };
149
+ window.addEventListener("popstate", handlePopState);
150
+ return () => {
151
+ trajectory?.stop();
152
+ cache?.destroy();
153
+ speculator?.cancelAll();
154
+ window.removeEventListener("popstate", handlePopState);
155
+ };
156
+ }, [strategy, cacheConfig, prefetchConfig, morphConfig, graphConfig, onCacheHit, onNavigateEnd]);
157
+ const value = {
158
+ trajectory,
159
+ cache,
160
+ morpher,
161
+ speculator,
162
+ graph,
163
+ adaptive,
164
+ isNavigating,
165
+ pendingHref,
166
+ progress,
167
+ cacheHits,
168
+ cacheMisses,
169
+ setNavigating: wrappedSetNavigating,
170
+ setProgress,
171
+ incrementCacheMiss: () => setCacheMisses((prev) => prev + 1)
172
+ };
173
+ return /* @__PURE__ */ jsxs(NavigationContext.Provider, { value, children: [
174
+ progressConfig?.enabled !== false && /* @__PURE__ */ jsx(ProgressBar, { config: progressConfig, progress, isNavigating }),
175
+ children
176
+ ] });
177
+ }
178
+ function ProgressBar({
179
+ config,
180
+ progress,
181
+ isNavigating
182
+ }) {
183
+ const color = config?.color ?? "#6366f1";
184
+ const height = config?.height ?? 3;
185
+ const position = config?.position ?? "top";
186
+ return /* @__PURE__ */ jsx(
187
+ "div",
188
+ {
189
+ style: {
190
+ position: "fixed",
191
+ [position]: 0,
192
+ left: 0,
193
+ right: 0,
194
+ height: `${height}px`,
195
+ zIndex: 9999,
196
+ pointerEvents: "none"
197
+ },
198
+ children: /* @__PURE__ */ jsx(
199
+ "div",
200
+ {
201
+ style: {
202
+ height: "100%",
203
+ width: `${progress * 100}%`,
204
+ background: color,
205
+ transition: isNavigating ? "width 200ms ease" : "opacity 200ms ease",
206
+ opacity: isNavigating ? 1 : 0
207
+ }
208
+ }
209
+ )
210
+ }
211
+ );
212
+ }
213
+ init_prefetch();
214
+ function Link({
215
+ href,
216
+ prefetch = "trajectory",
217
+ morph = true,
218
+ cache = true,
219
+ replace = false,
220
+ scroll = true,
221
+ children,
222
+ ...props
223
+ }) {
224
+ const ctx = useNavigationContext();
225
+ const linkRef = useRef(null);
226
+ const abortControllerRef = useRef(null);
227
+ const isSafeUrl = (url) => {
228
+ try {
229
+ const parsed = new URL(url, window.location.origin);
230
+ return parsed.origin === window.location.origin;
231
+ } catch {
232
+ return false;
233
+ }
234
+ };
235
+ useEffect(() => {
236
+ if (!linkRef.current || !ctx.trajectory || prefetch === false) return;
237
+ const element = linkRef.current;
238
+ ctx.trajectory.registerLink(href, element);
239
+ return () => {
240
+ ctx.trajectory?.unregisterLink(element);
241
+ abortControllerRef.current?.abort();
242
+ };
243
+ }, [href, ctx.trajectory, prefetch]);
244
+ const handleMouseEnter = async () => {
245
+ if (prefetch === "hover" && ctx.adaptive?.shouldPrefetch()) {
246
+ await prefetchPage2(href);
247
+ }
248
+ };
249
+ const handleMouseLeave = () => {
250
+ };
251
+ const handleFocus = async () => {
252
+ if (ctx.adaptive?.shouldPrefetch()) {
253
+ await prefetchPage2(href);
254
+ }
255
+ };
256
+ const prefetchPage2 = async (url) => {
257
+ if (!isSafeUrl(url) || !ctx.cache) return;
258
+ const cached = await ctx.cache.get(url);
259
+ if (cached) return;
260
+ if (inflightRequests.has(url)) {
261
+ await inflightRequests.get(url);
262
+ return;
263
+ }
264
+ abortControllerRef.current = new AbortController();
265
+ const fetchPromise = fetch(url, {
266
+ headers: { "x-specnav": "1" },
267
+ signal: abortControllerRef.current.signal
268
+ }).then((res) => res.text()).then((html) => {
269
+ ctx.cache?.set(url, html);
270
+ if (ctx.speculator && ctx.adaptive?.shouldSpeculate()) {
271
+ ctx.speculator.speculate(url, html);
272
+ }
273
+ return html;
274
+ }).catch((err) => {
275
+ if (err.name !== "AbortError") {
276
+ console.warn("Prefetch failed:", err);
277
+ }
278
+ throw err;
279
+ }).finally(() => {
280
+ inflightRequests.delete(url);
281
+ });
282
+ inflightRequests.set(url, fetchPromise);
283
+ await fetchPromise;
284
+ };
285
+ const handleClick = async (e) => {
286
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
287
+ e.preventDefault();
288
+ if (!isSafeUrl(href)) {
289
+ window.location.href = href;
290
+ return;
291
+ }
292
+ if (!morph || !ctx.morpher || !ctx.cache) {
293
+ window.location.href = href;
294
+ return;
295
+ }
296
+ ctx.setNavigating(href);
297
+ ctx.setProgress(0.3);
298
+ const timeoutId = setTimeout(() => {
299
+ console.error("Navigation timeout");
300
+ ctx.setNavigating(null);
301
+ window.location.href = href;
302
+ }, 1e4);
303
+ try {
304
+ let newContent = ctx.speculator?.get(href);
305
+ if (!newContent) {
306
+ let html = cache ? await ctx.cache.get(href) : null;
307
+ if (!html) {
308
+ ctx.incrementCacheMiss();
309
+ ctx.setProgress(0.5);
310
+ if (inflightRequests.has(href)) {
311
+ html = await inflightRequests.get(href);
312
+ } else {
313
+ const controller = new AbortController();
314
+ const fetchPromise = fetch(href, {
315
+ headers: { "x-specnav": "1" },
316
+ signal: controller.signal
317
+ }).then((res) => {
318
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
319
+ return res.text();
320
+ });
321
+ inflightRequests.set(href, fetchPromise);
322
+ html = await fetchPromise;
323
+ inflightRequests.delete(href);
324
+ }
325
+ if (cache) {
326
+ await ctx.cache.set(href, html);
327
+ }
328
+ }
329
+ ctx.setProgress(0.7);
330
+ const parser = new DOMParser();
331
+ const doc = parser.parseFromString(html, "text/html");
332
+ newContent = doc.body;
333
+ }
334
+ ctx.setProgress(0.9);
335
+ if ("startViewTransition" in document && ctx.adaptive?.shouldUseTransitions()) {
336
+ await document.startViewTransition(() => {
337
+ ctx.morpher?.morph(document.body, newContent);
338
+ }).finished;
339
+ } else {
340
+ ctx.morpher?.morph(document.body, newContent);
341
+ }
342
+ const currentHref = window.location.pathname;
343
+ if (replace) {
344
+ window.history.replaceState({ specnav: true }, "", href);
345
+ } else {
346
+ window.history.pushState({ specnav: true }, "", href);
347
+ }
348
+ ctx.graph?.recordNavigation(currentHref, href);
349
+ if (scroll) {
350
+ window.scrollTo(0, 0);
351
+ }
352
+ clearTimeout(timeoutId);
353
+ ctx.setProgress(1);
354
+ setTimeout(() => ctx.setNavigating(null), 200);
355
+ } catch (error) {
356
+ clearTimeout(timeoutId);
357
+ console.error("Navigation failed:", error);
358
+ ctx.setNavigating(null);
359
+ window.location.href = href;
360
+ }
361
+ };
362
+ return /* @__PURE__ */ jsx(
363
+ "a",
364
+ {
365
+ ref: linkRef,
366
+ href,
367
+ onClick: handleClick,
368
+ onMouseEnter: handleMouseEnter,
369
+ onMouseLeave: handleMouseLeave,
370
+ onFocus: handleFocus,
371
+ ...props,
372
+ children
373
+ }
374
+ );
375
+ }
376
+
377
+ // src/useNavigate.ts
378
+ init_prefetch();
379
+ function useNavigate() {
380
+ const ctx = useNavigationContext();
381
+ const isSafeUrl = (url) => {
382
+ try {
383
+ const parsed = new URL(url, window.location.origin);
384
+ return parsed.origin === window.location.origin;
385
+ } catch {
386
+ return false;
387
+ }
388
+ };
389
+ const navigate = async (href, options = {}) => {
390
+ const {
391
+ replace = false,
392
+ scroll = true,
393
+ morph = true,
394
+ cache: useCache = true
395
+ } = options;
396
+ if (!isSafeUrl(href)) {
397
+ window.location.href = href;
398
+ return;
399
+ }
400
+ if (!morph || !ctx.morpher || !ctx.cache) {
401
+ window.location.href = href;
402
+ return;
403
+ }
404
+ ctx.setNavigating(href);
405
+ ctx.setProgress(0.3);
406
+ try {
407
+ let newContent = ctx.speculator?.get(href);
408
+ if (!newContent) {
409
+ let html = useCache ? await ctx.cache.get(href) : null;
410
+ if (!html) {
411
+ ctx.setProgress(0.5);
412
+ if (inflightRequests.has(href)) {
413
+ html = await inflightRequests.get(href);
414
+ } else {
415
+ const controller = new AbortController();
416
+ const fetchPromise = fetch(href, {
417
+ headers: { "x-specnav": "1" },
418
+ signal: controller.signal
419
+ }).then((res) => {
420
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
421
+ return res.text();
422
+ });
423
+ inflightRequests.set(href, fetchPromise);
424
+ html = await fetchPromise;
425
+ inflightRequests.delete(href);
426
+ }
427
+ if (useCache) {
428
+ await ctx.cache.set(href, html);
429
+ }
430
+ }
431
+ ctx.setProgress(0.7);
432
+ const parser = new DOMParser();
433
+ const doc = parser.parseFromString(html, "text/html");
434
+ newContent = doc.body;
435
+ }
436
+ ctx.setProgress(0.9);
437
+ if ("startViewTransition" in document && ctx.adaptive?.shouldUseTransitions()) {
438
+ await document.startViewTransition(() => {
439
+ ctx.morpher?.morph(document.body, newContent);
440
+ }).finished;
441
+ } else {
442
+ ctx.morpher?.morph(document.body, newContent);
443
+ }
444
+ const currentHref = window.location.pathname;
445
+ if (replace) {
446
+ window.history.replaceState({ specnav: true }, "", href);
447
+ } else {
448
+ window.history.pushState({ specnav: true }, "", href);
449
+ }
450
+ ctx.graph?.recordNavigation(currentHref, href);
451
+ if (scroll) {
452
+ window.scrollTo(0, 0);
453
+ }
454
+ ctx.setProgress(1);
455
+ setTimeout(() => ctx.setNavigating(null), 200);
456
+ } catch (error) {
457
+ console.error("Navigation failed:", error);
458
+ ctx.setNavigating(null);
459
+ window.location.href = href;
460
+ }
461
+ };
462
+ const back = () => {
463
+ window.history.back();
464
+ };
465
+ const forward = () => {
466
+ window.history.forward();
467
+ };
468
+ const prefetch = async (href) => {
469
+ if (!ctx.cache || !isSafeUrl(href)) return;
470
+ try {
471
+ if (inflightRequests.has(href)) {
472
+ await inflightRequests.get(href);
473
+ return;
474
+ }
475
+ const controller = new AbortController();
476
+ const fetchPromise = fetch(href, {
477
+ headers: { "x-specnav": "1" },
478
+ signal: controller.signal
479
+ }).then((res) => res.text()).then((html) => {
480
+ ctx.cache?.set(href, html);
481
+ if (ctx.speculator && ctx.adaptive?.shouldSpeculate()) {
482
+ ctx.speculator.speculate(href, html);
483
+ }
484
+ return html;
485
+ }).finally(() => {
486
+ inflightRequests.delete(href);
487
+ });
488
+ inflightRequests.set(href, fetchPromise);
489
+ await fetchPromise;
490
+ } catch (error) {
491
+ if (error.name !== "AbortError") {
492
+ console.warn("Prefetch failed:", error);
493
+ }
494
+ }
495
+ };
496
+ const clearCache = (href) => {
497
+ ctx.cache?.clear(href);
498
+ if (href) {
499
+ ctx.speculator?.cancel(href);
500
+ } else {
501
+ ctx.speculator?.cancelAll();
502
+ }
503
+ };
504
+ return {
505
+ navigate,
506
+ back,
507
+ forward,
508
+ prefetch,
509
+ clearCache,
510
+ isNavigating: ctx.isNavigating,
511
+ pendingHref: ctx.pendingHref
512
+ };
513
+ }
514
+ function useNavigationState() {
515
+ const ctx = useNavigationContext();
516
+ const [previousHref, setPreviousHref] = useState(null);
517
+ const [lastNavigationMs, setLastNavigationMs] = useState(0);
518
+ const [startTime, setStartTime] = useState(null);
519
+ useEffect(() => {
520
+ if (ctx.isNavigating && ctx.pendingHref) {
521
+ setPreviousHref(window.location.pathname);
522
+ setStartTime(Date.now());
523
+ }
524
+ }, [ctx.isNavigating, ctx.pendingHref]);
525
+ useEffect(() => {
526
+ if (!ctx.isNavigating && startTime !== null) {
527
+ setLastNavigationMs(Date.now() - startTime);
528
+ setStartTime(null);
529
+ }
530
+ }, [ctx.isNavigating, startTime]);
531
+ const cacheSize = ctx.cache?.getSize() ?? 0;
532
+ const totalRequests = ctx.cacheHits + ctx.cacheMisses;
533
+ const cacheHitRate = totalRequests > 0 ? ctx.cacheHits / totalRequests : 0;
534
+ return {
535
+ isNavigating: ctx.isNavigating,
536
+ pendingHref: ctx.pendingHref,
537
+ previousHref,
538
+ cacheSize,
539
+ cacheHitRate,
540
+ lastNavigationMs,
541
+ progress: ctx.progress
542
+ };
543
+ }
544
+
545
+ export { Link, NavigateProvider, useNavigate, useNavigationState };
546
+ //# sourceMappingURL=index.js.map
547
+ //# sourceMappingURL=index.js.map