@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,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speculation Manager
|
|
3
|
+
*
|
|
4
|
+
* Client-side prefetching and prerendering for instant navigation.
|
|
5
|
+
* Uses the Speculation Rules API when available, with fallback to link prefetch.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export interface SpeculationOptions {
|
|
13
|
+
/** Maximum paths to prefetch */
|
|
14
|
+
maxPrefetch?: number;
|
|
15
|
+
|
|
16
|
+
/** Maximum paths to prerender (more expensive) */
|
|
17
|
+
maxPrerender?: number;
|
|
18
|
+
|
|
19
|
+
/** Prefetch on hover delay (ms) */
|
|
20
|
+
hoverDelay?: number;
|
|
21
|
+
|
|
22
|
+
/** Prefetch on viewport intersection */
|
|
23
|
+
prefetchOnVisible?: boolean;
|
|
24
|
+
|
|
25
|
+
/** Intersection observer threshold */
|
|
26
|
+
visibilityThreshold?: number;
|
|
27
|
+
|
|
28
|
+
/** Cache duration for prefetched resources (ms) */
|
|
29
|
+
cacheDuration?: number;
|
|
30
|
+
|
|
31
|
+
/** Callback when speculation occurs */
|
|
32
|
+
onSpeculate?: (path: string, type: 'prefetch' | 'prerender') => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SpeculationState {
|
|
36
|
+
prefetched: Set<string>;
|
|
37
|
+
prerendered: Set<string>;
|
|
38
|
+
pending: Set<string>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Feature Detection
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
function supportsSpeculationRules(): boolean {
|
|
46
|
+
if (typeof document === 'undefined') return false;
|
|
47
|
+
return (
|
|
48
|
+
'supports' in HTMLScriptElement &&
|
|
49
|
+
HTMLScriptElement.supports?.('speculationrules')
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function supportsLinkPrefetch(): boolean {
|
|
54
|
+
if (typeof document === 'undefined') return false;
|
|
55
|
+
const link = document.createElement('link');
|
|
56
|
+
return link.relList?.supports?.('prefetch') ?? false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Speculation Rules API
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Add speculation rules to the document
|
|
65
|
+
*/
|
|
66
|
+
function addSpeculationRules(
|
|
67
|
+
prefetch: string[],
|
|
68
|
+
prerender: string[],
|
|
69
|
+
): HTMLScriptElement | null {
|
|
70
|
+
if (!supportsSpeculationRules()) return null;
|
|
71
|
+
|
|
72
|
+
const rules: Record<string, Array<{ urls: string[] }>> = {};
|
|
73
|
+
|
|
74
|
+
if (prefetch.length > 0) {
|
|
75
|
+
rules.prefetch = [{ urls: prefetch }];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (prerender.length > 0) {
|
|
79
|
+
rules.prerender = [{ urls: prerender }];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (Object.keys(rules).length === 0) return null;
|
|
83
|
+
|
|
84
|
+
const script = document.createElement('script');
|
|
85
|
+
script.type = 'speculationrules';
|
|
86
|
+
script.textContent = JSON.stringify(rules);
|
|
87
|
+
document.head.appendChild(script);
|
|
88
|
+
|
|
89
|
+
return script;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Remove speculation rules script
|
|
94
|
+
*/
|
|
95
|
+
function removeSpeculationRules(script: HTMLScriptElement): void {
|
|
96
|
+
script.remove();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// Link Prefetch Fallback
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Prefetch a path using <link rel="prefetch">
|
|
105
|
+
*/
|
|
106
|
+
function linkPrefetch(path: string): HTMLLinkElement | null {
|
|
107
|
+
if (!supportsLinkPrefetch()) return null;
|
|
108
|
+
|
|
109
|
+
// Check if already prefetched
|
|
110
|
+
const existing = document.querySelector(
|
|
111
|
+
`link[rel="prefetch"][href="${path}"]`,
|
|
112
|
+
);
|
|
113
|
+
if (existing) return existing as HTMLLinkElement;
|
|
114
|
+
|
|
115
|
+
const link = document.createElement('link');
|
|
116
|
+
link.rel = 'prefetch';
|
|
117
|
+
link.href = path;
|
|
118
|
+
document.head.appendChild(link);
|
|
119
|
+
|
|
120
|
+
return link;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Remove prefetch link
|
|
125
|
+
*/
|
|
126
|
+
function removePrefetch(link: HTMLLinkElement): void {
|
|
127
|
+
link.remove();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// Speculation Manager
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
export class SpeculationManager {
|
|
135
|
+
private options: Required<SpeculationOptions>;
|
|
136
|
+
private state: SpeculationState;
|
|
137
|
+
private observers: Map<Element, IntersectionObserver> = new Map();
|
|
138
|
+
private hoverTimers: Map<Element, ReturnType<typeof setTimeout>> = new Map();
|
|
139
|
+
private speculationScript: HTMLScriptElement | null = null;
|
|
140
|
+
private prefetchLinks: Map<string, HTMLLinkElement> = new Map();
|
|
141
|
+
|
|
142
|
+
constructor(options: SpeculationOptions = {}) {
|
|
143
|
+
this.options = {
|
|
144
|
+
maxPrefetch: options.maxPrefetch ?? 5,
|
|
145
|
+
maxPrerender: options.maxPrerender ?? 1,
|
|
146
|
+
hoverDelay: options.hoverDelay ?? 100,
|
|
147
|
+
prefetchOnVisible: options.prefetchOnVisible ?? true,
|
|
148
|
+
visibilityThreshold: options.visibilityThreshold ?? 0.1,
|
|
149
|
+
cacheDuration: options.cacheDuration ?? 5 * 60 * 1000, // 5 minutes
|
|
150
|
+
onSpeculate: options.onSpeculate ?? (() => {}),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
this.state = {
|
|
154
|
+
prefetched: new Set(),
|
|
155
|
+
prerendered: new Set(),
|
|
156
|
+
pending: new Set(),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Initialize speculation from server hints
|
|
162
|
+
*/
|
|
163
|
+
initFromHints(prefetch: string[] = [], prerender: string[] = []): void {
|
|
164
|
+
// Filter out already handled paths
|
|
165
|
+
const newPrefetch = prefetch
|
|
166
|
+
.filter(
|
|
167
|
+
(p) => !this.state.prefetched.has(p) && !this.state.prerendered.has(p),
|
|
168
|
+
)
|
|
169
|
+
.slice(0, this.options.maxPrefetch);
|
|
170
|
+
|
|
171
|
+
const newPrerender = prerender
|
|
172
|
+
.filter((p) => !this.state.prerendered.has(p))
|
|
173
|
+
.slice(0, this.options.maxPrerender);
|
|
174
|
+
|
|
175
|
+
// Try Speculation Rules API first
|
|
176
|
+
if (supportsSpeculationRules()) {
|
|
177
|
+
this.speculationScript = addSpeculationRules(newPrefetch, newPrerender);
|
|
178
|
+
|
|
179
|
+
newPrefetch.forEach((p) => {
|
|
180
|
+
this.state.prefetched.add(p);
|
|
181
|
+
this.options.onSpeculate(p, 'prefetch');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
newPrerender.forEach((p) => {
|
|
185
|
+
this.state.prerendered.add(p);
|
|
186
|
+
this.options.onSpeculate(p, 'prerender');
|
|
187
|
+
});
|
|
188
|
+
} else {
|
|
189
|
+
// Fallback to link prefetch (no prerender fallback)
|
|
190
|
+
newPrefetch.forEach((path) => {
|
|
191
|
+
const link = linkPrefetch(path);
|
|
192
|
+
if (link) {
|
|
193
|
+
this.prefetchLinks.set(path, link);
|
|
194
|
+
this.state.prefetched.add(path);
|
|
195
|
+
this.options.onSpeculate(path, 'prefetch');
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Prefetch a specific path
|
|
203
|
+
*/
|
|
204
|
+
prefetch(path: string): boolean {
|
|
205
|
+
if (this.state.prefetched.has(path) || this.state.prerendered.has(path)) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (this.state.prefetched.size >= this.options.maxPrefetch) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (supportsSpeculationRules()) {
|
|
214
|
+
// Update speculation rules
|
|
215
|
+
const allPrefetch = [...this.state.prefetched, path];
|
|
216
|
+
const allPrerender = [...this.state.prerendered];
|
|
217
|
+
|
|
218
|
+
if (this.speculationScript) {
|
|
219
|
+
removeSpeculationRules(this.speculationScript);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
this.speculationScript = addSpeculationRules(allPrefetch, allPrerender);
|
|
223
|
+
} else {
|
|
224
|
+
const link = linkPrefetch(path);
|
|
225
|
+
if (link) {
|
|
226
|
+
this.prefetchLinks.set(path, link);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this.state.prefetched.add(path);
|
|
231
|
+
this.options.onSpeculate(path, 'prefetch');
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Watch an element for hover to prefetch its href
|
|
237
|
+
*/
|
|
238
|
+
watchHover(element: HTMLAnchorElement): () => void {
|
|
239
|
+
const path = new URL(element.href, window.location.href).pathname;
|
|
240
|
+
|
|
241
|
+
const handleMouseEnter = () => {
|
|
242
|
+
if (this.state.prefetched.has(path) || this.state.pending.has(path)) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
this.state.pending.add(path);
|
|
247
|
+
|
|
248
|
+
const timer = setTimeout(() => {
|
|
249
|
+
this.prefetch(path);
|
|
250
|
+
this.state.pending.delete(path);
|
|
251
|
+
}, this.options.hoverDelay);
|
|
252
|
+
|
|
253
|
+
this.hoverTimers.set(element, timer);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const handleMouseLeave = () => {
|
|
257
|
+
const timer = this.hoverTimers.get(element);
|
|
258
|
+
if (timer) {
|
|
259
|
+
clearTimeout(timer);
|
|
260
|
+
this.hoverTimers.delete(element);
|
|
261
|
+
}
|
|
262
|
+
this.state.pending.delete(path);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
element.addEventListener('mouseenter', handleMouseEnter);
|
|
266
|
+
element.addEventListener('mouseleave', handleMouseLeave);
|
|
267
|
+
|
|
268
|
+
return () => {
|
|
269
|
+
element.removeEventListener('mouseenter', handleMouseEnter);
|
|
270
|
+
element.removeEventListener('mouseleave', handleMouseLeave);
|
|
271
|
+
handleMouseLeave();
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Watch an element for visibility to prefetch its href
|
|
277
|
+
*/
|
|
278
|
+
watchVisible(element: HTMLAnchorElement): () => void {
|
|
279
|
+
if (!this.options.prefetchOnVisible) {
|
|
280
|
+
return () => {};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const path = new URL(element.href, window.location.href).pathname;
|
|
284
|
+
|
|
285
|
+
const observer = new IntersectionObserver(
|
|
286
|
+
(entries) => {
|
|
287
|
+
entries.forEach((entry) => {
|
|
288
|
+
if (entry.isIntersecting) {
|
|
289
|
+
this.prefetch(path);
|
|
290
|
+
observer.disconnect();
|
|
291
|
+
this.observers.delete(element);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
{ threshold: this.options.visibilityThreshold },
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
observer.observe(element);
|
|
299
|
+
this.observers.set(element, observer);
|
|
300
|
+
|
|
301
|
+
return () => {
|
|
302
|
+
observer.disconnect();
|
|
303
|
+
this.observers.delete(element);
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Auto-watch all internal links on the page
|
|
309
|
+
*/
|
|
310
|
+
watchAllLinks(): () => void {
|
|
311
|
+
const links = document.querySelectorAll('a[href^="/"]');
|
|
312
|
+
const cleanups: Array<() => void> = [];
|
|
313
|
+
|
|
314
|
+
links.forEach((link) => {
|
|
315
|
+
if (link instanceof HTMLAnchorElement) {
|
|
316
|
+
cleanups.push(this.watchHover(link));
|
|
317
|
+
cleanups.push(this.watchVisible(link));
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return () => {
|
|
322
|
+
cleanups.forEach((cleanup) => cleanup());
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Clear all speculation state
|
|
328
|
+
*/
|
|
329
|
+
clear(): void {
|
|
330
|
+
// Clear speculation rules
|
|
331
|
+
if (this.speculationScript) {
|
|
332
|
+
removeSpeculationRules(this.speculationScript);
|
|
333
|
+
this.speculationScript = null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Clear prefetch links
|
|
337
|
+
this.prefetchLinks.forEach((link) => removePrefetch(link));
|
|
338
|
+
this.prefetchLinks.clear();
|
|
339
|
+
|
|
340
|
+
// Clear observers
|
|
341
|
+
this.observers.forEach((observer) => observer.disconnect());
|
|
342
|
+
this.observers.clear();
|
|
343
|
+
|
|
344
|
+
// Clear timers
|
|
345
|
+
this.hoverTimers.forEach((timer) => clearTimeout(timer));
|
|
346
|
+
this.hoverTimers.clear();
|
|
347
|
+
|
|
348
|
+
// Reset state
|
|
349
|
+
this.state.prefetched.clear();
|
|
350
|
+
this.state.prerendered.clear();
|
|
351
|
+
this.state.pending.clear();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get current speculation state
|
|
356
|
+
*/
|
|
357
|
+
getState(): Readonly<SpeculationState> {
|
|
358
|
+
return {
|
|
359
|
+
prefetched: new Set(this.state.prefetched),
|
|
360
|
+
prerendered: new Set(this.state.prerendered),
|
|
361
|
+
pending: new Set(this.state.pending),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ============================================================================
|
|
367
|
+
// React Hook
|
|
368
|
+
// ============================================================================
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* React hook for speculation management
|
|
372
|
+
* Usage: const speculation = useSpeculation({ maxPrefetch: 5 });
|
|
373
|
+
*/
|
|
374
|
+
export function createSpeculationHook(
|
|
375
|
+
useState: <T>(initial: T) => [T, (v: T) => void],
|
|
376
|
+
useEffect: (effect: () => void | (() => void), deps: unknown[]) => void,
|
|
377
|
+
useRef: <T>(initial: T) => { current: T },
|
|
378
|
+
) {
|
|
379
|
+
return function useSpeculation(options: SpeculationOptions = {}) {
|
|
380
|
+
const managerRef = useRef<SpeculationManager | null>(null);
|
|
381
|
+
const [state, setState] = useState<SpeculationState>({
|
|
382
|
+
prefetched: new Set(),
|
|
383
|
+
prerendered: new Set(),
|
|
384
|
+
pending: new Set(),
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
managerRef.current = new SpeculationManager({
|
|
389
|
+
...options,
|
|
390
|
+
onSpeculate: (path, type) => {
|
|
391
|
+
options.onSpeculate?.(path, type);
|
|
392
|
+
setState(managerRef.current!.getState());
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Auto-watch all links
|
|
397
|
+
const cleanup = managerRef.current.watchAllLinks();
|
|
398
|
+
|
|
399
|
+
return () => {
|
|
400
|
+
cleanup();
|
|
401
|
+
managerRef.current?.clear();
|
|
402
|
+
};
|
|
403
|
+
}, []);
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
state,
|
|
407
|
+
prefetch: (path: string) => managerRef.current?.prefetch(path),
|
|
408
|
+
initFromHints: (prefetch: string[], prerender: string[]) =>
|
|
409
|
+
managerRef.current?.initFromHints(prefetch, prerender),
|
|
410
|
+
clear: () => managerRef.current?.clear(),
|
|
411
|
+
};
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ============================================================================
|
|
416
|
+
// Auto-init for non-React usage
|
|
417
|
+
// ============================================================================
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Auto-initialize speculation from window.__AEON_SPECULATION__ hints
|
|
421
|
+
*/
|
|
422
|
+
export function autoInitSpeculation(): SpeculationManager | null {
|
|
423
|
+
if (typeof window === 'undefined') return null;
|
|
424
|
+
|
|
425
|
+
// @ts-expect-error - Global hint injection
|
|
426
|
+
const hints = window.__AEON_SPECULATION__ as
|
|
427
|
+
| { prefetch?: string[]; prerender?: string[] }
|
|
428
|
+
| undefined;
|
|
429
|
+
|
|
430
|
+
const manager = new SpeculationManager();
|
|
431
|
+
|
|
432
|
+
if (hints) {
|
|
433
|
+
manager.initFromHints(hints.prefetch || [], hints.prerender || []);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Watch all links
|
|
437
|
+
manager.watchAllLinks();
|
|
438
|
+
|
|
439
|
+
return manager;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ============================================================================
|
|
443
|
+
// Exports
|
|
444
|
+
// ============================================================================
|
|
445
|
+
|
|
446
|
+
export {
|
|
447
|
+
supportsSpeculationRules,
|
|
448
|
+
supportsLinkPrefetch,
|
|
449
|
+
addSpeculationRules,
|
|
450
|
+
linkPrefetch,
|
|
451
|
+
};
|