@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.
- package/CHANGELOG.md +112 -0
- package/README.md +625 -0
- package/examples/basic/aeon.config.ts +39 -0
- package/examples/basic/components/Cursor.tsx +86 -0
- package/examples/basic/components/OfflineIndicator.tsx +103 -0
- package/examples/basic/components/PresenceBar.tsx +77 -0
- package/examples/basic/package.json +20 -0
- package/examples/basic/pages/index.tsx +80 -0
- package/package.json +101 -0
- package/packages/analytics/README.md +309 -0
- package/packages/analytics/build.ts +35 -0
- package/packages/analytics/package.json +50 -0
- package/packages/analytics/src/click-tracker.ts +368 -0
- package/packages/analytics/src/context-bridge.ts +319 -0
- package/packages/analytics/src/data-layer.ts +302 -0
- package/packages/analytics/src/gtm-loader.ts +239 -0
- package/packages/analytics/src/index.ts +230 -0
- package/packages/analytics/src/merkle-tree.ts +489 -0
- package/packages/analytics/src/provider.tsx +300 -0
- package/packages/analytics/src/types.ts +320 -0
- package/packages/analytics/src/use-analytics.ts +296 -0
- package/packages/analytics/tsconfig.json +19 -0
- package/packages/benchmarks/src/benchmark.test.ts +691 -0
- package/packages/cli/dist/index.js +61899 -0
- package/packages/cli/package.json +43 -0
- package/packages/cli/src/commands/build.test.ts +682 -0
- package/packages/cli/src/commands/build.ts +890 -0
- package/packages/cli/src/commands/dev.ts +473 -0
- package/packages/cli/src/commands/init.ts +409 -0
- package/packages/cli/src/commands/start.ts +297 -0
- package/packages/cli/src/index.ts +105 -0
- package/packages/directives/src/use-aeon.ts +272 -0
- package/packages/mcp-server/package.json +51 -0
- package/packages/mcp-server/src/index.ts +178 -0
- package/packages/mcp-server/src/resources.ts +346 -0
- package/packages/mcp-server/src/tools/index.ts +36 -0
- package/packages/mcp-server/src/tools/navigation.ts +545 -0
- package/packages/mcp-server/tsconfig.json +21 -0
- package/packages/react/package.json +40 -0
- package/packages/react/src/Link.tsx +388 -0
- package/packages/react/src/components/InstallPrompt.tsx +286 -0
- package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
- package/packages/react/src/components/PushNotifications.tsx +453 -0
- package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
- package/packages/react/src/hooks/useConflicts.ts +277 -0
- package/packages/react/src/hooks/useNetworkState.ts +209 -0
- package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
- package/packages/react/src/hooks/useServiceWorker.ts +278 -0
- package/packages/react/src/hooks.ts +195 -0
- package/packages/react/src/index.ts +151 -0
- package/packages/react/src/provider.tsx +467 -0
- package/packages/react/tsconfig.json +19 -0
- package/packages/runtime/README.md +399 -0
- package/packages/runtime/build.ts +48 -0
- package/packages/runtime/package.json +71 -0
- package/packages/runtime/schema.sql +40 -0
- package/packages/runtime/src/api-routes.ts +465 -0
- package/packages/runtime/src/benchmark.ts +171 -0
- package/packages/runtime/src/cache.ts +479 -0
- package/packages/runtime/src/durable-object.ts +1341 -0
- package/packages/runtime/src/index.ts +360 -0
- package/packages/runtime/src/navigation.test.ts +421 -0
- package/packages/runtime/src/navigation.ts +422 -0
- package/packages/runtime/src/nextjs-adapter.ts +272 -0
- package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
- package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
- package/packages/runtime/src/offline/encryption.test.ts +412 -0
- package/packages/runtime/src/offline/encryption.ts +397 -0
- package/packages/runtime/src/offline/types.ts +465 -0
- package/packages/runtime/src/predictor.ts +371 -0
- package/packages/runtime/src/registry.ts +351 -0
- package/packages/runtime/src/router/context-extractor.ts +661 -0
- package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
- package/packages/runtime/src/router/esi-control.ts +541 -0
- package/packages/runtime/src/router/esi-cyrano.ts +779 -0
- package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
- package/packages/runtime/src/router/esi-react.tsx +1065 -0
- package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
- package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
- package/packages/runtime/src/router/esi-translate.ts +503 -0
- package/packages/runtime/src/router/esi.ts +666 -0
- package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
- package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
- package/packages/runtime/src/router/index.ts +298 -0
- package/packages/runtime/src/router/merkle-capability.ts +473 -0
- package/packages/runtime/src/router/speculation.ts +451 -0
- package/packages/runtime/src/router/types.ts +630 -0
- package/packages/runtime/src/router.test.ts +470 -0
- package/packages/runtime/src/router.ts +302 -0
- package/packages/runtime/src/server.ts +481 -0
- package/packages/runtime/src/service-worker-push.ts +319 -0
- package/packages/runtime/src/service-worker.ts +553 -0
- package/packages/runtime/src/skeleton-hydrate.ts +237 -0
- package/packages/runtime/src/speculation.test.ts +389 -0
- package/packages/runtime/src/speculation.ts +486 -0
- package/packages/runtime/src/storage.test.ts +1297 -0
- package/packages/runtime/src/storage.ts +1048 -0
- package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
- package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
- package/packages/runtime/src/sync/coordinator.test.ts +608 -0
- package/packages/runtime/src/sync/coordinator.ts +596 -0
- package/packages/runtime/src/tree-compiler.ts +295 -0
- package/packages/runtime/src/types.ts +728 -0
- package/packages/runtime/src/worker.ts +327 -0
- package/packages/runtime/tsconfig.json +20 -0
- package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
- package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
- package/packages/runtime/wasm/package.json +21 -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 +480 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -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 +192 -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 +191 -0
- package/packages/runtime-wasm/src/render.rs +629 -0
- package/packages/runtime-wasm/src/router.rs +298 -0
- package/packages/runtime-wasm/src/skeleton.rs +430 -0
- 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
|
+
});
|