@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,421 @@
1
+ import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test';
2
+ import {
3
+ NavigationCache,
4
+ getNavigationCache,
5
+ setNavigationCache,
6
+ type CachedSession,
7
+ } from './cache';
8
+ import { AeonNavigationEngine, getNavigator, setNavigator } from './navigation';
9
+ import {
10
+ NavigationPredictor,
11
+ getPredictor,
12
+ setPredictor,
13
+ type NavigationRecord,
14
+ } from './predictor';
15
+ import { AeonRouter } from './router';
16
+
17
+ describe('NavigationCache', () => {
18
+ let cache: NavigationCache;
19
+
20
+ beforeEach(() => {
21
+ cache = new NavigationCache({ maxSize: 10, defaultTtl: 1000 });
22
+ });
23
+
24
+ const createSession = (id: string): CachedSession => ({
25
+ sessionId: id,
26
+ route: `/${id}`,
27
+ tree: { type: 'div', children: [] },
28
+ data: { title: id },
29
+ schemaVersion: '1.0.0',
30
+ cachedAt: Date.now(),
31
+ });
32
+
33
+ test('stores and retrieves sessions', () => {
34
+ const session = createSession('test');
35
+ cache.set(session);
36
+ const retrieved = cache.get('test');
37
+ expect(retrieved).not.toBeNull();
38
+ expect(retrieved?.sessionId).toBe('test');
39
+ });
40
+
41
+ test('returns null for non-existent sessions', () => {
42
+ expect(cache.get('nonexistent')).toBeNull();
43
+ });
44
+
45
+ test('tracks hit/miss rate', () => {
46
+ const session = createSession('test');
47
+ cache.set(session);
48
+
49
+ cache.get('test'); // Hit
50
+ cache.get('test'); // Hit
51
+ cache.get('nonexistent'); // Miss
52
+
53
+ const stats = cache.getStats();
54
+ expect(stats.hitRate).toBeCloseTo(0.67, 1);
55
+ });
56
+
57
+ test('evicts LRU items when at capacity', () => {
58
+ // Fill cache to capacity
59
+ for (let i = 0; i < 10; i++) {
60
+ cache.set(createSession(`session-${i}`));
61
+ }
62
+
63
+ // Access some sessions to update LRU order
64
+ cache.get('session-5');
65
+ cache.get('session-9');
66
+
67
+ // Add new session, should evict oldest (session-0)
68
+ cache.set(createSession('new-session'));
69
+
70
+ expect(cache.get('session-0')).toBeNull();
71
+ expect(cache.get('session-5')).not.toBeNull();
72
+ expect(cache.get('new-session')).not.toBeNull();
73
+ });
74
+
75
+ test('expires sessions after TTL', async () => {
76
+ const cache = new NavigationCache({ defaultTtl: 50 });
77
+ cache.set(createSession('expiring'));
78
+
79
+ expect(cache.get('expiring')).not.toBeNull();
80
+
81
+ // Wait for expiration
82
+ await new Promise((r) => setTimeout(r, 60));
83
+
84
+ expect(cache.get('expiring')).toBeNull();
85
+ });
86
+
87
+ test('prefetches sessions with fetcher', async () => {
88
+ const fetcher = mock(() => Promise.resolve(createSession('fetched')));
89
+
90
+ const result = await cache.prefetch('fetched', fetcher);
91
+
92
+ expect(result.sessionId).toBe('fetched');
93
+ expect(fetcher).toHaveBeenCalledTimes(1);
94
+
95
+ // Second call should use cache
96
+ await cache.prefetch('fetched', fetcher);
97
+ expect(fetcher).toHaveBeenCalledTimes(1);
98
+ });
99
+
100
+ test('prefetches many sessions in parallel', async () => {
101
+ const fetcher = mock((id: string) => Promise.resolve(createSession(id)));
102
+
103
+ const results = await cache.prefetchMany(['a', 'b', 'c'], fetcher);
104
+
105
+ expect(results).toHaveLength(3);
106
+ expect(fetcher).toHaveBeenCalledTimes(3);
107
+ });
108
+
109
+ test('preloads all sessions with progress', async () => {
110
+ const manifest = [
111
+ { sessionId: 's1', route: '/s1' },
112
+ { sessionId: 's2', route: '/s2' },
113
+ { sessionId: 's3', route: '/s3' },
114
+ ];
115
+
116
+ const fetcher = mock((id: string) => Promise.resolve(createSession(id)));
117
+
118
+ const progress: number[] = [];
119
+ await cache.preloadAll(manifest, fetcher, {
120
+ onProgress: (loaded, total) => progress.push(loaded),
121
+ });
122
+
123
+ expect(progress).toContain(3); // Final count
124
+ expect(fetcher).toHaveBeenCalledTimes(3);
125
+ });
126
+
127
+ test('invalidates specific sessions', () => {
128
+ cache.set(createSession('test'));
129
+ expect(cache.has('test')).toBe(true);
130
+
131
+ cache.invalidate('test');
132
+ expect(cache.has('test')).toBe(false);
133
+ });
134
+
135
+ test('clears all sessions', () => {
136
+ cache.set(createSession('a'));
137
+ cache.set(createSession('b'));
138
+
139
+ cache.clear();
140
+
141
+ expect(cache.get('a')).toBeNull();
142
+ expect(cache.get('b')).toBeNull();
143
+ expect(cache.getStats().size).toBe(0);
144
+ });
145
+
146
+ test('exports and imports sessions', () => {
147
+ cache.set(createSession('a'));
148
+ cache.set(createSession('b'));
149
+
150
+ const exported = cache.export();
151
+ expect(exported).toHaveLength(2);
152
+
153
+ const newCache = new NavigationCache();
154
+ newCache.import(exported);
155
+
156
+ expect(newCache.get('a')).not.toBeNull();
157
+ expect(newCache.get('b')).not.toBeNull();
158
+ });
159
+ });
160
+
161
+ describe('NavigationPredictor', () => {
162
+ let predictor: NavigationPredictor;
163
+
164
+ beforeEach(() => {
165
+ predictor = new NavigationPredictor();
166
+ });
167
+
168
+ const record = (from: string, to: string): NavigationRecord => ({
169
+ from,
170
+ to,
171
+ timestamp: Date.now(),
172
+ duration: 5000,
173
+ source: 'click',
174
+ });
175
+
176
+ test('records navigation and predicts based on history', () => {
177
+ // Build history: / -> /about (3x), / -> /blog (1x)
178
+ predictor.record(record('/', '/about'));
179
+ predictor.record(record('/', '/about'));
180
+ predictor.record(record('/', '/about'));
181
+ predictor.record(record('/', '/blog'));
182
+
183
+ const predictions = predictor.predict('/');
184
+
185
+ expect(predictions.length).toBeGreaterThan(0);
186
+ expect(predictions[0].route).toBe('/about'); // Higher probability
187
+ expect(predictions[0].probability).toBeGreaterThan(0.5);
188
+ });
189
+
190
+ test('applies decay to old records', () => {
191
+ predictor.record(record('/', '/old'));
192
+
193
+ // Record many new navigations to trigger decay
194
+ for (let i = 0; i < 100; i++) {
195
+ predictor.record(record('/other', '/new'));
196
+ }
197
+
198
+ const predictions = predictor.predict('/');
199
+ // Old prediction should have decayed significantly
200
+ const oldPred = predictions.find((p) => p.route === '/old');
201
+ expect(oldPred?.probability ?? 0).toBeLessThan(0.5);
202
+ });
203
+
204
+ test('returns empty predictions for unknown routes', () => {
205
+ const predictions = predictor.predict('/unknown');
206
+ expect(predictions).toEqual([]);
207
+ });
208
+
209
+ test('limits predictions to maxPredictions', () => {
210
+ const predictor = new NavigationPredictor({ maxPredictions: 2 });
211
+
212
+ predictor.record(record('/', '/a'));
213
+ predictor.record(record('/', '/b'));
214
+ predictor.record(record('/', '/c'));
215
+ predictor.record(record('/', '/d'));
216
+
217
+ const predictions = predictor.predict('/');
218
+ expect(predictions.length).toBeLessThanOrEqual(2);
219
+ });
220
+
221
+ test('filters out low probability predictions', () => {
222
+ const predictor = new NavigationPredictor({ minProbability: 0.3 });
223
+
224
+ // Create very uneven distribution
225
+ for (let i = 0; i < 100; i++) {
226
+ predictor.record(record('/', '/main'));
227
+ }
228
+ predictor.record(record('/', '/rare'));
229
+
230
+ const predictions = predictor.predict('/');
231
+
232
+ // /rare should be filtered out due to low probability
233
+ const rarePred = predictions.find((p) => p.route === '/rare');
234
+ expect(rarePred).toBeUndefined();
235
+ });
236
+
237
+ test('exports and imports data', () => {
238
+ predictor.record(record('/', '/about'));
239
+ predictor.record(record('/', '/blog'));
240
+
241
+ const exported = predictor.export();
242
+
243
+ const newPredictor = new NavigationPredictor();
244
+ newPredictor.import(exported);
245
+
246
+ const predictions = newPredictor.predict('/');
247
+ expect(predictions.length).toBeGreaterThan(0);
248
+ });
249
+
250
+ test('provides statistics', () => {
251
+ predictor.record(record('/', '/about'));
252
+ predictor.record(record('/', '/blog'));
253
+ predictor.record(record('/about', '/contact'));
254
+
255
+ const stats = predictor.getStats();
256
+
257
+ expect(stats.totalRecords).toBe(3);
258
+ expect(stats.uniqueRoutes).toBe(2); // / and /about
259
+ expect(stats.transitionPairs).toBe(3);
260
+ });
261
+
262
+ test('clears all data', () => {
263
+ predictor.record(record('/', '/about'));
264
+ predictor.clear();
265
+
266
+ expect(predictor.predict('/').length).toBe(0);
267
+ expect(predictor.getStats().totalRecords).toBe(0);
268
+ });
269
+
270
+ test('merges community patterns', () => {
271
+ const patterns = new Map([
272
+ [
273
+ '/',
274
+ {
275
+ route: '/',
276
+ popularity: 100,
277
+ avgTimeSpent: 5000,
278
+ nextRoutes: [
279
+ { route: '/popular', count: 80 },
280
+ { route: '/other', count: 20 },
281
+ ],
282
+ },
283
+ ],
284
+ ]);
285
+
286
+ predictor.updateCommunityPatterns(patterns);
287
+
288
+ const predictions = predictor.predict('/');
289
+
290
+ // Community prediction should be included
291
+ const popularPred = predictions.find((p) => p.route === '/popular');
292
+ expect(popularPred).toBeDefined();
293
+ });
294
+ });
295
+
296
+ describe('AeonNavigationEngine', () => {
297
+ let navigator: AeonNavigationEngine;
298
+ let mockRouter: AeonRouter;
299
+
300
+ beforeEach(() => {
301
+ mockRouter = new AeonRouter({ routesDir: './test-pages' });
302
+
303
+ navigator = new AeonNavigationEngine({
304
+ router: mockRouter,
305
+ initialRoute: '/',
306
+ sessionFetcher: async (id) => ({
307
+ sessionId: id,
308
+ route: `/${id}`,
309
+ tree: { type: 'div' },
310
+ data: {},
311
+ schemaVersion: '1.0.0',
312
+ cachedAt: Date.now(),
313
+ }),
314
+ });
315
+ });
316
+
317
+ test('initializes with current route', () => {
318
+ const state = navigator.getState();
319
+ expect(state.current).toBe('/');
320
+ expect(state.isNavigating).toBe(false);
321
+ });
322
+
323
+ test('prefetches routes', async () => {
324
+ // Mock the router's match method to return a valid match
325
+ const originalMatch = mockRouter.match.bind(mockRouter);
326
+ mockRouter.match = (path: string) => {
327
+ if (path === '/about') {
328
+ return {
329
+ sessionId: 'about',
330
+ componentId: 'About',
331
+ pattern: '/about',
332
+ params: {},
333
+ };
334
+ }
335
+ return originalMatch(path);
336
+ };
337
+
338
+ await navigator.prefetch('/about');
339
+
340
+ expect(navigator.isPreloaded('/about')).toBe(true);
341
+ });
342
+
343
+ test('subscribes to navigation changes', async () => {
344
+ const states: any[] = [];
345
+ const unsubscribe = navigator.subscribe((state) => states.push(state));
346
+
347
+ // Trigger a state change
348
+ (navigator as any).state.isNavigating = true;
349
+ (navigator as any).notifyListeners();
350
+
351
+ expect(states.length).toBeGreaterThan(0);
352
+ unsubscribe();
353
+ });
354
+
355
+ test('tracks navigation history', () => {
356
+ const state = navigator.getState();
357
+ expect(state.history).toContain('/');
358
+ });
359
+
360
+ test('provides cache statistics', () => {
361
+ const stats = navigator.getCacheStats();
362
+
363
+ expect(stats).toHaveProperty('size');
364
+ expect(stats).toHaveProperty('hitRate');
365
+ expect(stats).toHaveProperty('totalBytes');
366
+ });
367
+
368
+ test('predicts next routes based on history', () => {
369
+ // Record some navigation history
370
+ (navigator as any).recordNavigation('/', '/about');
371
+ (navigator as any).recordNavigation('/', '/about');
372
+ (navigator as any).recordNavigation('/', '/blog');
373
+
374
+ const predictions = navigator.predict('/');
375
+
376
+ expect(predictions.length).toBeGreaterThan(0);
377
+ expect(predictions[0].route).toBe('/about');
378
+ });
379
+ });
380
+
381
+ describe('Global singletons', () => {
382
+ test('getNavigationCache returns singleton', () => {
383
+ const cache1 = getNavigationCache();
384
+ const cache2 = getNavigationCache();
385
+ expect(cache1).toBe(cache2);
386
+ });
387
+
388
+ test('setNavigationCache replaces singleton', () => {
389
+ const originalCache = getNavigationCache();
390
+ const newCache = new NavigationCache();
391
+
392
+ setNavigationCache(newCache);
393
+ expect(getNavigationCache()).toBe(newCache);
394
+
395
+ // Restore original
396
+ setNavigationCache(originalCache);
397
+ });
398
+
399
+ test('getPredictor returns singleton', () => {
400
+ const pred1 = getPredictor();
401
+ const pred2 = getPredictor();
402
+ expect(pred1).toBe(pred2);
403
+ });
404
+
405
+ test('setPredictor replaces singleton', () => {
406
+ const originalPredictor = getPredictor();
407
+ const newPredictor = new NavigationPredictor();
408
+
409
+ setPredictor(newPredictor);
410
+ expect(getPredictor()).toBe(newPredictor);
411
+
412
+ // Restore original
413
+ setPredictor(originalPredictor);
414
+ });
415
+
416
+ test('getNavigator returns singleton', () => {
417
+ const nav1 = getNavigator();
418
+ const nav2 = getNavigator();
419
+ expect(nav1).toBe(nav2);
420
+ });
421
+ });