@affectively/aeon-flux 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/README.md +438 -0
- package/examples/basic/aeon.config.ts +39 -0
- package/examples/basic/components/Cursor.tsx +88 -0
- package/examples/basic/components/OfflineIndicator.tsx +93 -0
- package/examples/basic/components/PresenceBar.tsx +68 -0
- package/examples/basic/package.json +20 -0
- package/examples/basic/pages/index.tsx +73 -0
- package/package.json +90 -0
- package/packages/benchmarks/src/benchmark.test.ts +644 -0
- package/packages/cli/package.json +43 -0
- package/packages/cli/src/commands/build.test.ts +649 -0
- package/packages/cli/src/commands/build.ts +853 -0
- package/packages/cli/src/commands/dev.ts +463 -0
- package/packages/cli/src/commands/init.ts +395 -0
- package/packages/cli/src/commands/start.ts +289 -0
- package/packages/cli/src/index.ts +102 -0
- package/packages/directives/src/use-aeon.ts +266 -0
- package/packages/react/package.json +34 -0
- package/packages/react/src/Link.tsx +355 -0
- package/packages/react/src/hooks/useAeonNavigation.ts +204 -0
- package/packages/react/src/hooks/usePilotNavigation.ts +253 -0
- package/packages/react/src/hooks/useServiceWorker.ts +276 -0
- package/packages/react/src/hooks.ts +192 -0
- package/packages/react/src/index.ts +89 -0
- package/packages/react/src/provider.tsx +428 -0
- package/packages/runtime/package.json +70 -0
- package/packages/runtime/schema.sql +40 -0
- package/packages/runtime/src/api-routes.ts +453 -0
- package/packages/runtime/src/benchmark.ts +145 -0
- package/packages/runtime/src/cache.ts +287 -0
- package/packages/runtime/src/durable-object.ts +847 -0
- package/packages/runtime/src/index.ts +235 -0
- package/packages/runtime/src/navigation.test.ts +432 -0
- package/packages/runtime/src/navigation.ts +412 -0
- package/packages/runtime/src/nextjs-adapter.ts +254 -0
- package/packages/runtime/src/predictor.ts +368 -0
- package/packages/runtime/src/registry.ts +339 -0
- package/packages/runtime/src/router/context-extractor.ts +394 -0
- package/packages/runtime/src/router/esi-control-react.tsx +1172 -0
- package/packages/runtime/src/router/esi-control.ts +488 -0
- package/packages/runtime/src/router/esi-react.tsx +600 -0
- package/packages/runtime/src/router/esi.ts +595 -0
- package/packages/runtime/src/router/heuristic-adapter.test.ts +272 -0
- package/packages/runtime/src/router/heuristic-adapter.ts +544 -0
- package/packages/runtime/src/router/index.ts +158 -0
- package/packages/runtime/src/router/speculation.ts +442 -0
- package/packages/runtime/src/router/types.ts +514 -0
- package/packages/runtime/src/router.test.ts +466 -0
- package/packages/runtime/src/router.ts +285 -0
- package/packages/runtime/src/server.ts +446 -0
- package/packages/runtime/src/service-worker.ts +418 -0
- package/packages/runtime/src/speculation.test.ts +360 -0
- package/packages/runtime/src/speculation.ts +456 -0
- package/packages/runtime/src/storage.test.ts +1201 -0
- package/packages/runtime/src/storage.ts +1031 -0
- package/packages/runtime/src/tree-compiler.ts +252 -0
- package/packages/runtime/src/types.ts +444 -0
- package/packages/runtime/src/worker.ts +300 -0
- package/packages/runtime/tsconfig.json +19 -0
- package/packages/runtime/wrangler.toml +41 -0
- package/packages/runtime-wasm/Cargo.lock +436 -0
- package/packages/runtime-wasm/Cargo.toml +29 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +328 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1267 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +73 -0
- package/packages/runtime-wasm/pkg/package.json +21 -0
- package/packages/runtime-wasm/src/hydrate.rs +352 -0
- package/packages/runtime-wasm/src/lib.rs +189 -0
- package/packages/runtime-wasm/src/render.rs +629 -0
- package/packages/runtime-wasm/src/router.rs +298 -0
- package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aeon Link Component
|
|
3
|
+
*
|
|
4
|
+
* Drop-in replacement for <a> with superpowers:
|
|
5
|
+
* - Visibility-based prefetch
|
|
6
|
+
* - Hover prefetch
|
|
7
|
+
* - Intent detection (cursor trajectory)
|
|
8
|
+
* - View transitions
|
|
9
|
+
* - Presence awareness
|
|
10
|
+
* - Total preload support
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React, {
|
|
14
|
+
forwardRef,
|
|
15
|
+
useEffect,
|
|
16
|
+
useRef,
|
|
17
|
+
useCallback,
|
|
18
|
+
useState,
|
|
19
|
+
type ReactNode,
|
|
20
|
+
type MouseEvent,
|
|
21
|
+
type AnchorHTMLAttributes,
|
|
22
|
+
} from 'react';
|
|
23
|
+
import { useAeonNavigation, useRoutePresence } from './hooks/useAeonNavigation';
|
|
24
|
+
|
|
25
|
+
export type TransitionType = 'slide' | 'fade' | 'morph' | 'none';
|
|
26
|
+
export type PrefetchStrategy = 'hover' | 'visible' | 'intent' | 'none';
|
|
27
|
+
|
|
28
|
+
export interface PresenceRenderProps {
|
|
29
|
+
count: number;
|
|
30
|
+
editing: number;
|
|
31
|
+
hot: boolean;
|
|
32
|
+
users?: { userId: string; name?: string }[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> {
|
|
36
|
+
href: string;
|
|
37
|
+
prefetch?: PrefetchStrategy;
|
|
38
|
+
transition?: TransitionType;
|
|
39
|
+
showPresence?: boolean;
|
|
40
|
+
preloadData?: boolean;
|
|
41
|
+
replace?: boolean;
|
|
42
|
+
children?: ReactNode | ((props: { presence: PresenceRenderProps | null }) => ReactNode);
|
|
43
|
+
onNavigateStart?: () => void;
|
|
44
|
+
onNavigateEnd?: () => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
|
|
48
|
+
(
|
|
49
|
+
{
|
|
50
|
+
href,
|
|
51
|
+
prefetch = 'visible',
|
|
52
|
+
transition = 'fade',
|
|
53
|
+
showPresence = false,
|
|
54
|
+
preloadData = true,
|
|
55
|
+
replace = false,
|
|
56
|
+
children,
|
|
57
|
+
onNavigateStart,
|
|
58
|
+
onNavigateEnd,
|
|
59
|
+
onClick,
|
|
60
|
+
onMouseEnter,
|
|
61
|
+
onMouseMove,
|
|
62
|
+
className,
|
|
63
|
+
...props
|
|
64
|
+
},
|
|
65
|
+
ref
|
|
66
|
+
) => {
|
|
67
|
+
const internalRef = useRef<HTMLAnchorElement>(null);
|
|
68
|
+
const linkRef = (ref as React.RefObject<HTMLAnchorElement>) ?? internalRef;
|
|
69
|
+
const trajectoryRef = useRef<{ x: number; y: number; time: number }[]>([]);
|
|
70
|
+
const intentTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
71
|
+
|
|
72
|
+
const {
|
|
73
|
+
navigate,
|
|
74
|
+
prefetch: doPrefetch,
|
|
75
|
+
isPreloaded,
|
|
76
|
+
isNavigating,
|
|
77
|
+
} = useAeonNavigation();
|
|
78
|
+
const { getPresence, subscribePresence } = useRoutePresence();
|
|
79
|
+
|
|
80
|
+
const [presence, setPresence] = useState<PresenceRenderProps | null>(null);
|
|
81
|
+
const [isPrefetched, setIsPrefetched] = useState(false);
|
|
82
|
+
|
|
83
|
+
// Check initial prefetch state
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
setIsPrefetched(isPreloaded(href));
|
|
86
|
+
}, [href, isPreloaded]);
|
|
87
|
+
|
|
88
|
+
// Visibility-based prefetch
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (prefetch !== 'visible' || typeof IntersectionObserver === 'undefined') {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const observer = new IntersectionObserver(
|
|
95
|
+
([entry]) => {
|
|
96
|
+
if (entry.isIntersecting) {
|
|
97
|
+
doPrefetch(href, { data: preloadData, presence: showPresence });
|
|
98
|
+
setIsPrefetched(true);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{ rootMargin: '100px' }
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const element = linkRef.current;
|
|
105
|
+
if (element) observer.observe(element);
|
|
106
|
+
|
|
107
|
+
return () => observer.disconnect();
|
|
108
|
+
}, [href, prefetch, preloadData, showPresence, doPrefetch, linkRef]);
|
|
109
|
+
|
|
110
|
+
// Presence subscription
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!showPresence) return;
|
|
113
|
+
|
|
114
|
+
// Get initial presence
|
|
115
|
+
const initialPresence = getPresence(href);
|
|
116
|
+
if (initialPresence) {
|
|
117
|
+
setPresence(initialPresence);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Subscribe to updates
|
|
121
|
+
const unsubscribe = subscribePresence((route, info) => {
|
|
122
|
+
if (route === href) {
|
|
123
|
+
setPresence(info);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return unsubscribe;
|
|
128
|
+
}, [href, showPresence, getPresence, subscribePresence]);
|
|
129
|
+
|
|
130
|
+
// Hover prefetch handler
|
|
131
|
+
const handleMouseEnter = useCallback(
|
|
132
|
+
(e: MouseEvent<HTMLAnchorElement>) => {
|
|
133
|
+
onMouseEnter?.(e);
|
|
134
|
+
|
|
135
|
+
if (prefetch === 'hover' || prefetch === 'intent') {
|
|
136
|
+
doPrefetch(href, { data: preloadData, presence: showPresence });
|
|
137
|
+
setIsPrefetched(true);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
[href, prefetch, preloadData, showPresence, doPrefetch, onMouseEnter]
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Intent detection (cursor trajectory prediction)
|
|
144
|
+
const handleMouseMove = useCallback(
|
|
145
|
+
(e: MouseEvent<HTMLAnchorElement>) => {
|
|
146
|
+
onMouseMove?.(e);
|
|
147
|
+
|
|
148
|
+
if (prefetch !== 'intent') return;
|
|
149
|
+
|
|
150
|
+
// Track cursor trajectory
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
trajectoryRef.current.push({ x: e.clientX, y: e.clientY, time: now });
|
|
153
|
+
|
|
154
|
+
// Keep only last 5 points
|
|
155
|
+
if (trajectoryRef.current.length > 5) {
|
|
156
|
+
trajectoryRef.current.shift();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Clear previous timeout
|
|
160
|
+
if (intentTimeoutRef.current) {
|
|
161
|
+
clearTimeout(intentTimeoutRef.current);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Predict intent after short delay
|
|
165
|
+
intentTimeoutRef.current = setTimeout(() => {
|
|
166
|
+
const points = trajectoryRef.current;
|
|
167
|
+
if (points.length < 2) return;
|
|
168
|
+
|
|
169
|
+
const element = linkRef.current;
|
|
170
|
+
if (!element) return;
|
|
171
|
+
|
|
172
|
+
// Calculate if cursor is approaching the link
|
|
173
|
+
const rect = element.getBoundingClientRect();
|
|
174
|
+
const centerX = rect.left + rect.width / 2;
|
|
175
|
+
const centerY = rect.top + rect.height / 2;
|
|
176
|
+
|
|
177
|
+
const lastPoint = points[points.length - 1];
|
|
178
|
+
const prevPoint = points[points.length - 2];
|
|
179
|
+
|
|
180
|
+
const velocityX = lastPoint.x - prevPoint.x;
|
|
181
|
+
const velocityY = lastPoint.y - prevPoint.y;
|
|
182
|
+
|
|
183
|
+
// Project cursor position
|
|
184
|
+
const projectedX = lastPoint.x + velocityX * 10;
|
|
185
|
+
const projectedY = lastPoint.y + velocityY * 10;
|
|
186
|
+
|
|
187
|
+
// Check if projected position is closer to link
|
|
188
|
+
const currentDist = Math.hypot(lastPoint.x - centerX, lastPoint.y - centerY);
|
|
189
|
+
const projectedDist = Math.hypot(projectedX - centerX, projectedY - centerY);
|
|
190
|
+
|
|
191
|
+
if (projectedDist < currentDist) {
|
|
192
|
+
// Cursor is approaching - prefetch with high priority
|
|
193
|
+
doPrefetch(href, {
|
|
194
|
+
data: preloadData,
|
|
195
|
+
presence: showPresence,
|
|
196
|
+
priority: 'high',
|
|
197
|
+
});
|
|
198
|
+
setIsPrefetched(true);
|
|
199
|
+
}
|
|
200
|
+
}, 50);
|
|
201
|
+
},
|
|
202
|
+
[href, prefetch, preloadData, showPresence, doPrefetch, onMouseMove, linkRef]
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Click navigation with view transition
|
|
206
|
+
const handleClick = useCallback(
|
|
207
|
+
async (e: MouseEvent<HTMLAnchorElement>) => {
|
|
208
|
+
// Call original onClick if provided
|
|
209
|
+
onClick?.(e);
|
|
210
|
+
|
|
211
|
+
// Don't handle if default prevented or modified
|
|
212
|
+
if (
|
|
213
|
+
e.defaultPrevented ||
|
|
214
|
+
e.metaKey ||
|
|
215
|
+
e.ctrlKey ||
|
|
216
|
+
e.shiftKey ||
|
|
217
|
+
e.altKey ||
|
|
218
|
+
e.button !== 0
|
|
219
|
+
) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
|
|
225
|
+
onNavigateStart?.();
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
await navigate(href, { transition, replace });
|
|
229
|
+
} finally {
|
|
230
|
+
onNavigateEnd?.();
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
[href, transition, replace, navigate, onClick, onNavigateStart, onNavigateEnd]
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// Cleanup
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
return () => {
|
|
239
|
+
if (intentTimeoutRef.current) {
|
|
240
|
+
clearTimeout(intentTimeoutRef.current);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}, []);
|
|
244
|
+
|
|
245
|
+
// Render children
|
|
246
|
+
const renderChildren = () => {
|
|
247
|
+
if (typeof children === 'function') {
|
|
248
|
+
return children({ presence });
|
|
249
|
+
}
|
|
250
|
+
return (
|
|
251
|
+
<>
|
|
252
|
+
{children}
|
|
253
|
+
{showPresence && presence && presence.count > 0 && (
|
|
254
|
+
<span className="aeon-presence-badge" aria-label={`${presence.count} active`}>
|
|
255
|
+
{presence.hot ? '\uD83D\uDD25' : '\uD83D\uDC65'} {presence.count}
|
|
256
|
+
{presence.editing > 0 && ` (${presence.editing} editing)`}
|
|
257
|
+
</span>
|
|
258
|
+
)}
|
|
259
|
+
</>
|
|
260
|
+
);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<a
|
|
265
|
+
ref={linkRef}
|
|
266
|
+
href={href}
|
|
267
|
+
onClick={handleClick}
|
|
268
|
+
onMouseEnter={handleMouseEnter}
|
|
269
|
+
onMouseMove={handleMouseMove}
|
|
270
|
+
className={className}
|
|
271
|
+
data-preloaded={isPrefetched ? '' : undefined}
|
|
272
|
+
data-navigating={isNavigating ? '' : undefined}
|
|
273
|
+
data-transition={transition}
|
|
274
|
+
aria-busy={isNavigating}
|
|
275
|
+
{...props}
|
|
276
|
+
>
|
|
277
|
+
{renderChildren()}
|
|
278
|
+
</a>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
Link.displayName = 'Link';
|
|
284
|
+
|
|
285
|
+
// CSS for presence badge (can be overridden)
|
|
286
|
+
if (typeof document !== 'undefined') {
|
|
287
|
+
const style = document.createElement('style');
|
|
288
|
+
style.textContent = `
|
|
289
|
+
.aeon-presence-badge {
|
|
290
|
+
display: inline-flex;
|
|
291
|
+
align-items: center;
|
|
292
|
+
gap: 0.25rem;
|
|
293
|
+
font-size: 0.75rem;
|
|
294
|
+
padding: 0.125rem 0.375rem;
|
|
295
|
+
margin-left: 0.5rem;
|
|
296
|
+
background: rgba(0, 0, 0, 0.05);
|
|
297
|
+
border-radius: 9999px;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
[data-preloaded]::after {
|
|
301
|
+
content: '';
|
|
302
|
+
display: inline-block;
|
|
303
|
+
width: 4px;
|
|
304
|
+
height: 4px;
|
|
305
|
+
margin-left: 0.25rem;
|
|
306
|
+
background: #10b981;
|
|
307
|
+
border-radius: 50%;
|
|
308
|
+
opacity: 0.5;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* View transition styles */
|
|
312
|
+
::view-transition-old(aeon-page) {
|
|
313
|
+
animation: aeon-fade-out 200ms ease-out;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
::view-transition-new(aeon-page) {
|
|
317
|
+
animation: aeon-fade-in 300ms ease-out;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
@keyframes aeon-fade-out {
|
|
321
|
+
from { opacity: 1; }
|
|
322
|
+
to { opacity: 0; }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
@keyframes aeon-fade-in {
|
|
326
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
327
|
+
to { opacity: 1; transform: translateY(0); }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* Slide transition */
|
|
331
|
+
[data-transition="slide"]::view-transition-old(aeon-page) {
|
|
332
|
+
animation: aeon-slide-out 200ms ease-out;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
[data-transition="slide"]::view-transition-new(aeon-page) {
|
|
336
|
+
animation: aeon-slide-in 300ms ease-out;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
@keyframes aeon-slide-out {
|
|
340
|
+
from { transform: translateX(0); opacity: 1; }
|
|
341
|
+
to { transform: translateX(-20px); opacity: 0; }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
@keyframes aeon-slide-in {
|
|
345
|
+
from { transform: translateX(20px); opacity: 0; }
|
|
346
|
+
to { transform: translateX(0); opacity: 1; }
|
|
347
|
+
}
|
|
348
|
+
`;
|
|
349
|
+
|
|
350
|
+
// Only inject once
|
|
351
|
+
if (!document.getElementById('aeon-link-styles')) {
|
|
352
|
+
style.id = 'aeon-link-styles';
|
|
353
|
+
document.head.appendChild(style);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aeon Navigation Hooks
|
|
3
|
+
*
|
|
4
|
+
* React hooks for the cutting-edge navigation system.
|
|
5
|
+
* The navigation state itself is an Aeon - the site is a session.
|
|
6
|
+
*
|
|
7
|
+
* Recursive Aeon Architecture:
|
|
8
|
+
* - Component = Aeon entity
|
|
9
|
+
* - Page = Aeon session
|
|
10
|
+
* - Site = Aeon of sessions (routes are collaborative)
|
|
11
|
+
* - Federation = Aeon of Aeons (cross-site sync)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useContext, useCallback, useSyncExternalStore, createContext } from 'react';
|
|
15
|
+
import type {
|
|
16
|
+
AeonNavigationEngine,
|
|
17
|
+
NavigationOptions,
|
|
18
|
+
PrefetchOptions,
|
|
19
|
+
NavigationState,
|
|
20
|
+
PresenceInfo,
|
|
21
|
+
} from '../../runtime/src/navigation';
|
|
22
|
+
import { getNavigator } from '../../runtime/src/navigation';
|
|
23
|
+
|
|
24
|
+
// Context for providing custom navigation engine
|
|
25
|
+
export interface AeonNavigationContextValue {
|
|
26
|
+
navigator: AeonNavigationEngine;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const AeonNavigationContext = createContext<AeonNavigationContextValue | null>(null);
|
|
30
|
+
|
|
31
|
+
// Get navigator from context or use global singleton
|
|
32
|
+
function useNavigator(): AeonNavigationEngine {
|
|
33
|
+
const context = useContext(AeonNavigationContext);
|
|
34
|
+
return context?.navigator ?? getNavigator();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Main navigation hook - provides navigation, prefetch, and state
|
|
39
|
+
*/
|
|
40
|
+
export function useAeonNavigation() {
|
|
41
|
+
const navigator = useNavigator();
|
|
42
|
+
|
|
43
|
+
// Subscribe to navigation state changes with useSyncExternalStore
|
|
44
|
+
const state = useSyncExternalStore(
|
|
45
|
+
useCallback((callback) => navigator.subscribe(callback), [navigator]),
|
|
46
|
+
() => navigator.getState(),
|
|
47
|
+
() => navigator.getState()
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Navigation function with view transitions
|
|
51
|
+
const navigate = useCallback(
|
|
52
|
+
async (href: string, options?: NavigationOptions) => {
|
|
53
|
+
await navigator.navigate(href, options);
|
|
54
|
+
},
|
|
55
|
+
[navigator]
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Prefetch a route (session + presence)
|
|
59
|
+
const prefetch = useCallback(
|
|
60
|
+
async (href: string, options?: PrefetchOptions) => {
|
|
61
|
+
await navigator.prefetch(href, options);
|
|
62
|
+
},
|
|
63
|
+
[navigator]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Go back in history
|
|
67
|
+
const back = useCallback(async () => {
|
|
68
|
+
await navigator.back();
|
|
69
|
+
}, [navigator]);
|
|
70
|
+
|
|
71
|
+
// Check if route is preloaded
|
|
72
|
+
const isPreloaded = useCallback(
|
|
73
|
+
(href: string): boolean => {
|
|
74
|
+
return navigator.isPreloaded(href);
|
|
75
|
+
},
|
|
76
|
+
[navigator]
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Preload ALL routes (total preload strategy)
|
|
80
|
+
const preloadAll = useCallback(
|
|
81
|
+
async (onProgress?: (loaded: number, total: number) => void) => {
|
|
82
|
+
await navigator.preloadAll(onProgress);
|
|
83
|
+
},
|
|
84
|
+
[navigator]
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Get cache statistics
|
|
88
|
+
const getCacheStats = useCallback(() => {
|
|
89
|
+
return navigator.getCacheStats();
|
|
90
|
+
}, [navigator]);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
// State
|
|
94
|
+
current: state.current,
|
|
95
|
+
previous: state.previous,
|
|
96
|
+
history: state.history,
|
|
97
|
+
isNavigating: state.isNavigating,
|
|
98
|
+
|
|
99
|
+
// Actions
|
|
100
|
+
navigate,
|
|
101
|
+
prefetch,
|
|
102
|
+
back,
|
|
103
|
+
preloadAll,
|
|
104
|
+
|
|
105
|
+
// Utilities
|
|
106
|
+
isPreloaded,
|
|
107
|
+
getCacheStats,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Route Presence hook - subscribe to who's viewing/editing routes
|
|
113
|
+
*
|
|
114
|
+
* Presence flows upward through the Aeon hierarchy:
|
|
115
|
+
* - Page presence = users on this page
|
|
116
|
+
* - Site presence = aggregate of all page presence
|
|
117
|
+
* - Federation presence = aggregate across sites
|
|
118
|
+
*
|
|
119
|
+
* Note: This is different from usePresence in provider.tsx which is for
|
|
120
|
+
* page-level editing presence. This hook is for navigation-level presence
|
|
121
|
+
* (who's viewing what routes before you navigate there).
|
|
122
|
+
*/
|
|
123
|
+
export function useRoutePresence() {
|
|
124
|
+
const navigator = useNavigator();
|
|
125
|
+
|
|
126
|
+
// Get cached presence for a route
|
|
127
|
+
const getPresence = useCallback(
|
|
128
|
+
(route: string): PresenceInfo | null => {
|
|
129
|
+
return navigator.getPresence(route);
|
|
130
|
+
},
|
|
131
|
+
[navigator]
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Subscribe to presence updates
|
|
135
|
+
const subscribePresence = useCallback(
|
|
136
|
+
(callback: (route: string, presence: PresenceInfo) => void): (() => void) => {
|
|
137
|
+
return navigator.subscribePresence(callback);
|
|
138
|
+
},
|
|
139
|
+
[navigator]
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
getPresence,
|
|
144
|
+
subscribePresence,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Navigation prediction hook
|
|
150
|
+
*/
|
|
151
|
+
export function useNavigationPrediction() {
|
|
152
|
+
const navigator = useNavigator();
|
|
153
|
+
|
|
154
|
+
// Get predictions for current route
|
|
155
|
+
const predict = useCallback(
|
|
156
|
+
(fromRoute?: string) => {
|
|
157
|
+
const state = navigator.getState();
|
|
158
|
+
return navigator.predict(fromRoute ?? state.current);
|
|
159
|
+
},
|
|
160
|
+
[navigator]
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
predict,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Hook for observing links and auto-prefetching
|
|
170
|
+
*/
|
|
171
|
+
export function useLinkObserver(containerRef: React.RefObject<Element>) {
|
|
172
|
+
const navigator = useNavigator();
|
|
173
|
+
|
|
174
|
+
// Set up observation on mount
|
|
175
|
+
const observe = useCallback(() => {
|
|
176
|
+
if (!containerRef.current) return () => {};
|
|
177
|
+
return navigator.observeLinks(containerRef.current);
|
|
178
|
+
}, [navigator, containerRef]);
|
|
179
|
+
|
|
180
|
+
return { observe };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Hook for total preload progress
|
|
185
|
+
*/
|
|
186
|
+
export function useTotalPreload() {
|
|
187
|
+
const { preloadAll, getCacheStats } = useAeonNavigation();
|
|
188
|
+
|
|
189
|
+
// Preload with progress tracking
|
|
190
|
+
const startPreload = useCallback(
|
|
191
|
+
async (onProgress?: (loaded: number, total: number) => void) => {
|
|
192
|
+
await preloadAll(onProgress);
|
|
193
|
+
},
|
|
194
|
+
[preloadAll]
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
startPreload,
|
|
199
|
+
getStats: getCacheStats,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Re-export types for convenience
|
|
204
|
+
export type { NavigationOptions, PrefetchOptions, NavigationState, PresenceInfo };
|