@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.
Files changed (72) hide show
  1. package/README.md +438 -0
  2. package/examples/basic/aeon.config.ts +39 -0
  3. package/examples/basic/components/Cursor.tsx +88 -0
  4. package/examples/basic/components/OfflineIndicator.tsx +93 -0
  5. package/examples/basic/components/PresenceBar.tsx +68 -0
  6. package/examples/basic/package.json +20 -0
  7. package/examples/basic/pages/index.tsx +73 -0
  8. package/package.json +90 -0
  9. package/packages/benchmarks/src/benchmark.test.ts +644 -0
  10. package/packages/cli/package.json +43 -0
  11. package/packages/cli/src/commands/build.test.ts +649 -0
  12. package/packages/cli/src/commands/build.ts +853 -0
  13. package/packages/cli/src/commands/dev.ts +463 -0
  14. package/packages/cli/src/commands/init.ts +395 -0
  15. package/packages/cli/src/commands/start.ts +289 -0
  16. package/packages/cli/src/index.ts +102 -0
  17. package/packages/directives/src/use-aeon.ts +266 -0
  18. package/packages/react/package.json +34 -0
  19. package/packages/react/src/Link.tsx +355 -0
  20. package/packages/react/src/hooks/useAeonNavigation.ts +204 -0
  21. package/packages/react/src/hooks/usePilotNavigation.ts +253 -0
  22. package/packages/react/src/hooks/useServiceWorker.ts +276 -0
  23. package/packages/react/src/hooks.ts +192 -0
  24. package/packages/react/src/index.ts +89 -0
  25. package/packages/react/src/provider.tsx +428 -0
  26. package/packages/runtime/package.json +70 -0
  27. package/packages/runtime/schema.sql +40 -0
  28. package/packages/runtime/src/api-routes.ts +453 -0
  29. package/packages/runtime/src/benchmark.ts +145 -0
  30. package/packages/runtime/src/cache.ts +287 -0
  31. package/packages/runtime/src/durable-object.ts +847 -0
  32. package/packages/runtime/src/index.ts +235 -0
  33. package/packages/runtime/src/navigation.test.ts +432 -0
  34. package/packages/runtime/src/navigation.ts +412 -0
  35. package/packages/runtime/src/nextjs-adapter.ts +254 -0
  36. package/packages/runtime/src/predictor.ts +368 -0
  37. package/packages/runtime/src/registry.ts +339 -0
  38. package/packages/runtime/src/router/context-extractor.ts +394 -0
  39. package/packages/runtime/src/router/esi-control-react.tsx +1172 -0
  40. package/packages/runtime/src/router/esi-control.ts +488 -0
  41. package/packages/runtime/src/router/esi-react.tsx +600 -0
  42. package/packages/runtime/src/router/esi.ts +595 -0
  43. package/packages/runtime/src/router/heuristic-adapter.test.ts +272 -0
  44. package/packages/runtime/src/router/heuristic-adapter.ts +544 -0
  45. package/packages/runtime/src/router/index.ts +158 -0
  46. package/packages/runtime/src/router/speculation.ts +442 -0
  47. package/packages/runtime/src/router/types.ts +514 -0
  48. package/packages/runtime/src/router.test.ts +466 -0
  49. package/packages/runtime/src/router.ts +285 -0
  50. package/packages/runtime/src/server.ts +446 -0
  51. package/packages/runtime/src/service-worker.ts +418 -0
  52. package/packages/runtime/src/speculation.test.ts +360 -0
  53. package/packages/runtime/src/speculation.ts +456 -0
  54. package/packages/runtime/src/storage.test.ts +1201 -0
  55. package/packages/runtime/src/storage.ts +1031 -0
  56. package/packages/runtime/src/tree-compiler.ts +252 -0
  57. package/packages/runtime/src/types.ts +444 -0
  58. package/packages/runtime/src/worker.ts +300 -0
  59. package/packages/runtime/tsconfig.json +19 -0
  60. package/packages/runtime/wrangler.toml +41 -0
  61. package/packages/runtime-wasm/Cargo.lock +436 -0
  62. package/packages/runtime-wasm/Cargo.toml +29 -0
  63. package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +328 -0
  64. package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1267 -0
  65. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
  66. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +73 -0
  67. package/packages/runtime-wasm/pkg/package.json +21 -0
  68. package/packages/runtime-wasm/src/hydrate.rs +352 -0
  69. package/packages/runtime-wasm/src/lib.rs +189 -0
  70. package/packages/runtime-wasm/src/render.rs +629 -0
  71. package/packages/runtime-wasm/src/router.rs +298 -0
  72. 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 };