@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tanzim (NextRush)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,386 @@
1
+ # @nextrush/adapter-edge
2
+
3
+ > Runs a NextRush app on any Fetch API edge runtime -- Cloudflare Workers, Vercel Edge
4
+ > Functions, and Netlify Edge Functions -- through one shared handler implementation.
5
+
6
+ [![npm version](https://img.shields.io/npm/v/@nextrush/adapter-edge.svg)](https://www.npmjs.com/package/@nextrush/adapter-edge)
7
+ [![downloads](https://img.shields.io/npm/dm/@nextrush/adapter-edge.svg)](https://www.npmjs.com/package/@nextrush/adapter-edge)
8
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@nextrush/adapter-edge.svg)](https://bundlephobia.com/package/@nextrush/adapter-edge)
9
+ [![types](https://img.shields.io/npm/types/@nextrush/adapter-edge.svg)](https://www.npmjs.com/package/@nextrush/adapter-edge)
10
+ [![ESM only](https://img.shields.io/badge/module-ESM--only-blue.svg)](https://nodejs.org/api/esm.html)
11
+ [![license](https://img.shields.io/npm/l/@nextrush/adapter-edge.svg)](https://github.com/0xTanzim/nextRush/blob/main/LICENSE)
12
+
13
+ | | |
14
+ | --- | --- |
15
+ | **Purpose** | Fetch-API entry points that run a NextRush `Application` on edge runtimes |
16
+ | **Package type** | Adapter |
17
+ | **Status** | Stable (implementation) -- but see Support tier below |
18
+ | **Included in `nextrush`?** | No -- standalone install |
19
+ | **Support tier** | Internal -- non-`-node` adapter until GA, may change without a major (see [ADR-0005](https://github.com/0xTanzim/nextRush/blob/main/docs/adr/ADR-0005-package-tiers-sealed-surface-deprecation.md)) |
20
+ | **Maintenance** | Active |
21
+ | **Runtime** | Edge -- any runtime implementing the Fetch API (Cloudflare Workers, Vercel Edge, Netlify Edge) |
22
+ | **Requires** | Node `>=22` for local tooling/build -- ESM-only -- TypeScript `>=5.x` |
23
+ | **Introduced** | `v1.0.0` |
24
+
25
+ ## Highlights
26
+
27
+ - Zero external runtime dependencies -- only `@nextrush/{core,errors,runtime,stream,types}` (workspace packages)
28
+ - ESM-only, tree-shakable, side-effect-free (`sideEffects: false`)
29
+ - One shared request runner behind three platform-specific entry points -- Cloudflare, Vercel,
30
+ and Netlify handlers cannot silently drift from each other
31
+ - Fully typed -- strict TypeScript, zero `any`
32
+
33
+ ## The problem
34
+
35
+ Edge runtimes agree on the Fetch API (`Request` in, `Response` out) but disagree on the
36
+ surrounding module contract: Cloudflare Workers export `{ fetch(request, env, ctx) }`, Vercel
37
+ Edge Functions export a plain `(request) => Response` with `export const config = { runtime:
38
+ 'edge' }`, and Netlify Edge Functions export the same plain shape again. Writing a NextRush app
39
+ against each platform's raw contract by hand means re-deriving request timeout handling,
40
+ 404/error fallback, and `Application` boot sequencing three separate times -- and one of those
41
+ copies quietly drifting is how a platform-specific bug ships.
42
+
43
+ ```ts
44
+ // TODAY, without this package -- three near-duplicate handlers, easy to drift:
45
+ export default {
46
+ fetch(request: Request, env: Env, ctx: ExecutionContext) {
47
+ // boot the app, race a timeout, build a Response... written by hand, three times
48
+ },
49
+ };
50
+ ```
51
+
52
+ `@nextrush/adapter-edge` centralizes that request-handling logic in one internal runner and
53
+ exposes a thin, platform-shaped wrapper per target.
54
+
55
+ ## When to use
56
+
57
+ **Use `@nextrush/adapter-edge` if:**
58
+
59
+ - You are deploying a NextRush app to Cloudflare Workers, Vercel Edge Functions, or Netlify
60
+ Edge Functions.
61
+ - Your app only needs the Web-standard `Request`/`Response`/`fetch` surface -- no Node.js APIs.
62
+
63
+ **Reach for something else if:**
64
+
65
+ - You are deploying to a long-running Node.js process -> use
66
+ [`@nextrush/adapter-node`](../node) instead.
67
+ - You are deploying to AWS Lambda, Google Cloud Functions, or another FaaS platform with its
68
+ own event format -> use [`@nextrush/adapter-serverless`](../serverless) (built on top of this
69
+ package's fetch handler -- see Package relationships below).
70
+ - You are running on Bun or Deno directly (not via an edge platform) -> use
71
+ [`@nextrush/adapter-bun`](../bun) or [`@nextrush/adapter-deno`](../deno).
72
+
73
+ ## Installation
74
+
75
+ ```bash
76
+ pnpm add @nextrush/adapter-edge @nextrush/core
77
+ # npm i @nextrush/adapter-edge @nextrush/core
78
+ # yarn add @nextrush/adapter-edge @nextrush/core
79
+ # bun add @nextrush/adapter-edge @nextrush/core
80
+ ```
81
+
82
+ > [!NOTE]
83
+ > Not included in the `nextrush` meta package. Install it directly when deploying to an edge
84
+ > runtime.
85
+
86
+ ## Quick start
87
+
88
+ ```ts
89
+ // Cloudflare Workers -- src/index.ts
90
+ import { createApp } from '@nextrush/core';
91
+ import { createCloudflareHandler } from '@nextrush/adapter-edge';
92
+
93
+ const app = createApp();
94
+
95
+ app.use(async (ctx) => {
96
+ ctx.json({ message: 'Hello from the edge', runtime: ctx.runtime });
97
+ });
98
+
99
+ export default createCloudflareHandler(app);
100
+ ```
101
+
102
+ The handler boots the `Application` on the first incoming request (there is no separate
103
+ `listen()` step on edge), then reuses that boot for every subsequent request in the same
104
+ isolate.
105
+
106
+ ## Capabilities
107
+
108
+ **Capabilities**
109
+ - **Three platform entry points, one runner** -- `createCloudflareHandler`,
110
+ `createVercelHandler`, and `createNetlifyHandler` all delegate to the same internal request
111
+ runner (`createFetchHandler` is the runner exposed directly, for any other Fetch-API host).
112
+ - **Cloudflare `env` bindings threaded onto context** -- `createCloudflareHandler`'s `env`
113
+ argument (KV, D1, R2, Durable Objects, Queues, secrets) is exposed as `ctx.env`, typed via a
114
+ generic.
115
+ - **Cooperative request timeout** -- races the handler against a timer; on timeout, aborts
116
+ `ctx.signal` and returns `504`, so the framework's own bounded timeout fires before the
117
+ platform kills the isolate.
118
+ - **`waitUntil` for fire-and-forget work** -- `ctx.waitUntil(promise)` extends the request
119
+ lifetime for background tasks (logging, analytics) when the platform provides an execution
120
+ context; a no-op otherwise.
121
+
122
+ **Developer experience**
123
+ - Fully typed exports, including a compile-time guard (`FetchAdapter`,
124
+ `AdapterContextFactory`) that fails the build if this adapter's shape drifts from the shared
125
+ cross-adapter contract.
126
+
127
+ ## Mental model
128
+
129
+ ```text
130
+ Request (Fetch API) --> createXxxHandler --> shared request runner --> Application.callback()
131
+ |
132
+ +-- races Application against a timeout timer
133
+ +-- builds Response via EdgeContext
134
+ ```
135
+
136
+ **Rule:** every platform-specific handler (`createCloudflareHandler`, `createVercelHandler`,
137
+ `createNetlifyHandler`) is a thin wrapper around the same internal runner -- there is exactly
138
+ one request-handling code path to reason about, regardless of platform.
139
+
140
+ > [!TIP]
141
+ > The full request lifecycle (Mermaid) is in [`ARCHITECTURE.md`](./ARCHITECTURE.md).
142
+
143
+ ## Common tasks
144
+
145
+ ### Deploy to Cloudflare Workers
146
+
147
+ ```ts
148
+ // src/index.ts
149
+ import { createApp } from '@nextrush/core';
150
+ import { createCloudflareHandler } from '@nextrush/adapter-edge';
151
+
152
+ interface Env {
153
+ MY_KV: KVNamespace;
154
+ }
155
+
156
+ const app = createApp();
157
+ app.use(async (ctx) => {
158
+ const value = await ctx.env?.MY_KV.get('key');
159
+ ctx.json({ value });
160
+ });
161
+
162
+ export default createCloudflareHandler<Env>(app);
163
+ ```
164
+
165
+ ```toml
166
+ # wrangler.toml
167
+ name = "my-nextrush-worker"
168
+ main = "src/index.ts"
169
+ compatibility_date = "2025-01-01"
170
+ ```
171
+
172
+ ### Deploy to Vercel Edge Functions
173
+
174
+ ```ts
175
+ // api/hello.ts
176
+ import { createApp } from '@nextrush/core';
177
+ import { createVercelHandler } from '@nextrush/adapter-edge';
178
+
179
+ const app = createApp();
180
+ app.use(async (ctx) => {
181
+ ctx.json({ message: 'Hello from Vercel Edge' });
182
+ });
183
+
184
+ export const config = { runtime: 'edge' };
185
+ export default createVercelHandler(app);
186
+ ```
187
+
188
+ ### Deploy to Netlify Edge Functions
189
+
190
+ ```ts
191
+ // netlify/edge-functions/api.ts
192
+ import { createApp } from '@nextrush/core';
193
+ import { createNetlifyHandler } from '@nextrush/adapter-edge';
194
+
195
+ const app = createApp();
196
+ app.use(async (ctx) => {
197
+ ctx.json({ message: 'Hello from Netlify Edge' });
198
+ });
199
+
200
+ export default createNetlifyHandler(app);
201
+ ```
202
+
203
+ ### Read the request body across runtimes
204
+
205
+ ```ts
206
+ app.use(async (ctx) => {
207
+ const text = await ctx.bodySource.text();
208
+ const json = await ctx.bodySource.json();
209
+ const buffer = await ctx.bodySource.buffer();
210
+ const stream = ctx.bodySource.stream();
211
+ });
212
+ ```
213
+
214
+ ### Handle errors with a custom handler
215
+
216
+ ```ts
217
+ const handler = createCloudflareHandler(app, {
218
+ onError: (error, ctx) => {
219
+ console.error({ error: error.message, path: ctx.path });
220
+ return new Response(JSON.stringify({ error: 'Internal Server Error' }), {
221
+ status: 500,
222
+ headers: { 'Content-Type': 'application/json' },
223
+ });
224
+ },
225
+ });
226
+ ```
227
+
228
+ ## API overview
229
+
230
+ | Export | Signature | Since | Stability | Description |
231
+ | ------ | --------- | ----- | --------- | ------------ |
232
+ | `createFetchHandler` | `(app: Application, options?: FetchHandlerOptions) => FetchHandler` | `1.0.0` | Internal | Generic Fetch-API handler; the runner every other export delegates to |
233
+ | `createCloudflareHandler` | `<Env>(app: Application, options?: FetchHandlerOptions) => { fetch: CloudflareFetchHandler<Env> }` | `1.0.0` | Internal | Cloudflare Workers module export -- `(request, env, ctx)` signature; threads `env` onto `ctx.env` |
234
+ | `createVercelHandler` | `(app: Application, options?: FetchHandlerOptions) => FetchHandler` | `1.0.0` | Internal | Vercel Edge Function handler (delegates to `createFetchHandler`) |
235
+ | `createNetlifyHandler` | `(app: Application, options?: FetchHandlerOptions) => FetchHandler` | `1.0.0` | Internal | Netlify Edge Function handler (delegates to `createFetchHandler`) |
236
+ | `createHandler` | same as `createFetchHandler` | `1.0.0` | Internal | Alias of `createFetchHandler`, kept for naming consistency with other adapters |
237
+ | `DEFAULT_EDGE_TIMEOUT_MS` | `25_000` | `1.0.0` | Internal | Default request timeout in ms when `options.timeout` is omitted |
238
+ | `detectEdgeRuntime` | `() => EdgeRuntimeInfo` | `1.0.0` | Internal | Detects which edge platform is running -- see Runtime detection below |
239
+ | `EdgeContext` | class | `1.0.0` | Internal | `Context` implementation for edge runtimes |
240
+ | `createEdgeContext` | `<Env>(request, executionContext?, trustProxy?, env?) => EdgeContext<Env>` | `1.0.0` | Internal | Constructs an `EdgeContext` directly |
241
+ | `type FetchHandler` | `(request: Request, ctx?: EdgeExecutionContext) => Response \| Promise<Response>` | `1.0.0` | Internal | The handler signature every `createXxxHandler` returns |
242
+ | `type EdgeExecutionContext` | `{ waitUntil, passThroughOnException? }` | `1.0.0` | Internal | Platform execution-context shape consumed by `ctx.waitUntil` |
243
+ | `type EdgeRuntimeInfo` | `{ runtime, isCloudflare, isVercel, isNetlify, isGenericEdge }` | `1.0.0` | Internal | Result shape of `detectEdgeRuntime()` |
244
+ | `HttpError` | re-exported from `@nextrush/errors` | `1.0.0` | Internal | Uniform error class across all adapters |
245
+
246
+ ## Options
247
+
248
+ `FetchHandlerOptions`, accepted by every `createXxxHandler`:
249
+
250
+ | Option | Type | Required | Default | Security-sensitive | Description |
251
+ | ------ | ---- | -------- | ------- | ------------------- | ------------ |
252
+ | `onError` | `(error: Error, ctx: EdgeContext) => Response \| Promise<Response>` | No | built-in JSON 500 | -- | Custom error handler; receives the thrown error and the in-flight context |
253
+ | `timeout` | `number` | No | `25000` (`DEFAULT_EDGE_TIMEOUT_MS`) | -- | Request timeout in ms; races the handler and returns `504` on expiry, aborting `ctx.signal`. `0` disables the framework timeout (the platform's own limit still applies) |
254
+
255
+ ## Runtime detection
256
+
257
+ `detectEdgeRuntime()` (re-exported from `@nextrush/runtime`) answers "which edge platform am I
258
+ on", distinct from the generic `detectRuntime()`. Verified against
259
+ `packages/runtime/src/detection.ts`, it checks exactly three platforms in order and has no
260
+ AWS/Lambda branch:
261
+
262
+ 1. **Cloudflare Workers** -- `navigator.userAgent` contains `'Cloudflare-Workers'` -> `runtime:
263
+ 'cloudflare-workers'`, `isCloudflare: true`.
264
+ 2. **Vercel Edge** -- `process.env.VERCEL_REGION` is defined -> `runtime: 'vercel-edge'`,
265
+ `isVercel: true`.
266
+ 3. **Netlify Edge** -- a `Deno` global exists AND `process.env.NETLIFY === 'true'` (Netlify Edge
267
+ Functions run on Deno under the hood) -> `runtime: 'edge'` (not a distinct string),
268
+ `isNetlify: true`.
269
+
270
+ If none of the three match, `runtime` stays at its initial value of `'edge'` and
271
+ `isGenericEdge` is `true`. There is no fourth branch for AWS Lambda or any other FaaS platform
272
+ -- `@nextrush/adapter-serverless` builds its own handler on top of this package's fetch runner
273
+ and inherits this same detection, so `ctx.runtime` reports `'edge'` there too (see
274
+ [`@nextrush/adapter-serverless`](../serverless)'s own docs for that package's scope).
275
+
276
+ ```ts
277
+ import { detectEdgeRuntime } from '@nextrush/adapter-edge';
278
+
279
+ const info = detectEdgeRuntime();
280
+ // Cloudflare Workers: { runtime: 'cloudflare-workers', isCloudflare: true, isVercel: false, isNetlify: false, isGenericEdge: false }
281
+ // Vercel Edge: { runtime: 'vercel-edge', isCloudflare: false, isVercel: true, isNetlify: false, isGenericEdge: false }
282
+ // Netlify Edge: { runtime: 'edge', isCloudflare: false, isVercel: false, isNetlify: true, isGenericEdge: false }
283
+ // Anything else: { runtime: 'edge', isCloudflare: false, isVercel: false, isNetlify: false, isGenericEdge: true }
284
+ ```
285
+
286
+ Result is cached after the first call within a given isolate.
287
+
288
+ ## Compatibility
289
+
290
+ **Requirements**
291
+
292
+ | Requirement | Version |
293
+ | ----------- | ------- |
294
+ | NextRush | `1.x` (`@nextrush/core`) |
295
+ | Node.js (local build/test only) | `>=22` |
296
+ | TypeScript | `>=5.x` |
297
+
298
+ **Runtimes**
299
+
300
+ | Runtime | Supported | Notes |
301
+ | ------- | --------- | ------ |
302
+ | Cloudflare Workers | Yes | via `createCloudflareHandler` |
303
+ | Vercel Edge Functions | Yes | via `createVercelHandler` |
304
+ | Netlify Edge Functions | Yes | via `createNetlifyHandler` (runs on Deno under the hood) |
305
+ | Node.js | No | use [`@nextrush/adapter-node`](../node) |
306
+
307
+ **Integration**
308
+ - **Peer dependencies:** none -- direct workspace dependencies on `@nextrush/{core,errors,runtime,stream,types}`.
309
+ - **Works with:** any NextRush middleware that only touches the `Context` API (no raw Node
310
+ `req`/`res` access).
311
+ - **Built on by:** [`@nextrush/adapter-serverless`](../serverless), which wraps this package's
312
+ fetch handler for FaaS platforms (AWS Lambda, Google Cloud Functions).
313
+
314
+ > [!IMPORTANT]
315
+ > NextRush is ESM-only, permanently -- no CommonJS build. See the
316
+ > [Module Format Policy](https://github.com/0xTanzim/nextRush#module-format-policy).
317
+
318
+ ## Limitations
319
+
320
+ Edge runtimes share constraints this package does not paper over:
321
+
322
+ - No file system access -- use platform storage (R2, KV) or an external store.
323
+ - No native Node.js modules or native addons -- Web API equivalents only.
324
+ - CPU time and memory limits vary by platform -- consult the platform's own documentation.
325
+ - Large payloads should be streamed rather than buffered in memory.
326
+
327
+ ## FAQ
328
+
329
+ **Does `ctx.runtime` tell me I am on AWS Lambda?**
330
+ No. `detectEdgeRuntime()` has no Lambda branch; it only distinguishes Cloudflare, Vercel, and
331
+ Netlify, defaulting to the generic `'edge'` otherwise. `@nextrush/adapter-serverless` is built
332
+ on this package and inherits the same detection, so Lambda deployments also report
333
+ `runtime: 'edge'` -- verified in `packages/runtime/src/detection.ts`.
334
+
335
+ **Why is this package tier "Internal" instead of "Public"?**
336
+ Per [ADR-0005](https://github.com/0xTanzim/nextRush/blob/main/docs/adr/ADR-0005-package-tiers-sealed-surface-deprecation.md),
337
+ non-`-node` adapters (bun, deno, edge, serverless) stay Internal until each is declared GA;
338
+ `@nextrush/adapter-node` is the only adapter in the Public-core tier today.
339
+
340
+ **Does it work without `@nextrush/router`?**
341
+ Yes -- `app.use()` alone is enough for simple handlers. Add
342
+ [`@nextrush/router`](../../router) only when you need dynamic route matching.
343
+
344
+ **Can I use it outside Cloudflare/Vercel/Netlify?**
345
+ Yes -- `createFetchHandler` (and its alias `createHandler`) is a plain `(request, ctx?) =>
346
+ Response` function usable on any host that speaks the Fetch API, not only the three named
347
+ platforms.
348
+
349
+ ## Package relationships
350
+
351
+ ```text
352
+ depends on @nextrush/{core,errors,runtime,stream,types}
353
+ @nextrush/adapter-edge -------------->
354
+ often used with @nextrush/router
355
+ usually used next @nextrush/adapter-serverless (for FaaS platforms)
356
+ ```
357
+
358
+ - **Depends on:** [`@nextrush/core`](../../core), [`@nextrush/errors`](../../errors),
359
+ [`@nextrush/runtime`](../../runtime), [`@nextrush/stream`](../../stream),
360
+ [`@nextrush/types`](../../types).
361
+ - **Often used with:** [`@nextrush/router`](../../router) -- for dynamic route matching beyond
362
+ a single `app.use()` handler.
363
+ - **Usually used next:** [`@nextrush/adapter-serverless`](../serverless) -- if the deploy target
364
+ turns out to be AWS Lambda or Google Cloud Functions rather than an edge platform.
365
+ - **Alternative:** [`@nextrush/adapter-node`](../node) -- for a long-running Node.js process
366
+ instead of an edge isolate; [`@nextrush/adapter-bun`](../bun) / [`@nextrush/adapter-deno`](../deno)
367
+ -- for running directly on Bun/Deno outside an edge platform.
368
+
369
+ ## Architecture
370
+
371
+ Maintaining or contributing to this package? The internal design -- the shared request runner,
372
+ context construction, and the compile-time adapter-contract guards -- is in
373
+ [`ARCHITECTURE.md`](./ARCHITECTURE.md). Design history:
374
+ [RFC-013 (adapter contract)](https://github.com/0xTanzim/nextRush/blob/main/docs/RFC/runtime-adapters/013-adapter-contract.md),
375
+ [ADR-0010 (cross-runtime parity hardening)](https://github.com/0xTanzim/nextRush/blob/main/docs/adr/ADR-0010-cross-runtime-parity-hardening.md).
376
+
377
+ ## Resources
378
+
379
+ - Learn -- [Documentation](https://0xtanzim.github.io/nextRush/docs) -- [Architecture](./ARCHITECTURE.md) -- [RFCs](https://github.com/0xTanzim/nextRush/tree/main/docs/RFC)
380
+ - Changelog -- [CHANGELOG.md](./CHANGELOG.md)
381
+ - Report an issue -- [GitHub Issues](https://github.com/0xTanzim/nextRush/issues)
382
+ - Contribute -- [CONTRIBUTING.md](https://github.com/0xTanzim/nextRush/blob/main/CONTRIBUTING.md)
383
+
384
+ ---
385
+
386
+ MIT (c) [Tanzim Hossain](https://github.com/0xTanzim)