@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,486 @@
1
+ /**
2
+ * Aeon Speculative Pre-Rendering
3
+ *
4
+ * Pre-renders pages before user clicks based on:
5
+ * 1. NavigationPredictor predictions (Markov chain, community patterns)
6
+ * 2. Link visibility (IntersectionObserver)
7
+ * 3. Hover intent signals
8
+ * 4. Browser Speculation Rules API (when available)
9
+ *
10
+ * This enables zero-latency navigation by having the page ready in memory.
11
+ */
12
+
13
+ import { getPredictor, type PredictedRoute } from './predictor';
14
+
15
+ export interface PreRenderedPage {
16
+ route: string;
17
+ html: string;
18
+ prefetchedAt: number;
19
+ confidence: number;
20
+ stale: boolean;
21
+ size: number;
22
+ }
23
+
24
+ export interface SpeculativeRendererConfig {
25
+ /** Maximum pages to keep in memory cache */
26
+ maxCachedPages: number;
27
+ /** Maximum total size in bytes for cache */
28
+ maxCacheSize: number;
29
+ /** Time before a cached page is considered stale (ms) */
30
+ staleTTL: number;
31
+ /** Minimum confidence threshold to pre-render */
32
+ minConfidence: number;
33
+ /** Root margin for IntersectionObserver */
34
+ intersectionRootMargin: string;
35
+ /** Whether to use browser's Speculation Rules API */
36
+ useSpeculationRules: boolean;
37
+ /** Whether to pre-render on hover */
38
+ prerenderOnHover: boolean;
39
+ /** Hover delay before pre-rendering (ms) */
40
+ hoverDelay: number;
41
+ /** Base URL for session fetches */
42
+ sessionBaseUrl: string;
43
+ }
44
+
45
+ const DEFAULT_CONFIG: SpeculativeRendererConfig = {
46
+ maxCachedPages: 5,
47
+ maxCacheSize: 5 * 1024 * 1024, // 5MB
48
+ staleTTL: 5 * 60 * 1000, // 5 minutes
49
+ minConfidence: 0.3,
50
+ intersectionRootMargin: '200px',
51
+ useSpeculationRules: true,
52
+ prerenderOnHover: true,
53
+ hoverDelay: 100,
54
+ sessionBaseUrl: '/_aeon/session',
55
+ };
56
+
57
+ export class SpeculativeRenderer {
58
+ private config: SpeculativeRendererConfig;
59
+ private cache = new Map<string, PreRenderedPage>();
60
+ private currentCacheSize = 0;
61
+ private observer: IntersectionObserver | null = null;
62
+ private hoverTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
63
+ private initialized = false;
64
+
65
+ constructor(config: Partial<SpeculativeRendererConfig> = {}) {
66
+ this.config = { ...DEFAULT_CONFIG, ...config };
67
+ }
68
+
69
+ /**
70
+ * Initialize the speculative renderer
71
+ * Call this after the page has loaded
72
+ */
73
+ init(): void {
74
+ if (this.initialized) return;
75
+ if (typeof window === 'undefined') return;
76
+
77
+ this.initialized = true;
78
+
79
+ // Setup IntersectionObserver for visible links
80
+ this.setupIntersectionObserver();
81
+
82
+ // Setup hover listeners if enabled
83
+ if (this.config.prerenderOnHover) {
84
+ this.setupHoverListeners();
85
+ }
86
+
87
+ // Inject Speculation Rules if supported
88
+ if (this.config.useSpeculationRules) {
89
+ this.injectSpeculationRules();
90
+ }
91
+
92
+ // Setup navigation interception
93
+ this.setupNavigationInterception();
94
+
95
+ // Start prediction-based pre-rendering
96
+ this.startPredictivePrerendering();
97
+
98
+ console.log('[aeon:speculation] Initialized');
99
+ }
100
+
101
+ /**
102
+ * Cleanup resources
103
+ */
104
+ destroy(): void {
105
+ if (this.observer) {
106
+ this.observer.disconnect();
107
+ this.observer = null;
108
+ }
109
+
110
+ for (const timeout of this.hoverTimeouts.values()) {
111
+ clearTimeout(timeout);
112
+ }
113
+ this.hoverTimeouts.clear();
114
+
115
+ this.cache.clear();
116
+ this.currentCacheSize = 0;
117
+ this.initialized = false;
118
+
119
+ console.log('[aeon:speculation] Destroyed');
120
+ }
121
+
122
+ /**
123
+ * Pre-render a specific route
124
+ */
125
+ async prerender(route: string, confidence = 1): Promise<boolean> {
126
+ // Skip if already cached and not stale
127
+ const existing = this.cache.get(route);
128
+ if (
129
+ existing &&
130
+ !existing.stale &&
131
+ Date.now() - existing.prefetchedAt < this.config.staleTTL
132
+ ) {
133
+ return true;
134
+ }
135
+
136
+ // Skip if current route
137
+ if (typeof window !== 'undefined' && window.location.pathname === route) {
138
+ return false;
139
+ }
140
+
141
+ try {
142
+ console.log(`[aeon:speculation] Pre-rendering: ${route}`);
143
+
144
+ // Fetch the pre-rendered HTML from the server
145
+ const response = await fetch(`${route}?_aeon_prerender=1`, {
146
+ headers: {
147
+ 'X-Aeon-Prerender': '1',
148
+ Accept: 'text/html',
149
+ },
150
+ });
151
+
152
+ if (!response.ok) {
153
+ console.warn(
154
+ `[aeon:speculation] Failed to fetch: ${route}`,
155
+ response.status,
156
+ );
157
+ return false;
158
+ }
159
+
160
+ const html = await response.text();
161
+ const size = html.length;
162
+
163
+ // Make room if needed
164
+ this.evictIfNeeded(size);
165
+
166
+ // Store in cache
167
+ const page: PreRenderedPage = {
168
+ route,
169
+ html,
170
+ prefetchedAt: Date.now(),
171
+ confidence,
172
+ stale: false,
173
+ size,
174
+ };
175
+
176
+ this.cache.set(route, page);
177
+ this.currentCacheSize += size;
178
+
179
+ console.log(
180
+ `[aeon:speculation] Cached: ${route} (${(size / 1024).toFixed(1)}KB)`,
181
+ );
182
+ return true;
183
+ } catch (err) {
184
+ console.warn(`[aeon:speculation] Error pre-rendering: ${route}`, err);
185
+ return false;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Navigate to a route using pre-rendered content if available
191
+ * Returns true if handled, false if fallback to normal navigation
192
+ */
193
+ async navigate(route: string): Promise<boolean> {
194
+ const cached = this.cache.get(route);
195
+
196
+ if (
197
+ cached &&
198
+ !cached.stale &&
199
+ Date.now() - cached.prefetchedAt < this.config.staleTTL
200
+ ) {
201
+ console.log(`[aeon:speculation] Instant nav to: ${route}`);
202
+
203
+ // Instant navigation - replace document content
204
+ document.open();
205
+ document.write(cached.html);
206
+ document.close();
207
+
208
+ // Update URL
209
+ history.pushState({ aeonSpeculative: true }, '', route);
210
+
211
+ // Re-initialize for new page
212
+ this.reinitialize();
213
+
214
+ return true;
215
+ }
216
+
217
+ return false;
218
+ }
219
+
220
+ /**
221
+ * Invalidate cached pages
222
+ */
223
+ invalidate(routes?: string[]): void {
224
+ if (routes) {
225
+ for (const route of routes) {
226
+ const cached = this.cache.get(route);
227
+ if (cached) {
228
+ cached.stale = true;
229
+ }
230
+ }
231
+ } else {
232
+ // Mark all as stale
233
+ for (const page of this.cache.values()) {
234
+ page.stale = true;
235
+ }
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Get cache statistics
241
+ */
242
+ getStats(): {
243
+ cachedPages: number;
244
+ cacheSize: number;
245
+ cacheHitRate: number;
246
+ } {
247
+ return {
248
+ cachedPages: this.cache.size,
249
+ cacheSize: this.currentCacheSize,
250
+ cacheHitRate: 0, // Would need to track hits/misses
251
+ };
252
+ }
253
+
254
+ // ---------- Private Methods ----------
255
+
256
+ private setupIntersectionObserver(): void {
257
+ this.observer = new IntersectionObserver(
258
+ (entries) => this.onLinksVisible(entries),
259
+ { rootMargin: this.config.intersectionRootMargin },
260
+ );
261
+
262
+ // Observe all internal links
263
+ this.observeLinks();
264
+ }
265
+
266
+ private observeLinks(): void {
267
+ if (!this.observer) return;
268
+
269
+ document.querySelectorAll('a[href^="/"]').forEach((link) => {
270
+ this.observer!.observe(link);
271
+ });
272
+ }
273
+
274
+ private async onLinksVisible(
275
+ entries: IntersectionObserverEntry[],
276
+ ): Promise<void> {
277
+ for (const entry of entries) {
278
+ if (!entry.isIntersecting) continue;
279
+
280
+ const link = entry.target as HTMLAnchorElement;
281
+ const route = new URL(link.href, window.location.origin).pathname;
282
+
283
+ // Stop observing this link
284
+ this.observer?.unobserve(link);
285
+
286
+ // Pre-render with visibility confidence
287
+ await this.prerender(route, 0.7);
288
+ }
289
+ }
290
+
291
+ private setupHoverListeners(): void {
292
+ document.addEventListener('mouseenter', (e) => this.onLinkHover(e), true);
293
+ document.addEventListener('mouseleave', (e) => this.onLinkLeave(e), true);
294
+ }
295
+
296
+ private onLinkHover(e: Event): void {
297
+ const link = (e.target as Element).closest(
298
+ 'a[href^="/"]',
299
+ ) as HTMLAnchorElement | null;
300
+ if (!link) return;
301
+
302
+ const route = new URL(link.href, window.location.origin).pathname;
303
+
304
+ // Set timeout to pre-render on sustained hover
305
+ const timeout = setTimeout(() => {
306
+ this.prerender(route, 0.9);
307
+ }, this.config.hoverDelay);
308
+
309
+ this.hoverTimeouts.set(route, timeout);
310
+ }
311
+
312
+ private onLinkLeave(e: Event): void {
313
+ const link = (e.target as Element).closest(
314
+ 'a[href^="/"]',
315
+ ) as HTMLAnchorElement | null;
316
+ if (!link) return;
317
+
318
+ const route = new URL(link.href, window.location.origin).pathname;
319
+
320
+ // Clear pending timeout
321
+ const timeout = this.hoverTimeouts.get(route);
322
+ if (timeout) {
323
+ clearTimeout(timeout);
324
+ this.hoverTimeouts.delete(route);
325
+ }
326
+ }
327
+
328
+ private injectSpeculationRules(): void {
329
+ // Check if browser supports Speculation Rules
330
+ if (
331
+ !(
332
+ 'supports' in HTMLScriptElement &&
333
+ HTMLScriptElement.supports('speculationrules')
334
+ )
335
+ ) {
336
+ console.log(
337
+ '[aeon:speculation] Browser does not support Speculation Rules API',
338
+ );
339
+ return;
340
+ }
341
+
342
+ const rules = {
343
+ prerender: [
344
+ {
345
+ source: 'document',
346
+ where: {
347
+ href_matches: '/*',
348
+ not: {
349
+ or: [
350
+ { href_matches: '/api/*' },
351
+ { href_matches: '/_aeon/*' },
352
+ { selector_matches: '[data-aeon-no-prerender]' },
353
+ ],
354
+ },
355
+ },
356
+ eagerness: 'moderate',
357
+ },
358
+ ],
359
+ };
360
+
361
+ const script = document.createElement('script');
362
+ script.type = 'speculationrules';
363
+ script.textContent = JSON.stringify(rules);
364
+ document.head.appendChild(script);
365
+
366
+ console.log('[aeon:speculation] Speculation Rules injected');
367
+ }
368
+
369
+ private setupNavigationInterception(): void {
370
+ // Intercept link clicks
371
+ document.addEventListener('click', async (e) => {
372
+ const link = (e.target as Element).closest(
373
+ 'a[href^="/"]',
374
+ ) as HTMLAnchorElement | null;
375
+ if (!link) return;
376
+
377
+ // Skip if modifier keys pressed
378
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
379
+
380
+ const route = new URL(link.href, window.location.origin).pathname;
381
+
382
+ // Try speculative navigation
383
+ if (await this.navigate(route)) {
384
+ e.preventDefault();
385
+ }
386
+ });
387
+
388
+ // Handle popstate for back/forward
389
+ window.addEventListener('popstate', (e) => {
390
+ if (e.state?.aeonSpeculative) {
391
+ // This was a speculative navigation, handle accordingly
392
+ const route = window.location.pathname;
393
+ const cached = this.cache.get(route);
394
+ if (cached && !cached.stale) {
395
+ document.open();
396
+ document.write(cached.html);
397
+ document.close();
398
+ this.reinitialize();
399
+ }
400
+ }
401
+ });
402
+ }
403
+
404
+ private async startPredictivePrerendering(): Promise<void> {
405
+ const predictor = getPredictor();
406
+ const currentRoute = window.location.pathname;
407
+
408
+ // Get predictions
409
+ const predictions = predictor.predict(currentRoute);
410
+
411
+ // Pre-render high-confidence predictions
412
+ for (const prediction of predictions) {
413
+ if (prediction.probability >= this.config.minConfidence) {
414
+ // Don't await - fire and forget
415
+ this.prerender(prediction.route, prediction.probability);
416
+ }
417
+ }
418
+ }
419
+
420
+ private reinitialize(): void {
421
+ // Re-observe links after DOM replacement
422
+ setTimeout(() => {
423
+ this.observeLinks();
424
+ this.startPredictivePrerendering();
425
+ }, 0);
426
+ }
427
+
428
+ private evictIfNeeded(incomingSize: number): void {
429
+ // Check if we need to evict
430
+ while (
431
+ (this.cache.size >= this.config.maxCachedPages ||
432
+ this.currentCacheSize + incomingSize > this.config.maxCacheSize) &&
433
+ this.cache.size > 0
434
+ ) {
435
+ // Find oldest or lowest confidence page
436
+ let toEvict: string | null = null;
437
+ let lowestScore = Infinity;
438
+
439
+ for (const [route, page] of this.cache) {
440
+ // Score based on age and confidence
441
+ const age = Date.now() - page.prefetchedAt;
442
+ const score = page.confidence / (1 + age / 60000); // Decay over time
443
+
444
+ if (page.stale || score < lowestScore) {
445
+ lowestScore = score;
446
+ toEvict = route;
447
+ }
448
+ }
449
+
450
+ if (toEvict) {
451
+ const page = this.cache.get(toEvict)!;
452
+ this.cache.delete(toEvict);
453
+ this.currentCacheSize -= page.size;
454
+ console.log(`[aeon:speculation] Evicted: ${toEvict}`);
455
+ } else {
456
+ break;
457
+ }
458
+ }
459
+ }
460
+ }
461
+
462
+ // Singleton instance
463
+ let globalSpeculativeRenderer: SpeculativeRenderer | null = null;
464
+
465
+ export function getSpeculativeRenderer(): SpeculativeRenderer {
466
+ if (!globalSpeculativeRenderer) {
467
+ globalSpeculativeRenderer = new SpeculativeRenderer();
468
+ }
469
+ return globalSpeculativeRenderer;
470
+ }
471
+
472
+ export function setSpeculativeRenderer(renderer: SpeculativeRenderer): void {
473
+ globalSpeculativeRenderer = renderer;
474
+ }
475
+
476
+ /**
477
+ * Initialize speculative rendering (call on page load)
478
+ */
479
+ export function initSpeculativeRendering(
480
+ config?: Partial<SpeculativeRendererConfig>,
481
+ ): SpeculativeRenderer {
482
+ const renderer = new SpeculativeRenderer(config);
483
+ setSpeculativeRenderer(renderer);
484
+ renderer.init();
485
+ return renderer;
486
+ }