@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,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aeon Pages API Routes
|
|
3
|
+
*
|
|
4
|
+
* Server-side route handling for API endpoints.
|
|
5
|
+
* Enables full-stack functionality without a separate backend.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
AeonEnv,
|
|
10
|
+
AeonContext,
|
|
11
|
+
ApiRoute,
|
|
12
|
+
ApiRouteMatch,
|
|
13
|
+
ApiRouteModule,
|
|
14
|
+
ApiRouteHandler,
|
|
15
|
+
ApiRouteSegment,
|
|
16
|
+
HttpMethod,
|
|
17
|
+
ExecutionContext,
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* API Router - matches requests to registered API routes
|
|
22
|
+
*/
|
|
23
|
+
export class ApiRouter<E extends AeonEnv = AeonEnv> {
|
|
24
|
+
private routes: ApiRoute[] = [];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Register an API route
|
|
28
|
+
*/
|
|
29
|
+
register(pattern: string, module: ApiRouteModule<E>): void {
|
|
30
|
+
const segments = this.parsePattern(pattern);
|
|
31
|
+
this.routes.push({ pattern, segments, module: module as ApiRouteModule });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Register multiple routes from a route map
|
|
36
|
+
*/
|
|
37
|
+
registerAll(routes: Record<string, ApiRouteModule<E>>): void {
|
|
38
|
+
for (const [pattern, module] of Object.entries(routes)) {
|
|
39
|
+
this.register(pattern, module);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Match a request to a route
|
|
45
|
+
*/
|
|
46
|
+
match(request: Request): ApiRouteMatch | null {
|
|
47
|
+
const url = new URL(request.url);
|
|
48
|
+
const method = request.method.toUpperCase() as HttpMethod;
|
|
49
|
+
const pathSegments = url.pathname.split('/').filter(Boolean);
|
|
50
|
+
|
|
51
|
+
for (const route of this.routes) {
|
|
52
|
+
const params = this.matchSegments(route.segments, pathSegments);
|
|
53
|
+
if (params !== null) {
|
|
54
|
+
const handler = this.getHandler(route.module, method);
|
|
55
|
+
if (handler) {
|
|
56
|
+
return { route, params, handler };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Handle an API request
|
|
66
|
+
*/
|
|
67
|
+
async handle(
|
|
68
|
+
request: Request,
|
|
69
|
+
env: E,
|
|
70
|
+
ctx: ExecutionContext,
|
|
71
|
+
): Promise<Response | null> {
|
|
72
|
+
const match = this.match(request);
|
|
73
|
+
if (!match) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const url = new URL(request.url);
|
|
78
|
+
const context: AeonContext<E> = {
|
|
79
|
+
request,
|
|
80
|
+
env,
|
|
81
|
+
params: match.params,
|
|
82
|
+
url,
|
|
83
|
+
ctx,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
return await match.handler(context);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('API route error:', error);
|
|
90
|
+
return new Response(
|
|
91
|
+
JSON.stringify({
|
|
92
|
+
error: 'Internal server error',
|
|
93
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
94
|
+
}),
|
|
95
|
+
{
|
|
96
|
+
status: 500,
|
|
97
|
+
headers: { 'Content-Type': 'application/json' },
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Parse a route pattern into segments
|
|
105
|
+
*/
|
|
106
|
+
private parsePattern(pattern: string): ApiRouteSegment[] {
|
|
107
|
+
return pattern
|
|
108
|
+
.split('/')
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.map((segment) => {
|
|
111
|
+
// Catch-all: [...slug]
|
|
112
|
+
if (segment.startsWith('[...') && segment.endsWith(']')) {
|
|
113
|
+
return {
|
|
114
|
+
value: segment.slice(4, -1),
|
|
115
|
+
isDynamic: true,
|
|
116
|
+
isCatchAll: true,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// Dynamic: [id]
|
|
120
|
+
if (segment.startsWith('[') && segment.endsWith(']')) {
|
|
121
|
+
return {
|
|
122
|
+
value: segment.slice(1, -1),
|
|
123
|
+
isDynamic: true,
|
|
124
|
+
isCatchAll: false,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// Static
|
|
128
|
+
return {
|
|
129
|
+
value: segment,
|
|
130
|
+
isDynamic: false,
|
|
131
|
+
isCatchAll: false,
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Match path segments against route segments
|
|
138
|
+
*/
|
|
139
|
+
private matchSegments(
|
|
140
|
+
routeSegments: ApiRouteSegment[],
|
|
141
|
+
pathSegments: string[],
|
|
142
|
+
): Record<string, string> | null {
|
|
143
|
+
const params: Record<string, string> = {};
|
|
144
|
+
let pathIndex = 0;
|
|
145
|
+
|
|
146
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
|
147
|
+
const routeSegment = routeSegments[i];
|
|
148
|
+
|
|
149
|
+
if (routeSegment.isCatchAll) {
|
|
150
|
+
// Catch-all consumes remaining segments
|
|
151
|
+
params[routeSegment.value] = pathSegments.slice(pathIndex).join('/');
|
|
152
|
+
return params;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (pathIndex >= pathSegments.length) {
|
|
156
|
+
// No more path segments but route expects more
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (routeSegment.isDynamic) {
|
|
161
|
+
// Dynamic segment - capture value
|
|
162
|
+
params[routeSegment.value] = pathSegments[pathIndex];
|
|
163
|
+
pathIndex++;
|
|
164
|
+
} else {
|
|
165
|
+
// Static segment - must match exactly
|
|
166
|
+
if (routeSegment.value !== pathSegments[pathIndex]) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
pathIndex++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// All path segments must be consumed
|
|
174
|
+
if (pathIndex !== pathSegments.length) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return params;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get the handler for a given HTTP method
|
|
183
|
+
*/
|
|
184
|
+
private getHandler(
|
|
185
|
+
module: ApiRouteModule,
|
|
186
|
+
method: HttpMethod,
|
|
187
|
+
): ApiRouteHandler | null {
|
|
188
|
+
const handler = module[method];
|
|
189
|
+
if (handler) {
|
|
190
|
+
return handler;
|
|
191
|
+
}
|
|
192
|
+
// Fall back to default handler
|
|
193
|
+
if (module.default) {
|
|
194
|
+
return module.default;
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get all registered routes (for debugging/introspection)
|
|
201
|
+
*/
|
|
202
|
+
getRoutes(): ApiRoute[] {
|
|
203
|
+
return [...this.routes];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Create a new API router instance
|
|
209
|
+
*/
|
|
210
|
+
export function createApiRouter<E extends AeonEnv = AeonEnv>(): ApiRouter<E> {
|
|
211
|
+
return new ApiRouter<E>();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Helper to create a JSON response
|
|
216
|
+
*/
|
|
217
|
+
export function json<T>(data: T, init?: ResponseInit): Response {
|
|
218
|
+
return new Response(JSON.stringify(data), {
|
|
219
|
+
...init,
|
|
220
|
+
headers: {
|
|
221
|
+
'Content-Type': 'application/json',
|
|
222
|
+
...init?.headers,
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Helper to create a redirect response
|
|
229
|
+
*/
|
|
230
|
+
export function redirect(
|
|
231
|
+
url: string,
|
|
232
|
+
status: 301 | 302 | 303 | 307 | 308 = 302,
|
|
233
|
+
): Response {
|
|
234
|
+
return new Response(null, {
|
|
235
|
+
status,
|
|
236
|
+
headers: { Location: url },
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Helper to create an error response
|
|
242
|
+
*/
|
|
243
|
+
export function error(message: string, status = 500): Response {
|
|
244
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
245
|
+
status,
|
|
246
|
+
headers: { 'Content-Type': 'application/json' },
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Helper to create a not found response
|
|
252
|
+
*/
|
|
253
|
+
export function notFound(message = 'Not found'): Response {
|
|
254
|
+
return error(message, 404);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Helper to create a bad request response
|
|
259
|
+
*/
|
|
260
|
+
export function badRequest(message = 'Bad request'): Response {
|
|
261
|
+
return error(message, 400);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Helper to create an unauthorized response
|
|
266
|
+
*/
|
|
267
|
+
export function unauthorized(message = 'Unauthorized'): Response {
|
|
268
|
+
return error(message, 401);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Helper to create a forbidden response
|
|
273
|
+
*/
|
|
274
|
+
export function forbidden(message = 'Forbidden'): Response {
|
|
275
|
+
return error(message, 403);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// =============================================================================
|
|
279
|
+
// MIDDLEWARE SUPPORT
|
|
280
|
+
// =============================================================================
|
|
281
|
+
|
|
282
|
+
/** Middleware function type */
|
|
283
|
+
export type Middleware<E extends AeonEnv = AeonEnv> = (
|
|
284
|
+
context: AeonContext<E>,
|
|
285
|
+
next: () => Promise<Response>,
|
|
286
|
+
) => Response | Promise<Response>;
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Compose multiple middleware into a single handler
|
|
290
|
+
*/
|
|
291
|
+
export function composeMiddleware<E extends AeonEnv = AeonEnv>(
|
|
292
|
+
...middlewares: Middleware<E>[]
|
|
293
|
+
): (handler: ApiRouteHandler<E>) => ApiRouteHandler<E> {
|
|
294
|
+
return (handler: ApiRouteHandler<E>): ApiRouteHandler<E> => {
|
|
295
|
+
return async (context: AeonContext<E>): Promise<Response> => {
|
|
296
|
+
let index = 0;
|
|
297
|
+
|
|
298
|
+
const next = async (): Promise<Response> => {
|
|
299
|
+
if (index < middlewares.length) {
|
|
300
|
+
const middleware = middlewares[index++];
|
|
301
|
+
return middleware(context, next);
|
|
302
|
+
}
|
|
303
|
+
return handler(context);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
return next();
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* CORS middleware factory
|
|
313
|
+
*/
|
|
314
|
+
export function cors(options?: {
|
|
315
|
+
origin?: string | string[] | ((origin: string) => boolean);
|
|
316
|
+
methods?: string[];
|
|
317
|
+
headers?: string[];
|
|
318
|
+
credentials?: boolean;
|
|
319
|
+
maxAge?: number;
|
|
320
|
+
}): Middleware {
|
|
321
|
+
const opts = {
|
|
322
|
+
origin: '*',
|
|
323
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
|
|
324
|
+
headers: ['Content-Type', 'Authorization'],
|
|
325
|
+
credentials: false,
|
|
326
|
+
maxAge: 86400,
|
|
327
|
+
...options,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
return async (context, next) => {
|
|
331
|
+
const requestOrigin = context.request.headers.get('Origin') || '';
|
|
332
|
+
|
|
333
|
+
// Determine allowed origin
|
|
334
|
+
let allowedOrigin = '*';
|
|
335
|
+
if (typeof opts.origin === 'string') {
|
|
336
|
+
allowedOrigin = opts.origin;
|
|
337
|
+
} else if (Array.isArray(opts.origin)) {
|
|
338
|
+
if (opts.origin.includes(requestOrigin)) {
|
|
339
|
+
allowedOrigin = requestOrigin;
|
|
340
|
+
}
|
|
341
|
+
} else if (typeof opts.origin === 'function') {
|
|
342
|
+
if (opts.origin(requestOrigin)) {
|
|
343
|
+
allowedOrigin = requestOrigin;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const corsHeaders: Record<string, string> = {
|
|
348
|
+
'Access-Control-Allow-Origin': allowedOrigin,
|
|
349
|
+
'Access-Control-Allow-Methods': opts.methods.join(', '),
|
|
350
|
+
'Access-Control-Allow-Headers': opts.headers.join(', '),
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
if (opts.credentials) {
|
|
354
|
+
corsHeaders['Access-Control-Allow-Credentials'] = 'true';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Handle preflight
|
|
358
|
+
if (context.request.method === 'OPTIONS') {
|
|
359
|
+
return new Response(null, {
|
|
360
|
+
status: 204,
|
|
361
|
+
headers: {
|
|
362
|
+
...corsHeaders,
|
|
363
|
+
'Access-Control-Max-Age': String(opts.maxAge),
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const response = await next();
|
|
369
|
+
|
|
370
|
+
// Add CORS headers to response
|
|
371
|
+
const newHeaders = new Headers(response.headers);
|
|
372
|
+
for (const [key, value] of Object.entries(corsHeaders)) {
|
|
373
|
+
newHeaders.set(key, value);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return new Response(response.body, {
|
|
377
|
+
status: response.status,
|
|
378
|
+
statusText: response.statusText,
|
|
379
|
+
headers: newHeaders,
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Auth middleware factory - validates Authorization header
|
|
386
|
+
*/
|
|
387
|
+
export function requireAuth<E extends AeonEnv = AeonEnv>(
|
|
388
|
+
validate: (
|
|
389
|
+
token: string,
|
|
390
|
+
context: AeonContext<E>,
|
|
391
|
+
) => boolean | Promise<boolean>,
|
|
392
|
+
): Middleware<E> {
|
|
393
|
+
return async (context, next) => {
|
|
394
|
+
const authHeader = context.request.headers.get('Authorization');
|
|
395
|
+
if (!authHeader) {
|
|
396
|
+
return unauthorized('Missing Authorization header');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const token = authHeader.replace(/^Bearer\s+/i, '');
|
|
400
|
+
const isValid = await validate(token, context);
|
|
401
|
+
|
|
402
|
+
if (!isValid) {
|
|
403
|
+
return unauthorized('Invalid token');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return next();
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Rate limiting middleware (uses KV for distributed rate limiting)
|
|
412
|
+
*/
|
|
413
|
+
export function rateLimit<E extends AeonEnv = AeonEnv>(options: {
|
|
414
|
+
/** KV namespace key in env */
|
|
415
|
+
kvKey?: keyof E;
|
|
416
|
+
/** Requests per window */
|
|
417
|
+
limit: number;
|
|
418
|
+
/** Window size in seconds */
|
|
419
|
+
window: number;
|
|
420
|
+
/** Function to extract client identifier (default: IP) */
|
|
421
|
+
keyGenerator?: (context: AeonContext<E>) => string;
|
|
422
|
+
}): Middleware<E> {
|
|
423
|
+
return async (context, next) => {
|
|
424
|
+
const kv = options.kvKey
|
|
425
|
+
? (context.env[options.kvKey] as unknown)
|
|
426
|
+
: context.env.CACHE;
|
|
427
|
+
if (!kv || typeof (kv as Record<string, unknown>).get !== 'function') {
|
|
428
|
+
// No KV available, skip rate limiting
|
|
429
|
+
return next();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const kvNamespace = kv as {
|
|
433
|
+
get: (key: string) => Promise<string | null>;
|
|
434
|
+
put: (
|
|
435
|
+
key: string,
|
|
436
|
+
value: string,
|
|
437
|
+
options?: { expirationTtl?: number },
|
|
438
|
+
) => Promise<void>;
|
|
439
|
+
};
|
|
440
|
+
const clientKey = options.keyGenerator
|
|
441
|
+
? options.keyGenerator(context)
|
|
442
|
+
: context.request.headers.get('CF-Connecting-IP') || 'unknown';
|
|
443
|
+
|
|
444
|
+
const rateLimitKey = `ratelimit:${clientKey}`;
|
|
445
|
+
const current = await kvNamespace.get(rateLimitKey);
|
|
446
|
+
const count = current ? parseInt(current, 10) : 0;
|
|
447
|
+
|
|
448
|
+
if (count >= options.limit) {
|
|
449
|
+
return new Response(JSON.stringify({ error: 'Too many requests' }), {
|
|
450
|
+
status: 429,
|
|
451
|
+
headers: {
|
|
452
|
+
'Content-Type': 'application/json',
|
|
453
|
+
'Retry-After': String(options.window),
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Increment counter
|
|
459
|
+
await kvNamespace.put(rateLimitKey, String(count + 1), {
|
|
460
|
+
expirationTtl: options.window,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
return next();
|
|
464
|
+
};
|
|
465
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aeon Flux Benchmarks
|
|
3
|
+
*
|
|
4
|
+
* Run: bun run src/benchmark.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { HeuristicAdapter } from './router/heuristic-adapter';
|
|
8
|
+
import { compileTreeToTSX } from './tree-compiler';
|
|
9
|
+
import type { UserContext, ComponentTree, ComponentNode } from './router/types';
|
|
10
|
+
|
|
11
|
+
// Mock data
|
|
12
|
+
function createMockContext(): UserContext {
|
|
13
|
+
return {
|
|
14
|
+
tier: 'pro',
|
|
15
|
+
recentPages: ['/', '/chat', '/settings', '/chat', '/', '/tools'],
|
|
16
|
+
dwellTimes: new Map([
|
|
17
|
+
['/', 5000],
|
|
18
|
+
['/chat', 12000],
|
|
19
|
+
]),
|
|
20
|
+
clickPatterns: ['nav-chat', 'btn-send', 'nav-home'],
|
|
21
|
+
preferences: { theme: 'dark' },
|
|
22
|
+
viewport: { width: 1920, height: 1080 },
|
|
23
|
+
connection: 'fast',
|
|
24
|
+
reducedMotion: false,
|
|
25
|
+
localHour: 14,
|
|
26
|
+
timezone: 'America/New_York',
|
|
27
|
+
isNewSession: false,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createMockTree(nodeCount: number): ComponentTree {
|
|
32
|
+
const nodes = new Map<string, ComponentNode>();
|
|
33
|
+
|
|
34
|
+
nodes.set('root', { id: 'root', type: 'Page', children: [] });
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < nodeCount; i++) {
|
|
37
|
+
const id = `node-${i}`;
|
|
38
|
+
nodes.set(id, {
|
|
39
|
+
id,
|
|
40
|
+
type: i % 3 === 0 ? 'Section' : i % 3 === 1 ? 'Text' : 'Button',
|
|
41
|
+
props: { title: `Node ${i}`, className: 'test-class' },
|
|
42
|
+
});
|
|
43
|
+
(nodes.get('root')!.children as string[]).push(id);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
rootId: 'root',
|
|
48
|
+
nodes,
|
|
49
|
+
getNode: (id) => nodes.get(id),
|
|
50
|
+
getChildren: (id) => {
|
|
51
|
+
const node = nodes.get(id);
|
|
52
|
+
if (!node?.children) return [];
|
|
53
|
+
return (node.children as string[]).map((cid) => nodes.get(cid)!);
|
|
54
|
+
},
|
|
55
|
+
getSchema: () => ({
|
|
56
|
+
rootId: 'root',
|
|
57
|
+
nodeCount: nodes.size,
|
|
58
|
+
nodeTypes: ['Page', 'Section', 'Text', 'Button'],
|
|
59
|
+
depth: 2,
|
|
60
|
+
}),
|
|
61
|
+
clone: () => createMockTree(nodeCount),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createMockTreeForCompiler(nodeCount: number) {
|
|
66
|
+
const children = [];
|
|
67
|
+
for (let i = 0; i < nodeCount; i++) {
|
|
68
|
+
children.push({
|
|
69
|
+
id: `node-${i}`,
|
|
70
|
+
type: i % 3 === 0 ? 'Section' : i % 3 === 1 ? 'Text' : 'Button',
|
|
71
|
+
props: { title: `Node ${i}`, className: 'test-class' },
|
|
72
|
+
children:
|
|
73
|
+
i % 5 === 0
|
|
74
|
+
? [{ id: `nested-${i}`, type: 'Span', text: 'Nested content' }]
|
|
75
|
+
: [],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return { id: 'root', type: 'Page', children };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function benchmark(
|
|
82
|
+
name: string,
|
|
83
|
+
fn: () => unknown | Promise<unknown>,
|
|
84
|
+
iterations: number = 1000,
|
|
85
|
+
) {
|
|
86
|
+
// Warmup
|
|
87
|
+
for (let i = 0; i < 10; i++) await fn();
|
|
88
|
+
|
|
89
|
+
const start = performance.now();
|
|
90
|
+
for (let i = 0; i < iterations; i++) {
|
|
91
|
+
await fn();
|
|
92
|
+
}
|
|
93
|
+
const elapsed = performance.now() - start;
|
|
94
|
+
|
|
95
|
+
const avgMs = elapsed / iterations;
|
|
96
|
+
const opsPerSec = Math.round(1000 / avgMs);
|
|
97
|
+
|
|
98
|
+
console.log(
|
|
99
|
+
`${name.padEnd(40)} ${avgMs.toFixed(3).padStart(8)}ms ${opsPerSec.toLocaleString().padStart(8)} ops/sec`,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return { name, avgMs, opsPerSec };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function main() {
|
|
106
|
+
console.log('\nš Aeon Flux Benchmarks\n');
|
|
107
|
+
console.log('='.repeat(70));
|
|
108
|
+
|
|
109
|
+
const adapter = new HeuristicAdapter({
|
|
110
|
+
defaultPaths: ['/', '/chat', '/settings', '/tools', '/about'],
|
|
111
|
+
});
|
|
112
|
+
const context = createMockContext();
|
|
113
|
+
|
|
114
|
+
// Router benchmarks
|
|
115
|
+
console.log('\nš HeuristicAdapter.route()\n');
|
|
116
|
+
|
|
117
|
+
for (const nodeCount of [10, 50, 100, 500]) {
|
|
118
|
+
const tree = createMockTree(nodeCount);
|
|
119
|
+
await benchmark(` route() with ${nodeCount} nodes`, () =>
|
|
120
|
+
adapter.route('/', context, tree),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Speculation benchmarks
|
|
125
|
+
console.log('\nš® HeuristicAdapter.speculate()\n');
|
|
126
|
+
|
|
127
|
+
await benchmark(' speculate() - empty history', () =>
|
|
128
|
+
adapter.speculate('/', { ...context, recentPages: [] }),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
await benchmark(' speculate() - 6 page history', () =>
|
|
132
|
+
adapter.speculate('/', context),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
await benchmark(' speculate() - 50 page history', () =>
|
|
136
|
+
adapter.speculate('/', {
|
|
137
|
+
...context,
|
|
138
|
+
recentPages: Array(50)
|
|
139
|
+
.fill('/')
|
|
140
|
+
.map((_, i) => `/${i % 5}`),
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Tree compiler benchmarks
|
|
145
|
+
console.log('\nš³ Tree ā TSX Compiler\n');
|
|
146
|
+
|
|
147
|
+
for (const nodeCount of [10, 50, 100, 500]) {
|
|
148
|
+
const tree = createMockTreeForCompiler(nodeCount);
|
|
149
|
+
await benchmark(
|
|
150
|
+
` compile ${nodeCount} nodes ā TSX`,
|
|
151
|
+
() => compileTreeToTSX(tree, { route: '/test' }),
|
|
152
|
+
100,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Personalization benchmarks
|
|
157
|
+
console.log('\nšØ personalizeTree()\n');
|
|
158
|
+
|
|
159
|
+
for (const nodeCount of [10, 50, 100, 500]) {
|
|
160
|
+
const tree = createMockTree(nodeCount);
|
|
161
|
+
const decision = await adapter.route('/', context, tree);
|
|
162
|
+
await benchmark(` personalize ${nodeCount} nodes`, () =>
|
|
163
|
+
adapter.personalizeTree(tree, decision),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log('\n' + '='.repeat(70));
|
|
168
|
+
console.log('ā
Benchmarks complete\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
main().catch(console.error);
|