@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.
- package/README.md +438 -0
- package/examples/basic/aeon.config.ts +39 -0
- package/examples/basic/components/Cursor.tsx +88 -0
- package/examples/basic/components/OfflineIndicator.tsx +93 -0
- package/examples/basic/components/PresenceBar.tsx +68 -0
- package/examples/basic/package.json +20 -0
- package/examples/basic/pages/index.tsx +73 -0
- package/package.json +90 -0
- package/packages/benchmarks/src/benchmark.test.ts +644 -0
- package/packages/cli/package.json +43 -0
- package/packages/cli/src/commands/build.test.ts +649 -0
- package/packages/cli/src/commands/build.ts +853 -0
- package/packages/cli/src/commands/dev.ts +463 -0
- package/packages/cli/src/commands/init.ts +395 -0
- package/packages/cli/src/commands/start.ts +289 -0
- package/packages/cli/src/index.ts +102 -0
- package/packages/directives/src/use-aeon.ts +266 -0
- package/packages/react/package.json +34 -0
- package/packages/react/src/Link.tsx +355 -0
- package/packages/react/src/hooks/useAeonNavigation.ts +204 -0
- package/packages/react/src/hooks/usePilotNavigation.ts +253 -0
- package/packages/react/src/hooks/useServiceWorker.ts +276 -0
- package/packages/react/src/hooks.ts +192 -0
- package/packages/react/src/index.ts +89 -0
- package/packages/react/src/provider.tsx +428 -0
- package/packages/runtime/package.json +70 -0
- package/packages/runtime/schema.sql +40 -0
- package/packages/runtime/src/api-routes.ts +453 -0
- package/packages/runtime/src/benchmark.ts +145 -0
- package/packages/runtime/src/cache.ts +287 -0
- package/packages/runtime/src/durable-object.ts +847 -0
- package/packages/runtime/src/index.ts +235 -0
- package/packages/runtime/src/navigation.test.ts +432 -0
- package/packages/runtime/src/navigation.ts +412 -0
- package/packages/runtime/src/nextjs-adapter.ts +254 -0
- package/packages/runtime/src/predictor.ts +368 -0
- package/packages/runtime/src/registry.ts +339 -0
- package/packages/runtime/src/router/context-extractor.ts +394 -0
- package/packages/runtime/src/router/esi-control-react.tsx +1172 -0
- package/packages/runtime/src/router/esi-control.ts +488 -0
- package/packages/runtime/src/router/esi-react.tsx +600 -0
- package/packages/runtime/src/router/esi.ts +595 -0
- package/packages/runtime/src/router/heuristic-adapter.test.ts +272 -0
- package/packages/runtime/src/router/heuristic-adapter.ts +544 -0
- package/packages/runtime/src/router/index.ts +158 -0
- package/packages/runtime/src/router/speculation.ts +442 -0
- package/packages/runtime/src/router/types.ts +514 -0
- package/packages/runtime/src/router.test.ts +466 -0
- package/packages/runtime/src/router.ts +285 -0
- package/packages/runtime/src/server.ts +446 -0
- package/packages/runtime/src/service-worker.ts +418 -0
- package/packages/runtime/src/speculation.test.ts +360 -0
- package/packages/runtime/src/speculation.ts +456 -0
- package/packages/runtime/src/storage.test.ts +1201 -0
- package/packages/runtime/src/storage.ts +1031 -0
- package/packages/runtime/src/tree-compiler.ts +252 -0
- package/packages/runtime/src/types.ts +444 -0
- package/packages/runtime/src/worker.ts +300 -0
- package/packages/runtime/tsconfig.json +19 -0
- package/packages/runtime/wrangler.toml +41 -0
- package/packages/runtime-wasm/Cargo.lock +436 -0
- package/packages/runtime-wasm/Cargo.toml +29 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +328 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1267 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +73 -0
- package/packages/runtime-wasm/pkg/package.json +21 -0
- package/packages/runtime-wasm/src/hydrate.rs +352 -0
- package/packages/runtime-wasm/src/lib.rs +189 -0
- package/packages/runtime-wasm/src/render.rs +629 -0
- package/packages/runtime-wasm/src/router.rs +298 -0
- 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
|
+
});
|