@nextrush/adapter-edge 1.0.0-beta.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/LICENSE +21 -0
- package/README.md +386 -0
- package/dist/index.d.ts +287 -0
- package/dist/index.js +178 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
- package/src/__tests__/adapter.test.ts +89 -0
- package/src/__tests__/body-source.test.ts +85 -0
- package/src/__tests__/context-response-microtrims.test.ts +52 -0
- package/src/__tests__/context.test.ts +536 -0
- package/src/__tests__/default-timeout.test.ts +82 -0
- package/src/__tests__/per-request-work-trim.test.ts +171 -0
- package/src/__tests__/public-surface.test.ts +85 -0
- package/src/__tests__/utils.test.ts +140 -0
- package/src/adapter.ts +333 -0
- package/src/body-source.ts +19 -0
- package/src/context.ts +122 -0
- package/src/index.ts +64 -0
- package/src/utils.ts +34 -0
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nextrush/adapter-edge - Edge Runtime Adapter
|
|
3
|
+
*
|
|
4
|
+
* Connects NextRush Application to Edge runtimes via fetch handlers.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* **Size optimization**: Edge runtimes have strict bundle-size constraints
|
|
8
|
+
* (e.g. Cloudflare Workers 1 MB limit). Import only the packages you need:
|
|
9
|
+
*
|
|
10
|
+
* - Import `@nextrush/core` and `@nextrush/adapter-edge` only.
|
|
11
|
+
* - Avoid `@nextrush/di` unless you need DI (adds `reflect-metadata`).
|
|
12
|
+
* - Tree-shake unused middleware — each middleware is a separate package.
|
|
13
|
+
* - Use `@nextrush/router` only if you have dynamic routes; for simple
|
|
14
|
+
* handlers, use the application directly.
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { Application } from '@nextrush/core';
|
|
20
|
+
import { jsonErrorResponse } from '@nextrush/runtime';
|
|
21
|
+
import type { AdapterContextFactory, FetchAdapter } from '@nextrush/types';
|
|
22
|
+
import { createEdgeContext, EdgeContext, type EdgeExecutionContext } from './context';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default request timeout applied when the caller specifies none (F-07,
|
|
26
|
+
* ADR-0010), converging Edge's default contract with Node/Bun/Deno (which all
|
|
27
|
+
* default to a bounded timeout rather than none).
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* 25 000 ms — below the tightest common edge-platform wall limit (Vercel Edge
|
|
31
|
+
* Functions: 25 s), comfortably above typical handler durations, so the
|
|
32
|
+
* framework's clean `504` fires before the platform terminates the isolate.
|
|
33
|
+
* Pass `timeout: 0` to disable the framework timeout entirely (platform limit
|
|
34
|
+
* alone applies) or a positive value to override the default.
|
|
35
|
+
*/
|
|
36
|
+
export const DEFAULT_EDGE_TIMEOUT_MS = 25_000;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Options for the fetch handler
|
|
40
|
+
*/
|
|
41
|
+
export interface FetchHandlerOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Custom error handler
|
|
44
|
+
*/
|
|
45
|
+
onError?: (error: Error, ctx: EdgeContext) => Response | Promise<Response>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Request timeout in milliseconds. The handler races the application logic
|
|
49
|
+
* against a timer and returns a 504 Gateway Timeout if the timer fires
|
|
50
|
+
* first, cancelling the still-running handler via `ctx.signal`.
|
|
51
|
+
*
|
|
52
|
+
* @remarks
|
|
53
|
+
* Defaults to {@link DEFAULT_EDGE_TIMEOUT_MS} (25 000 ms) when omitted
|
|
54
|
+
* (F-07/ADR-0010) — below the tightest common edge-platform wall limit
|
|
55
|
+
* (Vercel Edge: 25 s), so the framework's clean `504` fires before the
|
|
56
|
+
* platform terminates the isolate. Pass `0` to disable the framework
|
|
57
|
+
* timeout entirely (the platform's own limit still applies).
|
|
58
|
+
*
|
|
59
|
+
* Per-platform CPU/wall limits for context when overriding:
|
|
60
|
+
* - Cloudflare Workers: 30 000 (30 s CPU limit)
|
|
61
|
+
* - Vercel Edge: 25 000 (25 s wall limit)
|
|
62
|
+
*
|
|
63
|
+
* @default DEFAULT_EDGE_TIMEOUT_MS (25000)
|
|
64
|
+
*/
|
|
65
|
+
timeout?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Standard edge fetch handler type
|
|
70
|
+
*/
|
|
71
|
+
export type FetchHandler = (
|
|
72
|
+
request: Request,
|
|
73
|
+
ctx?: EdgeExecutionContext
|
|
74
|
+
) => Response | Promise<Response>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a fetch handler for Edge runtimes
|
|
78
|
+
*
|
|
79
|
+
* @param app - NextRush Application instance
|
|
80
|
+
* @param options - Handler options
|
|
81
|
+
* @returns Fetch handler function
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* // Cloudflare Workers
|
|
86
|
+
* import { createApp } from '@nextrush/core';
|
|
87
|
+
* import { createFetchHandler } from '@nextrush/adapter-edge';
|
|
88
|
+
*
|
|
89
|
+
* const app = createApp();
|
|
90
|
+
* const handler = createFetchHandler(app);
|
|
91
|
+
*
|
|
92
|
+
* export default {
|
|
93
|
+
* fetch: handler
|
|
94
|
+
* };
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* // Vercel Edge Functions
|
|
100
|
+
* import { createApp } from '@nextrush/core';
|
|
101
|
+
* import { createFetchHandler } from '@nextrush/adapter-edge';
|
|
102
|
+
*
|
|
103
|
+
* const app = createApp();
|
|
104
|
+
* export const config = { runtime: 'edge' };
|
|
105
|
+
* export default createFetchHandler(app);
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export function createFetchHandler(
|
|
109
|
+
app: Application,
|
|
110
|
+
options: FetchHandlerOptions = {}
|
|
111
|
+
): FetchHandler {
|
|
112
|
+
const run = createRequestRunner(app, options);
|
|
113
|
+
return (request: Request, executionContext?: EdgeExecutionContext): Promise<Response> =>
|
|
114
|
+
run(request, executionContext);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Internal request runner shared by every edge entry point.
|
|
119
|
+
*
|
|
120
|
+
* @remarks
|
|
121
|
+
* Centralizes booting, context creation (threading Cloudflare `env` — F-03),
|
|
122
|
+
* timeout racing with cooperative cancellation (F-08), the header-preserving
|
|
123
|
+
* finalize path (F-02), and error handling — so Cloudflare/Vercel/Netlify
|
|
124
|
+
* handlers cannot drift.
|
|
125
|
+
*/
|
|
126
|
+
function createRequestRunner(
|
|
127
|
+
app: Application,
|
|
128
|
+
options: FetchHandlerOptions
|
|
129
|
+
): (request: Request, executionContext?: EdgeExecutionContext, env?: unknown) => Promise<Response> {
|
|
130
|
+
// F-07/ADR-0010: default to DEFAULT_EDGE_TIMEOUT_MS when the caller specifies
|
|
131
|
+
// none (`undefined`); `0` remains an explicit opt-out (no framework timeout).
|
|
132
|
+
const timeout = options.timeout ?? DEFAULT_EDGE_TIMEOUT_MS;
|
|
133
|
+
const trustProxy = app.options.proxy ?? false;
|
|
134
|
+
|
|
135
|
+
/** Sentinel value returned by the timeout racer */
|
|
136
|
+
const TIMEOUT_SENTINEL = Symbol('timeout');
|
|
137
|
+
|
|
138
|
+
// Edge has no serve()/start() phase, so the deferred boot barrier runs lazily
|
|
139
|
+
// on the first request (idempotent). The boot promise caches the snapshotted
|
|
140
|
+
// request handler, so every request awaits and reuses the same one.
|
|
141
|
+
let bootPromise: Promise<ReturnType<Application['callback']>> | null = null;
|
|
142
|
+
const ensureBooted = (): Promise<ReturnType<Application['callback']>> => {
|
|
143
|
+
bootPromise ??= app.ready().then(() => {
|
|
144
|
+
const handler = app.callback();
|
|
145
|
+
// F-14: mark the app running so `app.isRunning` is consistent with the
|
|
146
|
+
// server adapters. Edge has no server lifetime, so close()/destroy() are
|
|
147
|
+
// intentionally never called — that no-teardown contract is documented
|
|
148
|
+
// on createFetchHandler.
|
|
149
|
+
app.start();
|
|
150
|
+
return handler;
|
|
151
|
+
});
|
|
152
|
+
return bootPromise;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return async (
|
|
156
|
+
request: Request,
|
|
157
|
+
executionContext?: EdgeExecutionContext,
|
|
158
|
+
env?: unknown
|
|
159
|
+
): Promise<Response> => {
|
|
160
|
+
const handler = await ensureBooted();
|
|
161
|
+
const ctx = createEdgeContext(request, executionContext, trustProxy, env);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
if (timeout > 0) {
|
|
165
|
+
let timerId: ReturnType<typeof setTimeout> | undefined;
|
|
166
|
+
try {
|
|
167
|
+
const result = await Promise.race([
|
|
168
|
+
handler(ctx).then(() => undefined),
|
|
169
|
+
new Promise<typeof TIMEOUT_SENTINEL>((resolve) => {
|
|
170
|
+
timerId = setTimeout(() => {
|
|
171
|
+
resolve(TIMEOUT_SENTINEL);
|
|
172
|
+
}, timeout);
|
|
173
|
+
}),
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
if (result === TIMEOUT_SENTINEL) {
|
|
177
|
+
// F-08: cancel the still-running handler cooperatively via ctx.signal.
|
|
178
|
+
ctx.triggerTimeout();
|
|
179
|
+
return jsonErrorResponse(504, 'Gateway Timeout');
|
|
180
|
+
}
|
|
181
|
+
} finally {
|
|
182
|
+
// F-08: always clear the timer, on both handler-wins and timeout paths.
|
|
183
|
+
if (timerId !== undefined) clearTimeout(timerId);
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
await handler(ctx);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// F-02: finalize through the context so headers set via ctx.set() survive
|
|
190
|
+
// an implicit/empty response. The 404 default body is written through the
|
|
191
|
+
// same builder, preserving those headers too.
|
|
192
|
+
if (!ctx.responded && ctx.status === 404) {
|
|
193
|
+
ctx.json({ error: 'Not Found' });
|
|
194
|
+
}
|
|
195
|
+
return ctx.getResponse();
|
|
196
|
+
} catch (error) {
|
|
197
|
+
// Custom error handler
|
|
198
|
+
if (options.onError) {
|
|
199
|
+
return options.onError(error as Error, ctx);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Default error handling
|
|
203
|
+
app.logger.error('Request error:', error);
|
|
204
|
+
|
|
205
|
+
return jsonErrorResponse(500, 'Internal Server Error');
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Cloudflare Workers fetch handler type
|
|
212
|
+
*
|
|
213
|
+
* Cloudflare's module format passes `(request, env, ctx)` where:
|
|
214
|
+
* - `env` contains bindings (KV, D1, R2, secrets, etc.)
|
|
215
|
+
* - `ctx` provides `waitUntil()` and `passThroughOnException()`
|
|
216
|
+
*
|
|
217
|
+
* @typeParam Env - The shape of the Worker's bindings.
|
|
218
|
+
*/
|
|
219
|
+
export type CloudflareFetchHandler<Env = Record<string, unknown>> = (
|
|
220
|
+
request: Request,
|
|
221
|
+
env: Env,
|
|
222
|
+
ctx: EdgeExecutionContext
|
|
223
|
+
) => Response | Promise<Response>;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Create Cloudflare Workers module export
|
|
227
|
+
*
|
|
228
|
+
* @param app - NextRush Application instance
|
|
229
|
+
* @param options - Handler options
|
|
230
|
+
* @returns Cloudflare Workers module export object with correct `(request, env, ctx)` signature
|
|
231
|
+
*
|
|
232
|
+
* @remarks
|
|
233
|
+
* The Cloudflare `env` argument (KV, D1, R2, Durable Objects, Queues, secrets)
|
|
234
|
+
* is threaded onto the context as `ctx.env` (audit F-03). Pass a binding type
|
|
235
|
+
* as the generic to make `ctx.env` fully typed inside handlers.
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* ```typescript
|
|
239
|
+
* interface Env { MY_KV: KVNamespace }
|
|
240
|
+
* export default createCloudflareHandler<Env>(app);
|
|
241
|
+
* // inside a handler: ctx.env.MY_KV.get('key')
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
export function createCloudflareHandler<Env = Record<string, unknown>>(
|
|
245
|
+
app: Application,
|
|
246
|
+
options: FetchHandlerOptions = {}
|
|
247
|
+
): { fetch: CloudflareFetchHandler<Env> } {
|
|
248
|
+
const run = createRequestRunner(app, options);
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
fetch: (request: Request, env: Env, ctx: EdgeExecutionContext): Promise<Response> =>
|
|
252
|
+
run(request, ctx, env),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Create Vercel Edge Function handler
|
|
258
|
+
*
|
|
259
|
+
* @param app - NextRush Application instance
|
|
260
|
+
* @param options - Handler options
|
|
261
|
+
* @returns Vercel Edge Function handler
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```typescript
|
|
265
|
+
* // api/hello.ts
|
|
266
|
+
* import { createApp } from '@nextrush/core';
|
|
267
|
+
* import { createVercelHandler } from '@nextrush/adapter-edge';
|
|
268
|
+
*
|
|
269
|
+
* const app = createApp();
|
|
270
|
+
*
|
|
271
|
+
* app.use(async (ctx) => {
|
|
272
|
+
* ctx.json({
|
|
273
|
+
* message: 'Hello from Vercel Edge!',
|
|
274
|
+
* region: process.env.VERCEL_REGION
|
|
275
|
+
* });
|
|
276
|
+
* });
|
|
277
|
+
*
|
|
278
|
+
* export const config = { runtime: 'edge' };
|
|
279
|
+
* export default createVercelHandler(app);
|
|
280
|
+
* ```
|
|
281
|
+
*/
|
|
282
|
+
export function createVercelHandler(
|
|
283
|
+
app: Application,
|
|
284
|
+
options: FetchHandlerOptions = {}
|
|
285
|
+
): FetchHandler {
|
|
286
|
+
return createFetchHandler(app, options);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Create Netlify Edge Function handler
|
|
291
|
+
*
|
|
292
|
+
* @param app - NextRush Application instance
|
|
293
|
+
* @param options - Handler options
|
|
294
|
+
* @returns Netlify Edge Function handler
|
|
295
|
+
*
|
|
296
|
+
* @example
|
|
297
|
+
* ```typescript
|
|
298
|
+
* // netlify/edge-functions/api.ts
|
|
299
|
+
* import { createApp } from '@nextrush/core';
|
|
300
|
+
* import { createNetlifyHandler } from '@nextrush/adapter-edge';
|
|
301
|
+
*
|
|
302
|
+
* const app = createApp();
|
|
303
|
+
*
|
|
304
|
+
* app.use(async (ctx) => {
|
|
305
|
+
* ctx.json({ message: 'Hello from Netlify Edge!' });
|
|
306
|
+
* });
|
|
307
|
+
*
|
|
308
|
+
* export default createNetlifyHandler(app);
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
export function createNetlifyHandler(
|
|
312
|
+
app: Application,
|
|
313
|
+
options: FetchHandlerOptions = {}
|
|
314
|
+
): FetchHandler {
|
|
315
|
+
return createFetchHandler(app, options);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Alias for backwards compatibility and consistency
|
|
319
|
+
export const createHandler = createFetchHandler;
|
|
320
|
+
|
|
321
|
+
// F-01: compile-time conformance guard. If the exported fetch-adapter shape
|
|
322
|
+
// drifts from the shared `FetchAdapter` contract, this stops compiling.
|
|
323
|
+
const _edgeConformance: FetchAdapter<Application, EdgeExecutionContext> = { createFetchHandler };
|
|
324
|
+
void _edgeConformance;
|
|
325
|
+
|
|
326
|
+
// RFC-NEXTRUSH-ADAPTER-CONTRACT: prove the context factory produces an
|
|
327
|
+
// AdapterContext over the shared Context contract. A drift in createEdgeContext's
|
|
328
|
+
// return type stops compiling here.
|
|
329
|
+
const _edgeContextFactory: AdapterContextFactory<
|
|
330
|
+
[Request, EdgeExecutionContext?, boolean?, unknown?],
|
|
331
|
+
EdgeContext
|
|
332
|
+
> = createEdgeContext;
|
|
333
|
+
void _edgeContextFactory;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nextrush/adapter-edge - Body Source
|
|
3
|
+
*
|
|
4
|
+
* The bespoke `EdgeBodySource`/`EmptyBodySource`/`concatUint8Arrays` have been
|
|
5
|
+
* collapsed onto the shared cross-runtime {@link WebBodySource} in
|
|
6
|
+
* `@nextrush/runtime` (audit F-04a). Body-reading behavior is now identical
|
|
7
|
+
* across the Web adapters (Bun/Deno/Edge) and fixed in one place.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
createEmptyBodySource,
|
|
14
|
+
createWebBodySource,
|
|
15
|
+
EmptyBodySource,
|
|
16
|
+
WebBodySource,
|
|
17
|
+
} from '@nextrush/runtime';
|
|
18
|
+
|
|
19
|
+
export { createEmptyBodySource, createWebBodySource, EmptyBodySource, WebBodySource };
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nextrush/adapter-edge - Context Implementation
|
|
3
|
+
*
|
|
4
|
+
* Edge-specific Context implementation for Cloudflare Workers, Vercel Edge, etc.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Extends the shared {@link WebContextBase} (F-08, ADR-0010), which owns the
|
|
8
|
+
* response-building logic (json/send/html/redirect/set/getResponse and body
|
|
9
|
+
* suppression, composed from {@link WebResponseBuilder}), the lazy `raw`/
|
|
10
|
+
* `signal`/`triggerTimeout`, the streaming methods, and
|
|
11
|
+
* `get`/`next`/`throw`/`assert` — defined once across the Web adapters rather
|
|
12
|
+
* than copy-pasted per runtime. This file supplies only what is genuinely
|
|
13
|
+
* Edge-specific: `cf-connecting-ip`-aware IP resolution, platform bindings
|
|
14
|
+
* (`env`), and `waitUntil`/`executionContext`.
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { detectEdgeRuntime, getEdgeClientIp, WebContextBase } from '@nextrush/runtime';
|
|
20
|
+
import { runNDJSONStream, runSSEStream, runTextStream } from '@nextrush/stream';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Edge execution context interface
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* Provides access to edge-specific features like `waitUntil` and `passThroughOnException`.
|
|
27
|
+
*/
|
|
28
|
+
export interface EdgeExecutionContext {
|
|
29
|
+
/** Extend request lifetime for async operations */
|
|
30
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
31
|
+
|
|
32
|
+
/** Pass through to origin on exception */
|
|
33
|
+
passThroughOnException?(): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Edge Context implementation
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* Works with any edge runtime that implements the Web Fetch API:
|
|
41
|
+
* - Cloudflare Workers
|
|
42
|
+
* - Vercel Edge Functions
|
|
43
|
+
* - Netlify Edge Functions
|
|
44
|
+
*
|
|
45
|
+
* The response is built internally (via the inherited {@link WebResponseBuilder}
|
|
46
|
+
* composition) and returned via `getResponse()`.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const ctx = new EdgeContext(request);
|
|
51
|
+
* ctx.json({ message: 'Hello from Edge!' });
|
|
52
|
+
* const response = ctx.getResponse();
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export class EdgeContext<Env = unknown> extends WebContextBase {
|
|
56
|
+
/** Edge execution context (for waitUntil, etc.) */
|
|
57
|
+
readonly executionContext?: EdgeExecutionContext;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Platform bindings passed by the runtime (audit F-03).
|
|
61
|
+
*
|
|
62
|
+
* @remarks
|
|
63
|
+
* On Cloudflare Workers this is the `env` argument of
|
|
64
|
+
* `fetch(request, env, ctx)` — KV, D1, R2, Durable Objects, Queues, secrets.
|
|
65
|
+
* `undefined` on runtimes that do not supply bindings (e.g. Vercel Edge).
|
|
66
|
+
*/
|
|
67
|
+
readonly env?: Env;
|
|
68
|
+
|
|
69
|
+
constructor(
|
|
70
|
+
request: Request,
|
|
71
|
+
executionContext?: EdgeExecutionContext,
|
|
72
|
+
trustProxy = false,
|
|
73
|
+
env?: Env
|
|
74
|
+
) {
|
|
75
|
+
// Get client IP from CF headers or standard headers.
|
|
76
|
+
//
|
|
77
|
+
// HP-1 trim: Edge has no socket, so when `trustProxy` is false (default) the
|
|
78
|
+
// client IP is `''` — returned directly, with no per-request header-lookup
|
|
79
|
+
// closure and no `getEdgeClientIp` policy call (byte-identical to
|
|
80
|
+
// `getEdgeClientIp(request, false)`, whose `directIp` is `''`). When true,
|
|
81
|
+
// resolution still goes through `getEdgeClientIp`, preserving the Cloudflare
|
|
82
|
+
// `cf-connecting-ip` → `x-forwarded-for` → `x-real-ip` precedence.
|
|
83
|
+
const ip = trustProxy ? getEdgeClientIp(request, true) : '';
|
|
84
|
+
|
|
85
|
+
// Detect specific edge runtime
|
|
86
|
+
const runtime = detectEdgeRuntime().runtime;
|
|
87
|
+
|
|
88
|
+
super(request, ip, runtime, { runTextStream, runSSEStream, runNDJSONStream });
|
|
89
|
+
|
|
90
|
+
this.executionContext = executionContext;
|
|
91
|
+
this.env = env;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ===========================================================================
|
|
95
|
+
// Edge-Specific Methods
|
|
96
|
+
// ===========================================================================
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Extend request lifetime for async operations
|
|
100
|
+
*
|
|
101
|
+
* @remarks
|
|
102
|
+
* Use this for fire-and-forget operations that should complete
|
|
103
|
+
* after the response is sent (logging, analytics, etc.)
|
|
104
|
+
*/
|
|
105
|
+
waitUntil(promise: Promise<unknown>): void {
|
|
106
|
+
if (this.executionContext?.waitUntil) {
|
|
107
|
+
this.executionContext.waitUntil(promise);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create a new EdgeContext
|
|
114
|
+
*/
|
|
115
|
+
export function createEdgeContext<Env = unknown>(
|
|
116
|
+
request: Request,
|
|
117
|
+
executionContext?: EdgeExecutionContext,
|
|
118
|
+
trustProxy = false,
|
|
119
|
+
env?: Env
|
|
120
|
+
): EdgeContext<Env> {
|
|
121
|
+
return new EdgeContext<Env>(request, executionContext, trustProxy, env);
|
|
122
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nextrush/adapter-edge - Edge Runtime Adapter for NextRush
|
|
3
|
+
*
|
|
4
|
+
* Provides universal Edge runtime support for:
|
|
5
|
+
* - Cloudflare Workers
|
|
6
|
+
* - Vercel Edge Functions
|
|
7
|
+
* - Netlify Edge Functions
|
|
8
|
+
* - Any runtime supporting the Fetch API
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
* @module @nextrush/adapter-edge
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Main adapter functions
|
|
15
|
+
export {
|
|
16
|
+
createCloudflareHandler,
|
|
17
|
+
createFetchHandler,
|
|
18
|
+
createHandler,
|
|
19
|
+
createNetlifyHandler,
|
|
20
|
+
createVercelHandler,
|
|
21
|
+
DEFAULT_EDGE_TIMEOUT_MS,
|
|
22
|
+
type CloudflareFetchHandler, // Alias
|
|
23
|
+
type FetchHandler,
|
|
24
|
+
type FetchHandlerOptions,
|
|
25
|
+
} from './adapter';
|
|
26
|
+
|
|
27
|
+
// Context exports
|
|
28
|
+
export { EdgeContext, createEdgeContext, type EdgeExecutionContext } from './context';
|
|
29
|
+
|
|
30
|
+
// HttpError re-export (uniform across all adapters — audit F-10)
|
|
31
|
+
export { HttpError } from '@nextrush/errors';
|
|
32
|
+
|
|
33
|
+
// Body source exports (F-10: previously only `EdgeBodySource` was exported;
|
|
34
|
+
// the rest were dead. Now the full shared surface is wired up.)
|
|
35
|
+
export {
|
|
36
|
+
createEmptyBodySource,
|
|
37
|
+
createWebBodySource,
|
|
38
|
+
EmptyBodySource,
|
|
39
|
+
WebBodySource,
|
|
40
|
+
} from './body-source';
|
|
41
|
+
|
|
42
|
+
// Shared error classes (parity with node/bun/deno — audit F-10)
|
|
43
|
+
export { BodyConsumedError, BodyTooLargeError } from '@nextrush/runtime';
|
|
44
|
+
|
|
45
|
+
// Utility exports
|
|
46
|
+
/* eslint-disable @typescript-eslint/no-deprecated -- F-09: intentional compat re-export, not a usage site */
|
|
47
|
+
export {
|
|
48
|
+
detectEdgeRuntime,
|
|
49
|
+
getContentLength,
|
|
50
|
+
getContentType,
|
|
51
|
+
parseQueryString,
|
|
52
|
+
type EdgeRuntimeInfo,
|
|
53
|
+
} from './utils';
|
|
54
|
+
/* eslint-enable @typescript-eslint/no-deprecated */
|
|
55
|
+
|
|
56
|
+
// Re-export types for convenience (parity with node/bun/deno — audit F-10)
|
|
57
|
+
export type {
|
|
58
|
+
BodySource,
|
|
59
|
+
Context,
|
|
60
|
+
HttpMethod,
|
|
61
|
+
Middleware,
|
|
62
|
+
Runtime,
|
|
63
|
+
RuntimeCapabilities,
|
|
64
|
+
} from '@nextrush/types';
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nextrush/adapter-edge - Utility Functions
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { detectEdgeRuntime, parseQueryString } from '@nextrush/runtime';
|
|
8
|
+
export type { EdgeRuntimeInfo } from '@nextrush/runtime';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get Content-Type header value
|
|
12
|
+
*
|
|
13
|
+
* @deprecated F-09: unused internally (superseded by `@nextrush/body-parser`'s
|
|
14
|
+
* own content-type parsing). Kept for backward compatibility per the public-API
|
|
15
|
+
* contract (deprecate-before-remove); will be removed in a future major.
|
|
16
|
+
*/
|
|
17
|
+
export function getContentType(headers: Headers): string | undefined {
|
|
18
|
+
return headers.get('content-type') ?? undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get Content-Length header as number
|
|
23
|
+
*
|
|
24
|
+
* @deprecated F-09: unused internally (superseded by `@nextrush/body-parser`'s
|
|
25
|
+
* own content-length handling). Kept for backward compatibility per the
|
|
26
|
+
* public-API contract (deprecate-before-remove); will be removed in a future
|
|
27
|
+
* major.
|
|
28
|
+
*/
|
|
29
|
+
export function getContentLength(headers: Headers): number | undefined {
|
|
30
|
+
const value = headers.get('content-length');
|
|
31
|
+
if (value === null) return undefined;
|
|
32
|
+
const num = parseInt(value, 10);
|
|
33
|
+
return Number.isNaN(num) ? undefined : num;
|
|
34
|
+
}
|