@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,360 @@
1
+ import { describe, test, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
2
+ import {
3
+ SpeculativeRenderer,
4
+ getSpeculativeRenderer,
5
+ setSpeculativeRenderer,
6
+ initSpeculativeRendering,
7
+ type SpeculativeRendererConfig,
8
+ } from './speculation';
9
+ import { setPredictor, NavigationPredictor } from './predictor';
10
+
11
+ // Mock DOM environment
12
+ function createMockDOM() {
13
+ return {
14
+ querySelectorAll: mock(() => []),
15
+ addEventListener: mock(() => {}),
16
+ createElement: mock(() => ({
17
+ type: '',
18
+ textContent: '',
19
+ })),
20
+ head: {
21
+ appendChild: mock(() => {}),
22
+ },
23
+ open: mock(() => {}),
24
+ write: mock(() => {}),
25
+ close: mock(() => {}),
26
+ };
27
+ }
28
+
29
+ function createMockWindow() {
30
+ return {
31
+ location: { pathname: '/', origin: 'http://localhost' },
32
+ history: {
33
+ pushState: mock(() => {}),
34
+ },
35
+ addEventListener: mock(() => {}),
36
+ IntersectionObserver: mock((callback: Function, options: any) => ({
37
+ observe: mock(() => {}),
38
+ unobserve: mock(() => {}),
39
+ disconnect: mock(() => {}),
40
+ })),
41
+ };
42
+ }
43
+
44
+ describe('SpeculativeRenderer', () => {
45
+ let originalWindow: typeof globalThis.window;
46
+ let originalDocument: typeof globalThis.document;
47
+ let originalFetch: typeof globalThis.fetch;
48
+
49
+ beforeEach(() => {
50
+ originalWindow = globalThis.window;
51
+ originalDocument = globalThis.document;
52
+ originalFetch = globalThis.fetch;
53
+
54
+ // Set up a mock predictor
55
+ const predictor = new NavigationPredictor();
56
+ setPredictor(predictor);
57
+ });
58
+
59
+ afterEach(() => {
60
+ globalThis.window = originalWindow;
61
+ globalThis.document = originalDocument;
62
+ globalThis.fetch = originalFetch;
63
+ setSpeculativeRenderer(null as any);
64
+ });
65
+
66
+ test('creates instance with default config', () => {
67
+ const renderer = new SpeculativeRenderer();
68
+ expect(renderer).toBeDefined();
69
+
70
+ const stats = renderer.getStats();
71
+ expect(stats.cachedPages).toBe(0);
72
+ expect(stats.cacheSize).toBe(0);
73
+ });
74
+
75
+ test('creates instance with custom config', () => {
76
+ const config: Partial<SpeculativeRendererConfig> = {
77
+ maxCachedPages: 10,
78
+ maxCacheSize: 10 * 1024 * 1024,
79
+ minConfidence: 0.5,
80
+ };
81
+
82
+ const renderer = new SpeculativeRenderer(config);
83
+ expect(renderer).toBeDefined();
84
+ });
85
+
86
+ test('getSpeculativeRenderer returns singleton', () => {
87
+ const renderer1 = getSpeculativeRenderer();
88
+ const renderer2 = getSpeculativeRenderer();
89
+ expect(renderer1).toBe(renderer2);
90
+ });
91
+
92
+ test('setSpeculativeRenderer replaces singleton', () => {
93
+ const original = getSpeculativeRenderer();
94
+ const replacement = new SpeculativeRenderer();
95
+
96
+ setSpeculativeRenderer(replacement);
97
+ expect(getSpeculativeRenderer()).toBe(replacement);
98
+ expect(getSpeculativeRenderer()).not.toBe(original);
99
+ });
100
+
101
+ test('prerender caches page HTML', async () => {
102
+ const mockHtml = '<html><body>Test Page</body></html>';
103
+
104
+ globalThis.fetch = mock(async () => ({
105
+ ok: true,
106
+ text: async () => mockHtml,
107
+ } as Response));
108
+
109
+ globalThis.window = {
110
+ location: { pathname: '/' },
111
+ } as any;
112
+
113
+ const renderer = new SpeculativeRenderer();
114
+ const result = await renderer.prerender('/about');
115
+
116
+ expect(result).toBe(true);
117
+ expect(renderer.getStats().cachedPages).toBe(1);
118
+ expect(renderer.getStats().cacheSize).toBe(mockHtml.length);
119
+ });
120
+
121
+ test('prerender skips current route', async () => {
122
+ globalThis.window = {
123
+ location: { pathname: '/about' },
124
+ } as any;
125
+
126
+ const renderer = new SpeculativeRenderer();
127
+ const result = await renderer.prerender('/about');
128
+
129
+ expect(result).toBe(false);
130
+ expect(renderer.getStats().cachedPages).toBe(0);
131
+ });
132
+
133
+ test('prerender handles fetch failures', async () => {
134
+ globalThis.fetch = mock(async () => ({
135
+ ok: false,
136
+ status: 404,
137
+ } as Response));
138
+
139
+ globalThis.window = {
140
+ location: { pathname: '/' },
141
+ } as any;
142
+
143
+ const renderer = new SpeculativeRenderer();
144
+ const result = await renderer.prerender('/not-found');
145
+
146
+ expect(result).toBe(false);
147
+ expect(renderer.getStats().cachedPages).toBe(0);
148
+ });
149
+
150
+ test('prerender handles network errors', async () => {
151
+ globalThis.fetch = mock(async () => {
152
+ throw new Error('Network error');
153
+ });
154
+
155
+ globalThis.window = {
156
+ location: { pathname: '/' },
157
+ } as any;
158
+
159
+ const renderer = new SpeculativeRenderer();
160
+ const result = await renderer.prerender('/error');
161
+
162
+ expect(result).toBe(false);
163
+ expect(renderer.getStats().cachedPages).toBe(0);
164
+ });
165
+
166
+ test('invalidate marks pages as stale', async () => {
167
+ const mockHtml = '<html><body>Test</body></html>';
168
+
169
+ globalThis.fetch = mock(async () => ({
170
+ ok: true,
171
+ text: async () => mockHtml,
172
+ } as Response));
173
+
174
+ globalThis.window = {
175
+ location: { pathname: '/' },
176
+ } as any;
177
+
178
+ const renderer = new SpeculativeRenderer();
179
+ await renderer.prerender('/about');
180
+ await renderer.prerender('/contact');
181
+
182
+ // Invalidate specific routes
183
+ renderer.invalidate(['/about']);
184
+
185
+ // About should be stale, navigate should fail
186
+ const navigateResult = await renderer.navigate('/about');
187
+ expect(navigateResult).toBe(false);
188
+ });
189
+
190
+ test('invalidate without routes marks all as stale', async () => {
191
+ const mockHtml = '<html><body>Test</body></html>';
192
+
193
+ globalThis.fetch = mock(async () => ({
194
+ ok: true,
195
+ text: async () => mockHtml,
196
+ } as Response));
197
+
198
+ globalThis.window = {
199
+ location: { pathname: '/' },
200
+ } as any;
201
+
202
+ const renderer = new SpeculativeRenderer();
203
+ await renderer.prerender('/about');
204
+ await renderer.prerender('/contact');
205
+
206
+ // Invalidate all
207
+ renderer.invalidate();
208
+
209
+ // Both should be stale
210
+ const aboutResult = await renderer.navigate('/about');
211
+ const contactResult = await renderer.navigate('/contact');
212
+
213
+ expect(aboutResult).toBe(false);
214
+ expect(contactResult).toBe(false);
215
+ });
216
+
217
+ test('evicts old pages when cache is full', async () => {
218
+ const mockHtml = 'x'.repeat(1000); // 1KB per page
219
+
220
+ globalThis.fetch = mock(async () => ({
221
+ ok: true,
222
+ text: async () => mockHtml,
223
+ } as Response));
224
+
225
+ globalThis.window = {
226
+ location: { pathname: '/' },
227
+ } as any;
228
+
229
+ // Small cache - max 2 pages
230
+ const renderer = new SpeculativeRenderer({
231
+ maxCachedPages: 2,
232
+ });
233
+
234
+ await renderer.prerender('/page1');
235
+ await renderer.prerender('/page2');
236
+
237
+ expect(renderer.getStats().cachedPages).toBe(2);
238
+
239
+ // Adding third page should evict oldest
240
+ await renderer.prerender('/page3');
241
+
242
+ expect(renderer.getStats().cachedPages).toBe(2);
243
+ });
244
+
245
+ test('evicts based on cache size limit', async () => {
246
+ globalThis.fetch = mock(async () => ({
247
+ ok: true,
248
+ text: async () => 'x'.repeat(1000), // 1KB
249
+ } as Response));
250
+
251
+ globalThis.window = {
252
+ location: { pathname: '/' },
253
+ } as any;
254
+
255
+ // Small cache - max 1.5KB
256
+ const renderer = new SpeculativeRenderer({
257
+ maxCacheSize: 1500,
258
+ maxCachedPages: 100,
259
+ });
260
+
261
+ await renderer.prerender('/page1');
262
+ await renderer.prerender('/page2');
263
+
264
+ // Should only keep one page due to size limit
265
+ expect(renderer.getStats().cachedPages).toBe(1);
266
+ expect(renderer.getStats().cacheSize).toBeLessThanOrEqual(1500);
267
+ });
268
+
269
+ test('uses cached page on second prerender', async () => {
270
+ let fetchCount = 0;
271
+ const mockHtml = '<html><body>Test</body></html>';
272
+
273
+ globalThis.fetch = mock(async () => {
274
+ fetchCount++;
275
+ return {
276
+ ok: true,
277
+ text: async () => mockHtml,
278
+ } as Response;
279
+ });
280
+
281
+ globalThis.window = {
282
+ location: { pathname: '/' },
283
+ } as any;
284
+
285
+ const renderer = new SpeculativeRenderer();
286
+
287
+ await renderer.prerender('/about');
288
+ expect(fetchCount).toBe(1);
289
+
290
+ // Second call should use cache
291
+ await renderer.prerender('/about');
292
+ expect(fetchCount).toBe(1); // No additional fetch
293
+ });
294
+
295
+ test('destroy cleans up resources', async () => {
296
+ const mockHtml = '<html><body>Test</body></html>';
297
+
298
+ globalThis.fetch = mock(async () => ({
299
+ ok: true,
300
+ text: async () => mockHtml,
301
+ } as Response));
302
+
303
+ globalThis.window = {
304
+ location: { pathname: '/' },
305
+ } as any;
306
+
307
+ const renderer = new SpeculativeRenderer();
308
+ await renderer.prerender('/about');
309
+
310
+ expect(renderer.getStats().cachedPages).toBe(1);
311
+
312
+ renderer.destroy();
313
+
314
+ expect(renderer.getStats().cachedPages).toBe(0);
315
+ expect(renderer.getStats().cacheSize).toBe(0);
316
+ });
317
+ });
318
+
319
+ describe('initSpeculativeRendering', () => {
320
+ test('creates and initializes renderer', () => {
321
+ // Can't fully test init without DOM, but verify function exists
322
+ expect(initSpeculativeRendering).toBeDefined();
323
+ expect(typeof initSpeculativeRendering).toBe('function');
324
+ });
325
+ });
326
+
327
+ describe('Speculation integration with predictor', () => {
328
+ test('predictor predictions are used for pre-rendering', () => {
329
+ const predictor = new NavigationPredictor();
330
+
331
+ // Record some navigation history
332
+ predictor.record({
333
+ from: '/',
334
+ to: '/dashboard',
335
+ timestamp: Date.now(),
336
+ duration: 5000,
337
+ });
338
+
339
+ predictor.record({
340
+ from: '/',
341
+ to: '/dashboard',
342
+ timestamp: Date.now(),
343
+ duration: 3000,
344
+ });
345
+
346
+ predictor.record({
347
+ from: '/',
348
+ to: '/explore',
349
+ timestamp: Date.now(),
350
+ duration: 2000,
351
+ });
352
+
353
+ // Predict from home
354
+ const predictions = predictor.predict('/');
355
+
356
+ expect(predictions.length).toBeGreaterThan(0);
357
+ expect(predictions[0].route).toBe('/dashboard'); // Most likely
358
+ expect(predictions[0].probability).toBeGreaterThan(0.4); // ~0.48 expected
359
+ });
360
+ });