@affectively/aeon-pages 1.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.
Files changed (124) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +625 -0
  3. package/examples/basic/aeon.config.ts +39 -0
  4. package/examples/basic/components/Cursor.tsx +86 -0
  5. package/examples/basic/components/OfflineIndicator.tsx +103 -0
  6. package/examples/basic/components/PresenceBar.tsx +77 -0
  7. package/examples/basic/package.json +20 -0
  8. package/examples/basic/pages/index.tsx +80 -0
  9. package/package.json +101 -0
  10. package/packages/analytics/README.md +309 -0
  11. package/packages/analytics/build.ts +35 -0
  12. package/packages/analytics/package.json +50 -0
  13. package/packages/analytics/src/click-tracker.ts +368 -0
  14. package/packages/analytics/src/context-bridge.ts +319 -0
  15. package/packages/analytics/src/data-layer.ts +302 -0
  16. package/packages/analytics/src/gtm-loader.ts +239 -0
  17. package/packages/analytics/src/index.ts +230 -0
  18. package/packages/analytics/src/merkle-tree.ts +489 -0
  19. package/packages/analytics/src/provider.tsx +300 -0
  20. package/packages/analytics/src/types.ts +320 -0
  21. package/packages/analytics/src/use-analytics.ts +296 -0
  22. package/packages/analytics/tsconfig.json +19 -0
  23. package/packages/benchmarks/src/benchmark.test.ts +691 -0
  24. package/packages/cli/dist/index.js +61899 -0
  25. package/packages/cli/package.json +43 -0
  26. package/packages/cli/src/commands/build.test.ts +682 -0
  27. package/packages/cli/src/commands/build.ts +890 -0
  28. package/packages/cli/src/commands/dev.ts +473 -0
  29. package/packages/cli/src/commands/init.ts +409 -0
  30. package/packages/cli/src/commands/start.ts +297 -0
  31. package/packages/cli/src/index.ts +105 -0
  32. package/packages/directives/src/use-aeon.ts +272 -0
  33. package/packages/mcp-server/package.json +51 -0
  34. package/packages/mcp-server/src/index.ts +178 -0
  35. package/packages/mcp-server/src/resources.ts +346 -0
  36. package/packages/mcp-server/src/tools/index.ts +36 -0
  37. package/packages/mcp-server/src/tools/navigation.ts +545 -0
  38. package/packages/mcp-server/tsconfig.json +21 -0
  39. package/packages/react/package.json +40 -0
  40. package/packages/react/src/Link.tsx +388 -0
  41. package/packages/react/src/components/InstallPrompt.tsx +286 -0
  42. package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
  43. package/packages/react/src/components/PushNotifications.tsx +453 -0
  44. package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
  45. package/packages/react/src/hooks/useConflicts.ts +277 -0
  46. package/packages/react/src/hooks/useNetworkState.ts +209 -0
  47. package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
  48. package/packages/react/src/hooks/useServiceWorker.ts +278 -0
  49. package/packages/react/src/hooks.ts +195 -0
  50. package/packages/react/src/index.ts +151 -0
  51. package/packages/react/src/provider.tsx +467 -0
  52. package/packages/react/tsconfig.json +19 -0
  53. package/packages/runtime/README.md +399 -0
  54. package/packages/runtime/build.ts +48 -0
  55. package/packages/runtime/package.json +71 -0
  56. package/packages/runtime/schema.sql +40 -0
  57. package/packages/runtime/src/api-routes.ts +465 -0
  58. package/packages/runtime/src/benchmark.ts +171 -0
  59. package/packages/runtime/src/cache.ts +479 -0
  60. package/packages/runtime/src/durable-object.ts +1341 -0
  61. package/packages/runtime/src/index.ts +360 -0
  62. package/packages/runtime/src/navigation.test.ts +421 -0
  63. package/packages/runtime/src/navigation.ts +422 -0
  64. package/packages/runtime/src/nextjs-adapter.ts +272 -0
  65. package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
  66. package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
  67. package/packages/runtime/src/offline/encryption.test.ts +412 -0
  68. package/packages/runtime/src/offline/encryption.ts +397 -0
  69. package/packages/runtime/src/offline/types.ts +465 -0
  70. package/packages/runtime/src/predictor.ts +371 -0
  71. package/packages/runtime/src/registry.ts +351 -0
  72. package/packages/runtime/src/router/context-extractor.ts +661 -0
  73. package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
  74. package/packages/runtime/src/router/esi-control.ts +541 -0
  75. package/packages/runtime/src/router/esi-cyrano.ts +779 -0
  76. package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
  77. package/packages/runtime/src/router/esi-react.tsx +1065 -0
  78. package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
  79. package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
  80. package/packages/runtime/src/router/esi-translate.ts +503 -0
  81. package/packages/runtime/src/router/esi.ts +666 -0
  82. package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
  83. package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
  84. package/packages/runtime/src/router/index.ts +298 -0
  85. package/packages/runtime/src/router/merkle-capability.ts +473 -0
  86. package/packages/runtime/src/router/speculation.ts +451 -0
  87. package/packages/runtime/src/router/types.ts +630 -0
  88. package/packages/runtime/src/router.test.ts +470 -0
  89. package/packages/runtime/src/router.ts +302 -0
  90. package/packages/runtime/src/server.ts +481 -0
  91. package/packages/runtime/src/service-worker-push.ts +319 -0
  92. package/packages/runtime/src/service-worker.ts +553 -0
  93. package/packages/runtime/src/skeleton-hydrate.ts +237 -0
  94. package/packages/runtime/src/speculation.test.ts +389 -0
  95. package/packages/runtime/src/speculation.ts +486 -0
  96. package/packages/runtime/src/storage.test.ts +1297 -0
  97. package/packages/runtime/src/storage.ts +1048 -0
  98. package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
  99. package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
  100. package/packages/runtime/src/sync/coordinator.test.ts +608 -0
  101. package/packages/runtime/src/sync/coordinator.ts +596 -0
  102. package/packages/runtime/src/tree-compiler.ts +295 -0
  103. package/packages/runtime/src/types.ts +728 -0
  104. package/packages/runtime/src/worker.ts +327 -0
  105. package/packages/runtime/tsconfig.json +20 -0
  106. package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
  107. package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
  108. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
  109. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
  110. package/packages/runtime/wasm/package.json +21 -0
  111. package/packages/runtime/wrangler.toml +41 -0
  112. package/packages/runtime-wasm/Cargo.lock +436 -0
  113. package/packages/runtime-wasm/Cargo.toml +29 -0
  114. package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
  115. package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
  116. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
  117. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
  118. package/packages/runtime-wasm/pkg/package.json +21 -0
  119. package/packages/runtime-wasm/src/hydrate.rs +352 -0
  120. package/packages/runtime-wasm/src/lib.rs +191 -0
  121. package/packages/runtime-wasm/src/render.rs +629 -0
  122. package/packages/runtime-wasm/src/router.rs +298 -0
  123. package/packages/runtime-wasm/src/skeleton.rs +430 -0
  124. package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
@@ -0,0 +1,453 @@
1
+ /**
2
+ * PushNotifications Component
3
+ *
4
+ * Push notification management component for PWA applications.
5
+ * Handles subscription, permission, and notification sending.
6
+ *
7
+ * Features:
8
+ * - VAPID-based push subscription
9
+ * - Permission handling
10
+ * - Subscription serialization
11
+ * - Customizable UI via render props
12
+ * - Headless hook export
13
+ */
14
+
15
+ 'use client';
16
+
17
+ import { useState, useEffect, useCallback, type ReactNode } from 'react';
18
+
19
+ // ============================================================================
20
+ // Types
21
+ // ============================================================================
22
+
23
+ export interface PushSubscriptionData {
24
+ endpoint: string;
25
+ keys: {
26
+ p256dh: string;
27
+ auth: string;
28
+ };
29
+ }
30
+
31
+ export interface PushNotificationState {
32
+ /** Whether push is supported */
33
+ isSupported: boolean;
34
+ /** Current permission state */
35
+ permission: NotificationPermission | 'unsupported';
36
+ /** Current subscription (if subscribed) */
37
+ subscription: PushSubscriptionData | null;
38
+ /** Whether currently loading */
39
+ isLoading: boolean;
40
+ /** Last error message */
41
+ error: string | null;
42
+ /** Subscribe to push notifications */
43
+ subscribe: () => Promise<PushSubscriptionData | null>;
44
+ /** Unsubscribe from push notifications */
45
+ unsubscribe: () => Promise<boolean>;
46
+ /** Request permission */
47
+ requestPermission: () => Promise<NotificationPermission>;
48
+ /** Clear error */
49
+ clearError: () => void;
50
+ }
51
+
52
+ export interface PushNotificationsProps {
53
+ /** VAPID public key */
54
+ vapidPublicKey?: string;
55
+ /** Called when subscription changes */
56
+ onSubscribe?: (subscription: PushSubscriptionData) => Promise<void> | void;
57
+ /** Called when unsubscribing */
58
+ onUnsubscribe?: (endpoint: string) => Promise<void> | void;
59
+ /** Custom render function */
60
+ render?: (state: PushNotificationState) => ReactNode;
61
+ /** Show default UI */
62
+ showUI?: boolean;
63
+ /** CSS class for container */
64
+ className?: string;
65
+ }
66
+
67
+ // ============================================================================
68
+ // Utility Functions
69
+ // ============================================================================
70
+
71
+ /**
72
+ * Convert VAPID key from base64url to Uint8Array
73
+ */
74
+ function urlBase64ToUint8Array(base64String: string): Uint8Array {
75
+ const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
76
+ const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
77
+
78
+ const rawData = atob(base64);
79
+ const outputArray = new Uint8Array(rawData.length);
80
+
81
+ for (let i = 0; i < rawData.length; ++i) {
82
+ outputArray[i] = rawData.charCodeAt(i);
83
+ }
84
+ return outputArray;
85
+ }
86
+
87
+ /**
88
+ * Serialize PushSubscription for server
89
+ */
90
+ function serializeSubscription(sub: PushSubscription): PushSubscriptionData {
91
+ const p256dh = sub.getKey('p256dh');
92
+ const auth = sub.getKey('auth');
93
+
94
+ return {
95
+ endpoint: sub.endpoint,
96
+ keys: {
97
+ p256dh: p256dh
98
+ ? btoa(String.fromCharCode(...new Uint8Array(p256dh)))
99
+ : '',
100
+ auth: auth ? btoa(String.fromCharCode(...new Uint8Array(auth))) : '',
101
+ },
102
+ };
103
+ }
104
+
105
+ // ============================================================================
106
+ // usePushNotifications Hook
107
+ // ============================================================================
108
+
109
+ export interface UsePushNotificationsConfig {
110
+ /** VAPID public key */
111
+ vapidPublicKey?: string;
112
+ /** Service worker URL */
113
+ serviceWorkerUrl?: string;
114
+ }
115
+
116
+ /**
117
+ * Hook for managing push notifications
118
+ */
119
+ export function usePushNotifications(
120
+ config: UsePushNotificationsConfig = {},
121
+ ): PushNotificationState {
122
+ const [isSupported, setIsSupported] = useState(false);
123
+ const [permission, setPermission] = useState<
124
+ NotificationPermission | 'unsupported'
125
+ >('unsupported');
126
+ const [subscription, setSubscription] = useState<PushSubscriptionData | null>(
127
+ null,
128
+ );
129
+ const [isLoading, setIsLoading] = useState(false);
130
+ const [error, setError] = useState<string | null>(null);
131
+
132
+ const { vapidPublicKey, serviceWorkerUrl = '/sw.js' } = config;
133
+
134
+ // Check support and load existing subscription
135
+ useEffect(() => {
136
+ if (typeof window === 'undefined') return;
137
+
138
+ const supported = 'serviceWorker' in navigator && 'PushManager' in window;
139
+ setIsSupported(supported);
140
+
141
+ if (!supported) {
142
+ setPermission('unsupported');
143
+ return;
144
+ }
145
+
146
+ // Check current permission
147
+ setPermission(Notification.permission);
148
+
149
+ // Load existing subscription
150
+ navigator.serviceWorker.ready.then(async (registration) => {
151
+ try {
152
+ const existingSub = await registration.pushManager.getSubscription();
153
+ if (existingSub) {
154
+ setSubscription(serializeSubscription(existingSub));
155
+ }
156
+ } catch (err) {
157
+ console.error('Error loading push subscription:', err);
158
+ }
159
+ });
160
+ }, []);
161
+
162
+ const requestPermission =
163
+ useCallback(async (): Promise<NotificationPermission> => {
164
+ if (!isSupported) {
165
+ return 'denied';
166
+ }
167
+
168
+ const result = await Notification.requestPermission();
169
+ setPermission(result);
170
+ return result;
171
+ }, [isSupported]);
172
+
173
+ const subscribe =
174
+ useCallback(async (): Promise<PushSubscriptionData | null> => {
175
+ if (!isSupported) {
176
+ setError('Push notifications are not supported');
177
+ return null;
178
+ }
179
+
180
+ if (!vapidPublicKey) {
181
+ setError('VAPID public key is required');
182
+ return null;
183
+ }
184
+
185
+ setIsLoading(true);
186
+ setError(null);
187
+
188
+ try {
189
+ // Ensure service worker is registered
190
+ let registration: ServiceWorkerRegistration;
191
+
192
+ try {
193
+ registration = await navigator.serviceWorker.ready;
194
+ } catch {
195
+ // Try to register if not ready
196
+ registration = await navigator.serviceWorker.register(
197
+ serviceWorkerUrl,
198
+ {
199
+ scope: '/',
200
+ },
201
+ );
202
+ }
203
+
204
+ // Request permission if needed
205
+ if (Notification.permission === 'default') {
206
+ const perm = await Notification.requestPermission();
207
+ setPermission(perm);
208
+
209
+ if (perm !== 'granted') {
210
+ throw new Error('Notification permission denied');
211
+ }
212
+ } else if (Notification.permission !== 'granted') {
213
+ throw new Error('Notification permission not granted');
214
+ }
215
+
216
+ // Subscribe to push
217
+ const sub = await registration.pushManager.subscribe({
218
+ userVisibleOnly: true,
219
+ applicationServerKey: urlBase64ToUint8Array(
220
+ vapidPublicKey,
221
+ ) as BufferSource,
222
+ });
223
+
224
+ const serialized = serializeSubscription(sub);
225
+ setSubscription(serialized);
226
+
227
+ return serialized;
228
+ } catch (err) {
229
+ const message =
230
+ err instanceof Error ? err.message : 'Failed to subscribe';
231
+ setError(message);
232
+ return null;
233
+ } finally {
234
+ setIsLoading(false);
235
+ }
236
+ }, [isSupported, vapidPublicKey, serviceWorkerUrl]);
237
+
238
+ const unsubscribe = useCallback(async (): Promise<boolean> => {
239
+ if (!isSupported) {
240
+ return false;
241
+ }
242
+
243
+ setIsLoading(true);
244
+ setError(null);
245
+
246
+ try {
247
+ const registration = await navigator.serviceWorker.ready;
248
+ const sub = await registration.pushManager.getSubscription();
249
+
250
+ if (sub) {
251
+ await sub.unsubscribe();
252
+ }
253
+
254
+ setSubscription(null);
255
+ return true;
256
+ } catch (err) {
257
+ const message =
258
+ err instanceof Error ? err.message : 'Failed to unsubscribe';
259
+ setError(message);
260
+ return false;
261
+ } finally {
262
+ setIsLoading(false);
263
+ }
264
+ }, [isSupported]);
265
+
266
+ const clearError = useCallback(() => {
267
+ setError(null);
268
+ }, []);
269
+
270
+ return {
271
+ isSupported,
272
+ permission,
273
+ subscription,
274
+ isLoading,
275
+ error,
276
+ subscribe,
277
+ unsubscribe,
278
+ requestPermission,
279
+ clearError,
280
+ };
281
+ }
282
+
283
+ // ============================================================================
284
+ // PushNotifications Component
285
+ // ============================================================================
286
+
287
+ /**
288
+ * Push notifications management component
289
+ */
290
+ export function PushNotifications({
291
+ vapidPublicKey,
292
+ onSubscribe,
293
+ onUnsubscribe,
294
+ render,
295
+ showUI = true,
296
+ className,
297
+ }: PushNotificationsProps): ReactNode {
298
+ const state = usePushNotifications({ vapidPublicKey });
299
+
300
+ const handleSubscribe = async () => {
301
+ const sub = await state.subscribe();
302
+ if (sub && onSubscribe) {
303
+ await onSubscribe(sub);
304
+ }
305
+ };
306
+
307
+ const handleUnsubscribe = async () => {
308
+ const endpoint = state.subscription?.endpoint;
309
+ const success = await state.unsubscribe();
310
+ if (success && endpoint && onUnsubscribe) {
311
+ await onUnsubscribe(endpoint);
312
+ }
313
+ };
314
+
315
+ // Custom render
316
+ if (render) {
317
+ return render(state);
318
+ }
319
+
320
+ // Don't show UI if not requested
321
+ if (!showUI) {
322
+ return null;
323
+ }
324
+
325
+ // Not supported message
326
+ if (!state.isSupported) {
327
+ return (
328
+ <div className={className} role="region" aria-label="Push notifications">
329
+ <p style={{ color: '#6b7280', fontSize: '0.875rem' }}>
330
+ Push notifications are not supported in this browser.
331
+ </p>
332
+ </div>
333
+ );
334
+ }
335
+
336
+ return (
337
+ <div className={className} role="region" aria-label="Push notifications">
338
+ <h3
339
+ style={{
340
+ fontSize: '1.125rem',
341
+ fontWeight: 600,
342
+ marginBottom: '0.5rem',
343
+ }}
344
+ >
345
+ Push Notifications
346
+ </h3>
347
+
348
+ {state.error && (
349
+ <div
350
+ style={{
351
+ padding: '0.75rem',
352
+ backgroundColor: '#fef2f2',
353
+ border: '1px solid #fecaca',
354
+ borderRadius: '0.375rem',
355
+ color: '#dc2626',
356
+ fontSize: '0.875rem',
357
+ marginBottom: '1rem',
358
+ }}
359
+ role="alert"
360
+ >
361
+ {state.error}
362
+ <button
363
+ onClick={state.clearError}
364
+ style={{
365
+ marginLeft: '0.5rem',
366
+ color: '#dc2626',
367
+ background: 'none',
368
+ border: 'none',
369
+ cursor: 'pointer',
370
+ }}
371
+ aria-label="Dismiss error"
372
+ >
373
+
374
+ </button>
375
+ </div>
376
+ )}
377
+
378
+ {state.subscription ? (
379
+ <div>
380
+ <p
381
+ style={{
382
+ color: '#10b981',
383
+ fontSize: '0.875rem',
384
+ marginBottom: '1rem',
385
+ }}
386
+ >
387
+ ✓ You are subscribed to push notifications.
388
+ </p>
389
+ <button
390
+ onClick={handleUnsubscribe}
391
+ disabled={state.isLoading}
392
+ style={{
393
+ padding: '0.5rem 1rem',
394
+ backgroundColor: '#ef4444',
395
+ color: 'white',
396
+ border: 'none',
397
+ borderRadius: '0.375rem',
398
+ cursor: state.isLoading ? 'not-allowed' : 'pointer',
399
+ opacity: state.isLoading ? 0.5 : 1,
400
+ fontSize: '0.875rem',
401
+ }}
402
+ aria-label="Unsubscribe from push notifications"
403
+ >
404
+ {state.isLoading ? 'Unsubscribing...' : 'Unsubscribe'}
405
+ </button>
406
+ </div>
407
+ ) : (
408
+ <div>
409
+ <p
410
+ style={{
411
+ color: '#6b7280',
412
+ fontSize: '0.875rem',
413
+ marginBottom: '1rem',
414
+ }}
415
+ >
416
+ You are not subscribed to push notifications.
417
+ </p>
418
+ <button
419
+ onClick={handleSubscribe}
420
+ disabled={state.isLoading || !vapidPublicKey}
421
+ style={{
422
+ padding: '0.5rem 1rem',
423
+ backgroundColor: '#0d9488',
424
+ color: 'white',
425
+ border: 'none',
426
+ borderRadius: '0.375rem',
427
+ cursor:
428
+ state.isLoading || !vapidPublicKey ? 'not-allowed' : 'pointer',
429
+ opacity: state.isLoading || !vapidPublicKey ? 0.5 : 1,
430
+ fontSize: '0.875rem',
431
+ }}
432
+ aria-label="Subscribe to push notifications"
433
+ >
434
+ {state.isLoading ? 'Subscribing...' : 'Subscribe'}
435
+ </button>
436
+ {!vapidPublicKey && (
437
+ <p
438
+ style={{
439
+ color: '#f59e0b',
440
+ fontSize: '0.75rem',
441
+ marginTop: '0.5rem',
442
+ }}
443
+ >
444
+ VAPID public key is required for push notifications.
445
+ </p>
446
+ )}
447
+ </div>
448
+ )}
449
+ </div>
450
+ );
451
+ }
452
+
453
+ export default PushNotifications;
@@ -0,0 +1,219 @@
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 {
15
+ useContext,
16
+ useCallback,
17
+ useSyncExternalStore,
18
+ createContext,
19
+ } from 'react';
20
+ import type {
21
+ AeonNavigationEngine,
22
+ NavigationOptions,
23
+ PrefetchOptions,
24
+ NavigationState,
25
+ } from '@affectively/aeon-pages-runtime/navigation';
26
+ import { getNavigator } from '@affectively/aeon-pages-runtime/navigation';
27
+ import type { PresenceInfo as RoutePresenceInfo } from '@affectively/aeon-pages-runtime/navigation';
28
+
29
+ // Navigation-level predicted route (simpler than ML predictor's version)
30
+ export interface NavigationPrediction {
31
+ route: string;
32
+ probability: number;
33
+ reason: 'history' | 'hover' | 'visibility' | 'community';
34
+ }
35
+
36
+ // Context for providing custom navigation engine
37
+ export interface AeonNavigationContextValue {
38
+ navigator: AeonNavigationEngine;
39
+ }
40
+
41
+ export const AeonNavigationContext =
42
+ createContext<AeonNavigationContextValue | null>(null);
43
+
44
+ // Get navigator from context or use global singleton
45
+ function useNavigator(): AeonNavigationEngine {
46
+ const context = useContext(AeonNavigationContext);
47
+ return context?.navigator ?? getNavigator();
48
+ }
49
+
50
+ /**
51
+ * Main navigation hook - provides navigation, prefetch, and state
52
+ */
53
+ export function useAeonNavigation() {
54
+ const navigator = useNavigator();
55
+
56
+ // Subscribe to navigation state changes with useSyncExternalStore
57
+ const state = useSyncExternalStore(
58
+ useCallback((callback) => navigator.subscribe(callback), [navigator]),
59
+ () => navigator.getState(),
60
+ () => navigator.getState(),
61
+ );
62
+
63
+ // Navigation function with view transitions
64
+ const navigate = useCallback(
65
+ async (href: string, options?: NavigationOptions) => {
66
+ await navigator.navigate(href, options);
67
+ },
68
+ [navigator],
69
+ );
70
+
71
+ // Prefetch a route (session + presence)
72
+ const prefetch = useCallback(
73
+ async (href: string, options?: PrefetchOptions) => {
74
+ await navigator.prefetch(href, options);
75
+ },
76
+ [navigator],
77
+ );
78
+
79
+ // Go back in history
80
+ const back = useCallback(async () => {
81
+ await navigator.back();
82
+ }, [navigator]);
83
+
84
+ // Check if route is preloaded
85
+ const isPreloaded = useCallback(
86
+ (href: string): boolean => {
87
+ return navigator.isPreloaded(href);
88
+ },
89
+ [navigator],
90
+ );
91
+
92
+ // Preload ALL routes (total preload strategy)
93
+ const preloadAll = useCallback(
94
+ async (onProgress?: (loaded: number, total: number) => void) => {
95
+ await navigator.preloadAll(onProgress);
96
+ },
97
+ [navigator],
98
+ );
99
+
100
+ // Get cache statistics
101
+ const getCacheStats = useCallback(() => {
102
+ return navigator.getCacheStats();
103
+ }, [navigator]);
104
+
105
+ return {
106
+ // State
107
+ current: state.current,
108
+ previous: state.previous,
109
+ history: state.history,
110
+ isNavigating: state.isNavigating,
111
+
112
+ // Actions
113
+ navigate,
114
+ prefetch,
115
+ back,
116
+ preloadAll,
117
+
118
+ // Utilities
119
+ isPreloaded,
120
+ getCacheStats,
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Route Presence hook - subscribe to who's viewing/editing routes
126
+ *
127
+ * Presence flows upward through the Aeon hierarchy:
128
+ * - Page presence = users on this page
129
+ * - Site presence = aggregate of all page presence
130
+ * - Federation presence = aggregate across sites
131
+ *
132
+ * Note: This is different from usePresence in provider.tsx which is for
133
+ * page-level editing presence. This hook is for navigation-level presence
134
+ * (who's viewing what routes before you navigate there).
135
+ */
136
+ export function useRoutePresence() {
137
+ const navigator = useNavigator();
138
+
139
+ // Get cached presence for a route
140
+ const getPresence = useCallback(
141
+ (route: string): RoutePresenceInfo | null => {
142
+ return navigator.getPresence(route);
143
+ },
144
+ [navigator],
145
+ );
146
+
147
+ // Subscribe to presence updates
148
+ const subscribePresence = useCallback(
149
+ (
150
+ callback: (route: string, presence: RoutePresenceInfo) => void,
151
+ ): (() => void) => {
152
+ return navigator.subscribePresence(callback);
153
+ },
154
+ [navigator],
155
+ );
156
+
157
+ return {
158
+ getPresence,
159
+ subscribePresence,
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Navigation prediction hook
165
+ */
166
+ export function useNavigationPrediction(): {
167
+ predict: (fromRoute?: string) => NavigationPrediction[];
168
+ } {
169
+ const navigator = useNavigator();
170
+
171
+ // Get predictions for current route
172
+ const predict = useCallback(
173
+ (fromRoute?: string): NavigationPrediction[] => {
174
+ const state = navigator.getState();
175
+ return navigator.predict(fromRoute ?? state.current);
176
+ },
177
+ [navigator],
178
+ );
179
+
180
+ return {
181
+ predict,
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Hook for observing links and auto-prefetching
187
+ */
188
+ export function useLinkObserver(containerRef: React.RefObject<Element>) {
189
+ const navigator = useNavigator();
190
+
191
+ // Set up observation on mount
192
+ const observe = useCallback(() => {
193
+ if (!containerRef.current) return () => {};
194
+ return navigator.observeLinks(containerRef.current);
195
+ }, [navigator, containerRef]);
196
+
197
+ return { observe };
198
+ }
199
+
200
+ /**
201
+ * Hook for total preload progress
202
+ */
203
+ export function useTotalPreload() {
204
+ const { preloadAll, getCacheStats } = useAeonNavigation();
205
+
206
+ // Preload with progress tracking
207
+ const startPreload = useCallback(
208
+ async (onProgress?: (loaded: number, total: number) => void) => {
209
+ await preloadAll(onProgress);
210
+ },
211
+ [preloadAll],
212
+ );
213
+
214
+ return {
215
+ startPreload,
216
+ getStats: getCacheStats,
217
+ };
218
+ }
219
+