@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,479 @@
1
+ /**
2
+ * Navigation Cache - In-memory cache for total preload navigation
3
+ *
4
+ * With 8.4KB framework + ~2-5KB per page session, we can preload EVERYTHING.
5
+ * Site with 100 pages = ~315KB total (smaller than one hero image!)
6
+ */
7
+
8
+ export interface CachedSession {
9
+ sessionId: string;
10
+ route: string;
11
+ tree: unknown;
12
+ data: Record<string, unknown>;
13
+ schemaVersion: string;
14
+ cachedAt: number;
15
+ expiresAt?: number;
16
+ }
17
+
18
+ export interface CacheStats {
19
+ size: number;
20
+ totalBytes: number;
21
+ hitRate: number;
22
+ preloadedRoutes: number;
23
+ }
24
+
25
+ export interface NavigationCacheOptions {
26
+ maxSize?: number;
27
+ defaultTtl?: number;
28
+ onEvict?: (session: CachedSession) => void;
29
+ }
30
+
31
+ export class NavigationCache {
32
+ private cache: Map<string, CachedSession> = new Map();
33
+ private accessOrder: string[] = [];
34
+ private hits = 0;
35
+ private misses = 0;
36
+ private maxSize: number;
37
+ private defaultTtl: number;
38
+ private onEvict?: (session: CachedSession) => void;
39
+
40
+ constructor(options: NavigationCacheOptions = {}) {
41
+ this.maxSize = options.maxSize ?? 1000;
42
+ this.defaultTtl = options.defaultTtl ?? 5 * 60 * 1000; // 5 minutes
43
+ this.onEvict = options.onEvict;
44
+ }
45
+
46
+ /**
47
+ * Get a session from cache
48
+ */
49
+ get(sessionId: string): CachedSession | null {
50
+ const session = this.cache.get(sessionId);
51
+
52
+ if (!session) {
53
+ this.misses++;
54
+ return null;
55
+ }
56
+
57
+ // Check expiration
58
+ if (session.expiresAt && Date.now() > session.expiresAt) {
59
+ this.cache.delete(sessionId);
60
+ this.removeFromAccessOrder(sessionId);
61
+ this.misses++;
62
+ return null;
63
+ }
64
+
65
+ this.hits++;
66
+ this.updateAccessOrder(sessionId);
67
+ return session;
68
+ }
69
+
70
+ /**
71
+ * Store a session in cache
72
+ */
73
+ set(session: CachedSession, ttl?: number): void {
74
+ const sessionId = session.sessionId;
75
+
76
+ // Evict if at capacity
77
+ if (!this.cache.has(sessionId) && this.cache.size >= this.maxSize) {
78
+ this.evictLRU();
79
+ }
80
+
81
+ const cached: CachedSession = {
82
+ ...session,
83
+ cachedAt: Date.now(),
84
+ expiresAt: ttl ? Date.now() + ttl : Date.now() + this.defaultTtl,
85
+ };
86
+
87
+ this.cache.set(sessionId, cached);
88
+ this.updateAccessOrder(sessionId);
89
+ }
90
+
91
+ /**
92
+ * Check if session is cached
93
+ */
94
+ has(sessionId: string): boolean {
95
+ const session = this.cache.get(sessionId);
96
+ if (!session) return false;
97
+
98
+ // Check expiration
99
+ if (session.expiresAt && Date.now() > session.expiresAt) {
100
+ this.cache.delete(sessionId);
101
+ this.removeFromAccessOrder(sessionId);
102
+ return false;
103
+ }
104
+
105
+ return true;
106
+ }
107
+
108
+ /**
109
+ * Prefetch a session by ID
110
+ */
111
+ async prefetch(
112
+ sessionId: string,
113
+ fetcher: () => Promise<CachedSession>,
114
+ ): Promise<CachedSession> {
115
+ // Return cached if available
116
+ const cached = this.get(sessionId);
117
+ if (cached) return cached;
118
+
119
+ // Fetch and cache
120
+ const session = await fetcher();
121
+ this.set(session);
122
+ return session;
123
+ }
124
+
125
+ /**
126
+ * Prefetch multiple sessions in parallel
127
+ */
128
+ async prefetchMany(
129
+ sessionIds: string[],
130
+ fetcher: (sessionId: string) => Promise<CachedSession>,
131
+ ): Promise<CachedSession[]> {
132
+ const promises = sessionIds.map(async (sessionId) => {
133
+ const cached = this.get(sessionId);
134
+ if (cached) return cached;
135
+
136
+ const session = await fetcher(sessionId);
137
+ this.set(session);
138
+ return session;
139
+ });
140
+
141
+ return Promise.all(promises);
142
+ }
143
+
144
+ /**
145
+ * Preload all sessions (total preload strategy)
146
+ */
147
+ async preloadAll(
148
+ manifest: { sessionId: string; route: string }[],
149
+ fetcher: (sessionId: string) => Promise<CachedSession>,
150
+ options: { onProgress?: (loaded: number, total: number) => void } = {},
151
+ ): Promise<void> {
152
+ const total = manifest.length;
153
+ let loaded = 0;
154
+
155
+ // Use batching to avoid overwhelming the network
156
+ const batchSize = 10;
157
+ for (let i = 0; i < manifest.length; i += batchSize) {
158
+ const batch = manifest.slice(i, i + batchSize);
159
+
160
+ await Promise.all(
161
+ batch.map(async ({ sessionId }) => {
162
+ if (!this.has(sessionId)) {
163
+ try {
164
+ const session = await fetcher(sessionId);
165
+ this.set(session, Infinity); // Never expire for total preload
166
+ } catch {
167
+ // Ignore failed fetches during preload
168
+ }
169
+ }
170
+ loaded++;
171
+ options.onProgress?.(loaded, total);
172
+ }),
173
+ );
174
+
175
+ // Small delay between batches to keep main thread responsive
176
+ await new Promise((r) => setTimeout(r, 10));
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Invalidate a cached session
182
+ */
183
+ invalidate(sessionId: string): void {
184
+ const session = this.cache.get(sessionId);
185
+ if (session && this.onEvict) {
186
+ this.onEvict(session);
187
+ }
188
+ this.cache.delete(sessionId);
189
+ this.removeFromAccessOrder(sessionId);
190
+ }
191
+
192
+ /**
193
+ * Clear all cached sessions
194
+ */
195
+ clear(): void {
196
+ if (this.onEvict) {
197
+ for (const session of this.cache.values()) {
198
+ this.onEvict(session);
199
+ }
200
+ }
201
+ this.cache.clear();
202
+ this.accessOrder = [];
203
+ this.hits = 0;
204
+ this.misses = 0;
205
+ }
206
+
207
+ /**
208
+ * Get cache statistics
209
+ */
210
+ getStats(): CacheStats {
211
+ let totalBytes = 0;
212
+ for (const session of this.cache.values()) {
213
+ totalBytes += JSON.stringify(session).length;
214
+ }
215
+
216
+ const total = this.hits + this.misses;
217
+ return {
218
+ size: this.cache.size,
219
+ totalBytes,
220
+ hitRate: total > 0 ? this.hits / total : 0,
221
+ preloadedRoutes: this.cache.size,
222
+ };
223
+ }
224
+
225
+ /**
226
+ * Get all cached session IDs
227
+ */
228
+ keys(): string[] {
229
+ return Array.from(this.cache.keys());
230
+ }
231
+
232
+ /**
233
+ * Export cache for service worker storage
234
+ */
235
+ export(): CachedSession[] {
236
+ return Array.from(this.cache.values());
237
+ }
238
+
239
+ /**
240
+ * Import cache from service worker storage
241
+ */
242
+ import(sessions: CachedSession[]): void {
243
+ for (const session of sessions) {
244
+ this.set(session);
245
+ }
246
+ }
247
+
248
+ // LRU eviction
249
+ private evictLRU(): void {
250
+ if (this.accessOrder.length === 0) return;
251
+
252
+ const lruId = this.accessOrder.shift()!;
253
+ const session = this.cache.get(lruId);
254
+
255
+ if (session && this.onEvict) {
256
+ this.onEvict(session);
257
+ }
258
+
259
+ this.cache.delete(lruId);
260
+ }
261
+
262
+ private updateAccessOrder(sessionId: string): void {
263
+ this.removeFromAccessOrder(sessionId);
264
+ this.accessOrder.push(sessionId);
265
+ }
266
+
267
+ private removeFromAccessOrder(sessionId: string): void {
268
+ const index = this.accessOrder.indexOf(sessionId);
269
+ if (index !== -1) {
270
+ this.accessOrder.splice(index, 1);
271
+ }
272
+ }
273
+ }
274
+
275
+ // Singleton instance for global access
276
+ let globalCache: NavigationCache | null = null;
277
+
278
+ export function getNavigationCache(): NavigationCache {
279
+ if (!globalCache) {
280
+ globalCache = new NavigationCache();
281
+ }
282
+ return globalCache;
283
+ }
284
+
285
+ export function setNavigationCache(cache: NavigationCache): void {
286
+ globalCache = cache;
287
+ }
288
+
289
+ // =============================================================================
290
+ // SKELETON CACHE - Separate fast cache for skeleton-first rendering
291
+ // =============================================================================
292
+
293
+ /** Cached skeleton data for a route */
294
+ export interface CachedSkeleton {
295
+ route: string;
296
+ html: string;
297
+ css: string;
298
+ cachedAt: number;
299
+ expiresAt?: number;
300
+ }
301
+
302
+ /** Skeleton cache options */
303
+ export interface SkeletonCacheOptions {
304
+ /** Maximum number of skeletons to cache */
305
+ maxSize?: number;
306
+ /** Default TTL in milliseconds */
307
+ defaultTtl?: number;
308
+ }
309
+
310
+ /**
311
+ * Skeleton Cache - Optimized for instant skeleton delivery
312
+ *
313
+ * Skeletons are cached separately from full pages for faster access.
314
+ * In edge environments, skeletons are stored in KV for ~1ms access.
315
+ */
316
+ export class SkeletonCache {
317
+ private cache: Map<string, CachedSkeleton> = new Map();
318
+ private maxSize: number;
319
+ private defaultTtl: number;
320
+
321
+ constructor(options: SkeletonCacheOptions = {}) {
322
+ this.maxSize = options.maxSize ?? 500;
323
+ this.defaultTtl = options.defaultTtl ?? 30 * 60 * 1000; // 30 minutes
324
+ }
325
+
326
+ /**
327
+ * Get skeleton for a route
328
+ */
329
+ get(route: string): CachedSkeleton | null {
330
+ const skeleton = this.cache.get(route);
331
+
332
+ if (!skeleton) return null;
333
+
334
+ // Check expiration
335
+ if (skeleton.expiresAt && Date.now() > skeleton.expiresAt) {
336
+ this.cache.delete(route);
337
+ return null;
338
+ }
339
+
340
+ return skeleton;
341
+ }
342
+
343
+ /**
344
+ * Store skeleton for a route
345
+ */
346
+ set(skeleton: CachedSkeleton, ttl?: number): void {
347
+ // Evict oldest if at capacity
348
+ if (!this.cache.has(skeleton.route) && this.cache.size >= this.maxSize) {
349
+ const oldest = this.cache.keys().next().value;
350
+ if (oldest) this.cache.delete(oldest);
351
+ }
352
+
353
+ this.cache.set(skeleton.route, {
354
+ ...skeleton,
355
+ cachedAt: Date.now(),
356
+ expiresAt: ttl ? Date.now() + ttl : Date.now() + this.defaultTtl,
357
+ });
358
+ }
359
+
360
+ /**
361
+ * Check if skeleton is cached
362
+ */
363
+ has(route: string): boolean {
364
+ const skeleton = this.cache.get(route);
365
+ if (!skeleton) return false;
366
+
367
+ if (skeleton.expiresAt && Date.now() > skeleton.expiresAt) {
368
+ this.cache.delete(route);
369
+ return false;
370
+ }
371
+
372
+ return true;
373
+ }
374
+
375
+ /**
376
+ * Invalidate skeleton for a route
377
+ */
378
+ invalidate(route: string): void {
379
+ this.cache.delete(route);
380
+ }
381
+
382
+ /**
383
+ * Clear all cached skeletons
384
+ */
385
+ clear(): void {
386
+ this.cache.clear();
387
+ }
388
+
389
+ /**
390
+ * Get cache size
391
+ */
392
+ get size(): number {
393
+ return this.cache.size;
394
+ }
395
+
396
+ /**
397
+ * Export all skeletons for service worker
398
+ */
399
+ export(): CachedSkeleton[] {
400
+ return Array.from(this.cache.values());
401
+ }
402
+
403
+ /**
404
+ * Import skeletons from service worker
405
+ */
406
+ import(skeletons: CachedSkeleton[]): void {
407
+ for (const skeleton of skeletons) {
408
+ this.set(skeleton);
409
+ }
410
+ }
411
+ }
412
+
413
+ /** Skeleton and content result for progressive rendering */
414
+ export interface SkeletonWithContent {
415
+ /** Skeleton HTML (available immediately) */
416
+ skeleton: CachedSkeleton | null;
417
+ /** Content promise (resolves later) */
418
+ content: Promise<CachedSession | null>;
419
+ }
420
+
421
+ /**
422
+ * Get skeleton and content in parallel for optimal UX
423
+ *
424
+ * @param route - The route to fetch
425
+ * @param skeletonCache - Skeleton cache instance
426
+ * @param sessionCache - Session cache instance
427
+ * @param contentFetcher - Function to fetch full content
428
+ */
429
+ export function getWithSkeleton(
430
+ route: string,
431
+ skeletonCache: SkeletonCache,
432
+ sessionCache: NavigationCache,
433
+ contentFetcher: (route: string) => Promise<CachedSession>,
434
+ ): SkeletonWithContent {
435
+ // Get skeleton immediately (sync)
436
+ const skeleton = skeletonCache.get(route);
437
+
438
+ // Start content fetch in parallel
439
+ const content = (async () => {
440
+ // Check session cache first
441
+ const sessionId = routeToSessionId(route);
442
+ const cached = sessionCache.get(sessionId);
443
+ if (cached) return cached;
444
+
445
+ // Fetch from network
446
+ try {
447
+ const session = await contentFetcher(route);
448
+ sessionCache.set(session);
449
+ return session;
450
+ } catch {
451
+ return null;
452
+ }
453
+ })();
454
+
455
+ return { skeleton, content };
456
+ }
457
+
458
+ /**
459
+ * Convert route to session ID
460
+ * This is a simple implementation - real apps may have more complex mapping
461
+ */
462
+ function routeToSessionId(route: string): string {
463
+ // Remove leading/trailing slashes and replace slashes with dashes
464
+ return route.replace(/^\/|\/$/g, '').replace(/\//g, '-') || 'index';
465
+ }
466
+
467
+ // Singleton skeleton cache
468
+ let globalSkeletonCache: SkeletonCache | null = null;
469
+
470
+ export function getSkeletonCache(): SkeletonCache {
471
+ if (!globalSkeletonCache) {
472
+ globalSkeletonCache = new SkeletonCache();
473
+ }
474
+ return globalSkeletonCache;
475
+ }
476
+
477
+ export function setSkeletonCache(cache: SkeletonCache): void {
478
+ globalSkeletonCache = cache;
479
+ }