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