@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,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Context Extractor
|
|
3
|
+
*
|
|
4
|
+
* Middleware that extracts UserContext from HTTP requests.
|
|
5
|
+
* Gathers signals from headers, cookies, and request properties.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ConnectionType,
|
|
10
|
+
EmotionState,
|
|
11
|
+
UserContext,
|
|
12
|
+
UserTier,
|
|
13
|
+
Viewport,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Cookie Helpers
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
function parseCookies(cookieHeader: string | null): Record<string, string> {
|
|
21
|
+
if (!cookieHeader) return {};
|
|
22
|
+
|
|
23
|
+
return cookieHeader.split(';').reduce((acc, cookie) => {
|
|
24
|
+
const [key, value] = cookie.trim().split('=');
|
|
25
|
+
if (key && value) {
|
|
26
|
+
acc[key] = decodeURIComponent(value);
|
|
27
|
+
}
|
|
28
|
+
return acc;
|
|
29
|
+
}, {} as Record<string, string>);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseJSON<T>(value: string | undefined, fallback: T): T {
|
|
33
|
+
if (!value) return fallback;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(value) as T;
|
|
36
|
+
} catch {
|
|
37
|
+
return fallback;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Header Extraction
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extract viewport from client hints or fallback headers
|
|
47
|
+
*/
|
|
48
|
+
function extractViewport(request: Request): Viewport {
|
|
49
|
+
const headers = request.headers;
|
|
50
|
+
|
|
51
|
+
// Client Hints (if available)
|
|
52
|
+
const viewportWidth = headers.get('sec-ch-viewport-width');
|
|
53
|
+
const viewportHeight = headers.get('sec-ch-viewport-height');
|
|
54
|
+
const dpr = headers.get('sec-ch-dpr');
|
|
55
|
+
|
|
56
|
+
if (viewportWidth && viewportHeight) {
|
|
57
|
+
return {
|
|
58
|
+
width: parseInt(viewportWidth, 10),
|
|
59
|
+
height: parseInt(viewportHeight, 10),
|
|
60
|
+
devicePixelRatio: dpr ? parseFloat(dpr) : undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fallback to custom headers (set by client-side JS)
|
|
65
|
+
const xViewport = headers.get('x-viewport');
|
|
66
|
+
if (xViewport) {
|
|
67
|
+
const [width, height, devicePixelRatio] = xViewport.split(',').map(Number);
|
|
68
|
+
return { width: width || 1920, height: height || 1080, devicePixelRatio };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Default desktop viewport
|
|
72
|
+
return { width: 1920, height: 1080 };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extract connection type from Network Information API hints
|
|
77
|
+
*/
|
|
78
|
+
function extractConnection(request: Request): ConnectionType {
|
|
79
|
+
const headers = request.headers;
|
|
80
|
+
|
|
81
|
+
// Downlink and RTT for connection quality
|
|
82
|
+
const downlink = headers.get('downlink');
|
|
83
|
+
const rtt = headers.get('rtt');
|
|
84
|
+
const ect = headers.get('ect'); // Effective Connection Type
|
|
85
|
+
|
|
86
|
+
// ECT header (if available)
|
|
87
|
+
if (ect) {
|
|
88
|
+
switch (ect) {
|
|
89
|
+
case '4g': return 'fast';
|
|
90
|
+
case '3g': return '3g';
|
|
91
|
+
case '2g': return '2g';
|
|
92
|
+
case 'slow-2g': return 'slow-2g';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Infer from downlink (Mbps)
|
|
97
|
+
if (downlink) {
|
|
98
|
+
const mbps = parseFloat(downlink);
|
|
99
|
+
if (mbps >= 10) return 'fast';
|
|
100
|
+
if (mbps >= 2) return '4g';
|
|
101
|
+
if (mbps >= 0.5) return '3g';
|
|
102
|
+
if (mbps >= 0.1) return '2g';
|
|
103
|
+
return 'slow-2g';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Infer from RTT (ms)
|
|
107
|
+
if (rtt) {
|
|
108
|
+
const ms = parseInt(rtt, 10);
|
|
109
|
+
if (ms < 50) return 'fast';
|
|
110
|
+
if (ms < 100) return '4g';
|
|
111
|
+
if (ms < 300) return '3g';
|
|
112
|
+
if (ms < 700) return '2g';
|
|
113
|
+
return 'slow-2g';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Default to 4g
|
|
117
|
+
return '4g';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Extract reduced motion preference
|
|
122
|
+
*/
|
|
123
|
+
function extractReducedMotion(request: Request): boolean {
|
|
124
|
+
const prefersReducedMotion = request.headers.get('sec-ch-prefers-reduced-motion');
|
|
125
|
+
return prefersReducedMotion === 'reduce';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Extract timezone and local hour
|
|
130
|
+
*/
|
|
131
|
+
function extractTimeContext(request: Request): { timezone: string; localHour: number } {
|
|
132
|
+
const headers = request.headers;
|
|
133
|
+
|
|
134
|
+
// Custom header from client
|
|
135
|
+
const xTimezone = headers.get('x-timezone');
|
|
136
|
+
const xLocalHour = headers.get('x-local-hour');
|
|
137
|
+
|
|
138
|
+
// Cloudflare provides timezone in cf object
|
|
139
|
+
const cfTimezone = (request as Request & { cf?: { timezone?: string } }).cf?.timezone;
|
|
140
|
+
|
|
141
|
+
const timezone = xTimezone || cfTimezone || 'UTC';
|
|
142
|
+
const localHour = xLocalHour ? parseInt(xLocalHour, 10) : new Date().getUTCHours();
|
|
143
|
+
|
|
144
|
+
return { timezone, localHour };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ============================================================================
|
|
148
|
+
// Cookie/Session Extraction
|
|
149
|
+
// ============================================================================
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Extract user identity and tier from cookies/headers
|
|
153
|
+
*/
|
|
154
|
+
function extractIdentity(
|
|
155
|
+
cookies: Record<string, string>,
|
|
156
|
+
request: Request
|
|
157
|
+
): { userId?: string; tier: UserTier } {
|
|
158
|
+
// User ID from cookie or header
|
|
159
|
+
const userId = cookies['user_id'] || request.headers.get('x-user-id') || undefined;
|
|
160
|
+
|
|
161
|
+
// Tier from cookie, header, or default
|
|
162
|
+
const tierCookie = cookies['user_tier'] as UserTier | undefined;
|
|
163
|
+
const tierHeader = request.headers.get('x-user-tier') as UserTier | null;
|
|
164
|
+
const tier = tierCookie || tierHeader || 'free';
|
|
165
|
+
|
|
166
|
+
return { userId, tier };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Extract navigation history from cookies
|
|
171
|
+
*/
|
|
172
|
+
function extractNavigationHistory(cookies: Record<string, string>): {
|
|
173
|
+
recentPages: string[];
|
|
174
|
+
dwellTimes: Map<string, number>;
|
|
175
|
+
clickPatterns: string[];
|
|
176
|
+
} {
|
|
177
|
+
const recentPages = parseJSON<string[]>(cookies['recent_pages'], []);
|
|
178
|
+
const dwellTimesObj = parseJSON<Record<string, number>>(cookies['dwell_times'], {});
|
|
179
|
+
const clickPatterns = parseJSON<string[]>(cookies['click_patterns'], []);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
recentPages,
|
|
183
|
+
dwellTimes: new Map(Object.entries(dwellTimesObj)),
|
|
184
|
+
clickPatterns,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Extract emotion state from cookies/headers
|
|
190
|
+
*/
|
|
191
|
+
function extractEmotionState(
|
|
192
|
+
cookies: Record<string, string>,
|
|
193
|
+
request: Request
|
|
194
|
+
): EmotionState | undefined {
|
|
195
|
+
// Check custom header first (set by edge-workers inference)
|
|
196
|
+
const xEmotion = request.headers.get('x-emotion-state');
|
|
197
|
+
if (xEmotion) {
|
|
198
|
+
return parseJSON<EmotionState | undefined>(xEmotion, undefined);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check cookie
|
|
202
|
+
const emotionCookie = cookies['emotion_state'];
|
|
203
|
+
if (emotionCookie) {
|
|
204
|
+
return parseJSON<EmotionState | undefined>(emotionCookie, undefined);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Extract user preferences from cookies
|
|
212
|
+
*/
|
|
213
|
+
function extractPreferences(cookies: Record<string, string>): Record<string, unknown> {
|
|
214
|
+
return parseJSON<Record<string, unknown>>(cookies['user_preferences'], {});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Extract session info from cookies
|
|
219
|
+
*/
|
|
220
|
+
function extractSessionInfo(cookies: Record<string, string>): {
|
|
221
|
+
sessionId?: string;
|
|
222
|
+
isNewSession: boolean;
|
|
223
|
+
sessionStartedAt?: Date;
|
|
224
|
+
} {
|
|
225
|
+
const sessionId = cookies['session_id'];
|
|
226
|
+
const sessionStarted = cookies['session_started'];
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
sessionId,
|
|
230
|
+
isNewSession: !sessionId,
|
|
231
|
+
sessionStartedAt: sessionStarted ? new Date(sessionStarted) : undefined,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ============================================================================
|
|
236
|
+
// Main Extractor
|
|
237
|
+
// ============================================================================
|
|
238
|
+
|
|
239
|
+
export interface ContextExtractorOptions {
|
|
240
|
+
/** Custom emotion detector (e.g., call edge-workers) */
|
|
241
|
+
detectEmotion?: (request: Request) => Promise<EmotionState | undefined>;
|
|
242
|
+
|
|
243
|
+
/** Custom user tier resolver (e.g., from database) */
|
|
244
|
+
resolveUserTier?: (userId: string) => Promise<UserTier>;
|
|
245
|
+
|
|
246
|
+
/** Additional context enrichment */
|
|
247
|
+
enrich?: (context: UserContext, request: Request) => Promise<UserContext>;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Extract UserContext from an HTTP request
|
|
252
|
+
*/
|
|
253
|
+
export async function extractUserContext(
|
|
254
|
+
request: Request,
|
|
255
|
+
options: ContextExtractorOptions = {}
|
|
256
|
+
): Promise<UserContext> {
|
|
257
|
+
const cookies = parseCookies(request.headers.get('cookie'));
|
|
258
|
+
|
|
259
|
+
// Extract all signals
|
|
260
|
+
const viewport = extractViewport(request);
|
|
261
|
+
const connection = extractConnection(request);
|
|
262
|
+
const reducedMotion = extractReducedMotion(request);
|
|
263
|
+
const { timezone, localHour } = extractTimeContext(request);
|
|
264
|
+
const { userId, tier: initialTier } = extractIdentity(cookies, request);
|
|
265
|
+
const { recentPages, dwellTimes, clickPatterns } = extractNavigationHistory(cookies);
|
|
266
|
+
const preferences = extractPreferences(cookies);
|
|
267
|
+
const { sessionId, isNewSession, sessionStartedAt } = extractSessionInfo(cookies);
|
|
268
|
+
|
|
269
|
+
// Resolve user tier if we have a resolver and userId
|
|
270
|
+
let tier = initialTier;
|
|
271
|
+
if (options.resolveUserTier && userId) {
|
|
272
|
+
try {
|
|
273
|
+
tier = await options.resolveUserTier(userId);
|
|
274
|
+
} catch {
|
|
275
|
+
// Keep initial tier on error
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Extract or detect emotion state
|
|
280
|
+
let emotionState = extractEmotionState(cookies, request);
|
|
281
|
+
if (!emotionState && options.detectEmotion) {
|
|
282
|
+
try {
|
|
283
|
+
emotionState = await options.detectEmotion(request);
|
|
284
|
+
} catch {
|
|
285
|
+
// No emotion state on error
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Build context
|
|
290
|
+
let context: UserContext = {
|
|
291
|
+
userId,
|
|
292
|
+
tier,
|
|
293
|
+
recentPages,
|
|
294
|
+
dwellTimes,
|
|
295
|
+
clickPatterns,
|
|
296
|
+
emotionState,
|
|
297
|
+
preferences,
|
|
298
|
+
viewport,
|
|
299
|
+
connection,
|
|
300
|
+
reducedMotion,
|
|
301
|
+
localHour,
|
|
302
|
+
timezone,
|
|
303
|
+
sessionId,
|
|
304
|
+
isNewSession,
|
|
305
|
+
sessionStartedAt,
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Optional enrichment
|
|
309
|
+
if (options.enrich) {
|
|
310
|
+
context = await options.enrich(context, request);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return context;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Create middleware for user context extraction
|
|
318
|
+
*/
|
|
319
|
+
export function createContextMiddleware(options: ContextExtractorOptions = {}) {
|
|
320
|
+
return async (request: Request): Promise<UserContext> => {
|
|
321
|
+
return extractUserContext(request, options);
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ============================================================================
|
|
326
|
+
// Response Helpers
|
|
327
|
+
// ============================================================================
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Set context tracking cookies in response
|
|
331
|
+
*/
|
|
332
|
+
export function setContextCookies(
|
|
333
|
+
response: Response,
|
|
334
|
+
context: UserContext,
|
|
335
|
+
currentPath: string
|
|
336
|
+
): Response {
|
|
337
|
+
const headers = new Headers(response.headers);
|
|
338
|
+
|
|
339
|
+
// Update recent pages
|
|
340
|
+
const recentPages = [...context.recentPages.slice(-9), currentPath];
|
|
341
|
+
headers.append(
|
|
342
|
+
'Set-Cookie',
|
|
343
|
+
`recent_pages=${encodeURIComponent(JSON.stringify(recentPages))}; Path=/; Max-Age=604800; SameSite=Lax`
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// Set session cookie if new session
|
|
347
|
+
if (context.isNewSession) {
|
|
348
|
+
const sessionId = `sess_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
349
|
+
headers.append(
|
|
350
|
+
'Set-Cookie',
|
|
351
|
+
`session_id=${sessionId}; Path=/; Max-Age=86400; SameSite=Lax`
|
|
352
|
+
);
|
|
353
|
+
headers.append(
|
|
354
|
+
'Set-Cookie',
|
|
355
|
+
`session_started=${new Date().toISOString()}; Path=/; Max-Age=86400; SameSite=Lax`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return new Response(response.body, {
|
|
360
|
+
status: response.status,
|
|
361
|
+
statusText: response.statusText,
|
|
362
|
+
headers,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Add speculation hints to response headers
|
|
368
|
+
*/
|
|
369
|
+
export function addSpeculationHeaders(
|
|
370
|
+
response: Response,
|
|
371
|
+
prefetch: string[],
|
|
372
|
+
prerender: string[]
|
|
373
|
+
): Response {
|
|
374
|
+
const headers = new Headers(response.headers);
|
|
375
|
+
|
|
376
|
+
// Add Link headers for prefetch
|
|
377
|
+
if (prefetch.length > 0) {
|
|
378
|
+
const linkHeader = prefetch
|
|
379
|
+
.map((path) => `<${path}>; rel=prefetch`)
|
|
380
|
+
.join(', ');
|
|
381
|
+
headers.append('Link', linkHeader);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Add prerender hints (Speculation Rules API will be injected in HTML)
|
|
385
|
+
if (prerender.length > 0) {
|
|
386
|
+
headers.set('X-Prerender-Hints', prerender.join(','));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return new Response(response.body, {
|
|
390
|
+
status: response.status,
|
|
391
|
+
statusText: response.statusText,
|
|
392
|
+
headers,
|
|
393
|
+
});
|
|
394
|
+
}
|