@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,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
+ }