@nextrush/adapter-edge 1.0.0-beta.0 → 1.0.0-beta.1
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/README.md +67 -4
- package/dist/index.d.ts +72 -19
- package/dist/index.js +63 -9
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/boot-reuse-warning.test.ts +95 -0
- package/src/__tests__/context-ip-warning.test.ts +67 -0
- package/src/__tests__/context-platform.test.ts +41 -0
- package/src/__tests__/context.test.ts +40 -5
- package/src/__tests__/default-timeout.test.ts +5 -2
- package/src/__tests__/per-request-work-trim.test.ts +14 -14
- package/src/__tests__/timeout-attribution.test.ts +80 -0
- package/src/adapter.ts +84 -14
- package/src/context.ts +86 -10
- package/src/index.ts +1 -0
- package/src/utils.ts +4 -7
package/README.md
CHANGED
|
@@ -70,6 +70,11 @@ exposes a thin, platform-shaped wrapper per target.
|
|
|
70
70
|
- You are running on Bun or Deno directly (not via an edge platform) -> use
|
|
71
71
|
[`@nextrush/adapter-bun`](../bun) or [`@nextrush/adapter-deno`](../deno).
|
|
72
72
|
|
|
73
|
+
> [!TIP]
|
|
74
|
+
> Still unsure which of `adapter-edge` vs. `adapter-serverless` a given platform needs? See the
|
|
75
|
+
> [full "which package do I install?" table](https://github.com/0xTanzim/nextRush/blob/main/apps/website/content/docs/start/runtime/decision-guide.mdx) --
|
|
76
|
+
> one shared answer instead of re-deriving it per package.
|
|
77
|
+
|
|
73
78
|
## Installation
|
|
74
79
|
|
|
75
80
|
```bash
|
|
@@ -234,7 +239,7 @@ const handler = createCloudflareHandler(app, {
|
|
|
234
239
|
| `createVercelHandler` | `(app: Application, options?: FetchHandlerOptions) => FetchHandler` | `1.0.0` | Internal | Vercel Edge Function handler (delegates to `createFetchHandler`) |
|
|
235
240
|
| `createNetlifyHandler` | `(app: Application, options?: FetchHandlerOptions) => FetchHandler` | `1.0.0` | Internal | Netlify Edge Function handler (delegates to `createFetchHandler`) |
|
|
236
241
|
| `createHandler` | same as `createFetchHandler` | `1.0.0` | Internal | Alias of `createFetchHandler`, kept for naming consistency with other adapters |
|
|
237
|
-
| `DEFAULT_EDGE_TIMEOUT_MS` | `
|
|
242
|
+
| `DEFAULT_EDGE_TIMEOUT_MS` | `24_000` | `1.0.0` | Internal | Default request timeout in ms when `options.timeout` is omitted |
|
|
238
243
|
| `detectEdgeRuntime` | `() => EdgeRuntimeInfo` | `1.0.0` | Internal | Detects which edge platform is running -- see Runtime detection below |
|
|
239
244
|
| `EdgeContext` | class | `1.0.0` | Internal | `Context` implementation for edge runtimes |
|
|
240
245
|
| `createEdgeContext` | `<Env>(request, executionContext?, trustProxy?, env?) => EdgeContext<Env>` | `1.0.0` | Internal | Constructs an `EdgeContext` directly |
|
|
@@ -250,7 +255,63 @@ const handler = createCloudflareHandler(app, {
|
|
|
250
255
|
| Option | Type | Required | Default | Security-sensitive | Description |
|
|
251
256
|
| ------ | ---- | -------- | ------- | ------------------- | ------------ |
|
|
252
257
|
| `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 | `
|
|
258
|
+
| `timeout` | `number` | No | `24000` (`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) |
|
|
259
|
+
| `platform` | `PlatformId` | No | detected (see below) | -- | Overrides the detected value of `ctx.platform`. Set by `@nextrush/adapter-serverless`'s Tier-1 handlers, which know their provider unambiguously; application code on Cloudflare/Vercel/Netlify does not need it |
|
|
260
|
+
|
|
261
|
+
## Platform reporting (`ctx.platform`)
|
|
262
|
+
|
|
263
|
+
`ctx.runtime` answers "what kind of runtime am I on" (`'edge'` here, on every host). `ctx.platform`
|
|
264
|
+
answers the different question "which named deployment platform is this", typed as
|
|
265
|
+
`PlatformId | undefined` where `PlatformId` is
|
|
266
|
+
`'lambda' | 'gcf' | 'azure' | 'cloudflare-workers' | 'vercel-edge' | 'netlify-edge'`.
|
|
267
|
+
|
|
268
|
+
| Host | `ctx.runtime` | `ctx.platform` | How it is resolved |
|
|
269
|
+
| ----- | ------------- | -------------- | ------------------- |
|
|
270
|
+
| Cloudflare Workers | `'cloudflare-workers'` | `'cloudflare-workers'` | Auto-detected by `detectPlatform()` |
|
|
271
|
+
| Vercel Edge | `'vercel-edge'` | `'vercel-edge'` | Auto-detected |
|
|
272
|
+
| Netlify Edge | `'edge'` | `'netlify-edge'` | Auto-detected |
|
|
273
|
+
| Any other Fetch host | `'edge'` | `undefined` | Nothing matched, and nothing was passed |
|
|
274
|
+
| Lambda / GCF / Azure via `@nextrush/adapter-serverless` | `'edge'` | `'lambda'` / `'gcf'` / `'azure'` | Passed explicitly by that package's Tier-1 handler |
|
|
275
|
+
|
|
276
|
+
`detectPlatform()` reuses `detectEdgeRuntime()`'s exact three named-platform probes and has no
|
|
277
|
+
serverless branch: a FaaS platform is never guessed here, only ever declared through
|
|
278
|
+
`FetchHandlerOptions.platform`. An explicit value always wins over detection. `ctx.platform` was
|
|
279
|
+
added additively in RFC-026; `ctx.runtime`'s type and values are unchanged.
|
|
280
|
+
|
|
281
|
+
## Diagnostics
|
|
282
|
+
|
|
283
|
+
Three development-mode diagnostics exist to make otherwise-silent failures attributable. All three
|
|
284
|
+
are gated on `app.isProduction` being false, except the timeout log, which always fires.
|
|
285
|
+
|
|
286
|
+
**`ctx.waitUntil()` with no execution context** — the promise is dropped without running (unchanged
|
|
287
|
+
behavior). Outside production the first such call per context warns:
|
|
288
|
+
|
|
289
|
+
> `[nextrush/edge] ctx.waitUntil() was called, but this platform provided no execution context — the promise was dropped without running. This is expected on platforms with no background-task support (e.g. Vercel Edge); on Cloudflare Workers it usually means `ctx` was not passed through to createCloudflareHandler. (This message appears in development only.)`
|
|
290
|
+
|
|
291
|
+
**A second, different `Application` booting in the same isolate** — the mechanical signature of
|
|
292
|
+
building the app inside the exported handler instead of at module scope. Warns once per module
|
|
293
|
+
instance, outside production:
|
|
294
|
+
|
|
295
|
+
> `[nextrush/edge] A different Application booted in this process/isolate than the one that booted first. This usually means createApp() (or createFetchHandler/createCloudflareHandler/createVercelHandler/createNetlifyHandler) is being called inside the exported handler instead of at module scope — rebuilding the app on every invocation defeats warm-instance reuse and increases cold-start-like latency on every request. Build the app once, at module scope, above the export. (This message appears in development only.)`
|
|
296
|
+
|
|
297
|
+
**Timeout attribution** — when the timeout race wins, `app.logger.warn` records the effective
|
|
298
|
+
timeout, whether it came from the default or an explicit option, and the request:
|
|
299
|
+
|
|
300
|
+
> `[nextrush/edge] Request timed out after 24000ms (default) — returning 504. GET /slow`
|
|
301
|
+
|
|
302
|
+
The source reads `default` when `options.timeout` was omitted and `explicit options.timeout` when it
|
|
303
|
+
was supplied.
|
|
304
|
+
|
|
305
|
+
**`ctx.ip` reads empty with `trustProxy` off** — Edge has no raw socket to fall back to, so
|
|
306
|
+
`ctx.ip` is only ever populated from a proxy header (`CF-Connecting-IP`, `X-Forwarded-For`, …),
|
|
307
|
+
which is only trusted when `createApp({ proxy: true })` was set. Reading `ctx.ip` while `proxy` is
|
|
308
|
+
unset returns `''`, and warns once per context, outside production:
|
|
309
|
+
|
|
310
|
+
> `[nextrush/edge] ctx.ip is an empty string because trustProxy is false (the default) — Edge has no socket to fall back to, so no proxy headers means no IP. If this app runs behind a trusted proxy (e.g. Cloudflare, which sets cf-connecting-ip), pass { proxy: true } to createApp() to resolve it from headers. (This message appears in development only.)`
|
|
311
|
+
|
|
312
|
+
See `createApp`'s `proxy` option in [`@nextrush/core`'s README](https://github.com/0xTanzim/nextRush/blob/main/packages/core/README.md)
|
|
313
|
+
for what setting it does elsewhere in the framework; this warning exists only on this adapter,
|
|
314
|
+
since Node/Bun/Deno have a real socket to fall back to and never hit an unconditionally-empty `ip`.
|
|
254
315
|
|
|
255
316
|
## Runtime detection
|
|
256
317
|
|
|
@@ -291,7 +352,7 @@ Result is cached after the first call within a given isolate.
|
|
|
291
352
|
|
|
292
353
|
| Requirement | Version |
|
|
293
354
|
| ----------- | ------- |
|
|
294
|
-
| NextRush | `
|
|
355
|
+
| NextRush | `3.x` (`@nextrush/core`) |
|
|
295
356
|
| Node.js (local build/test only) | `>=22` |
|
|
296
357
|
| TypeScript | `>=5.x` |
|
|
297
358
|
|
|
@@ -330,7 +391,9 @@ Edge runtimes share constraints this package does not paper over:
|
|
|
330
391
|
No. `detectEdgeRuntime()` has no Lambda branch; it only distinguishes Cloudflare, Vercel, and
|
|
331
392
|
Netlify, defaulting to the generic `'edge'` otherwise. `@nextrush/adapter-serverless` is built
|
|
332
393
|
on this package and inherits the same detection, so Lambda deployments also report
|
|
333
|
-
`runtime: 'edge'` -- verified in `packages/runtime/src/detection.ts`.
|
|
394
|
+
`runtime: 'edge'` -- verified in `packages/runtime/src/detection.ts`. Use `ctx.platform`, which
|
|
395
|
+
serverless's Tier-1 handlers set explicitly (`'lambda'`, `'gcf'`, `'azure'`), to identify the
|
|
396
|
+
provider.
|
|
334
397
|
|
|
335
398
|
**Why is this package tier "Internal" instead of "Public"?**
|
|
336
399
|
Per [ADR-0005](https://github.com/0xTanzim/nextRush/blob/main/docs/adr/ADR-0005-package-tiers-sealed-surface-deprecation.md),
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Application } from '@nextrush/core';
|
|
2
|
+
import { ProxyTrust, PlatformId } from '@nextrush/types';
|
|
3
|
+
export { BodySource, Context, HttpMethod, Middleware, Runtime, RuntimeCapabilities } from '@nextrush/types';
|
|
2
4
|
import { WebContextBase } from '@nextrush/runtime';
|
|
3
5
|
export { BodyConsumedError, BodyTooLargeError, EdgeRuntimeInfo, EmptyBodySource, WebBodySource, createEmptyBodySource, createWebBodySource, detectEdgeRuntime, parseQueryString } from '@nextrush/runtime';
|
|
4
6
|
export { HttpError } from '@nextrush/errors';
|
|
5
|
-
export { BodySource, Context, HttpMethod, Middleware, Runtime, RuntimeCapabilities } from '@nextrush/types';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @nextrush/adapter-edge - Context Implementation
|
|
@@ -65,20 +66,46 @@ declare class EdgeContext<Env = unknown> extends WebContextBase {
|
|
|
65
66
|
* `undefined` on runtimes that do not supply bindings (e.g. Vercel Edge).
|
|
66
67
|
*/
|
|
67
68
|
readonly env?: Env;
|
|
68
|
-
|
|
69
|
+
/** Whether {@link waitUntil} has already warned once on this context (P2-4). */
|
|
70
|
+
private _waitUntilWarned;
|
|
71
|
+
/** Dev-mode gate for the {@link waitUntil} no-op warning (P2-4). */
|
|
72
|
+
private readonly _isProduction;
|
|
73
|
+
/** Whether the empty-`ctx.ip` warning has already fired once on this context (P2-3). */
|
|
74
|
+
private _ipWarned;
|
|
75
|
+
/** Whether the caller opted into trusting proxy headers for IP resolution. */
|
|
76
|
+
private readonly _proxy;
|
|
77
|
+
constructor(request: Request, executionContext?: EdgeExecutionContext, proxy?: ProxyTrust, env?: Env, isProduction?: boolean, platform?: PlatformId);
|
|
78
|
+
/**
|
|
79
|
+
* The resolved client IP.
|
|
80
|
+
*
|
|
81
|
+
* @remarks
|
|
82
|
+
* `''` when `trustProxy` is `false` (the default) — Edge has no socket to
|
|
83
|
+
* fall back to, unlike Node. Outside production, the first read of this
|
|
84
|
+
* getter in that state warns once per context (P2-3): a silent empty
|
|
85
|
+
* string is indistinguishable from "resolution ran and found nothing,"
|
|
86
|
+
* which silently degrades IP-keyed middleware (rate limiting, audit
|
|
87
|
+
* logging) with no signal that `trustProxy` is the fix.
|
|
88
|
+
*/
|
|
89
|
+
get ip(): string;
|
|
69
90
|
/**
|
|
70
91
|
* Extend request lifetime for async operations
|
|
71
92
|
*
|
|
72
93
|
* @remarks
|
|
73
94
|
* Use this for fire-and-forget operations that should complete
|
|
74
95
|
* after the response is sent (logging, analytics, etc.)
|
|
96
|
+
*
|
|
97
|
+
* Silently does nothing when the platform provides no execution context
|
|
98
|
+
* (e.g. Vercel Edge, or a serverless invocation with no `waitUntil` support)
|
|
99
|
+
* — the same as always. Outside production, the first such call additionally
|
|
100
|
+
* warns once per context, since a dropped promise here is otherwise
|
|
101
|
+
* undebuggable (P2-4).
|
|
75
102
|
*/
|
|
76
103
|
waitUntil(promise: Promise<unknown>): void;
|
|
77
104
|
}
|
|
78
105
|
/**
|
|
79
106
|
* Create a new EdgeContext
|
|
80
107
|
*/
|
|
81
|
-
declare function createEdgeContext<Env = unknown>(request: Request, executionContext?: EdgeExecutionContext,
|
|
108
|
+
declare function createEdgeContext<Env = unknown>(request: Request, executionContext?: EdgeExecutionContext, proxy?: ProxyTrust, env?: Env, isProduction?: boolean, platform?: PlatformId): EdgeContext<Env>;
|
|
82
109
|
|
|
83
110
|
/**
|
|
84
111
|
* @nextrush/adapter-edge - Edge Runtime Adapter
|
|
@@ -104,13 +131,19 @@ declare function createEdgeContext<Env = unknown>(request: Request, executionCon
|
|
|
104
131
|
* default to a bounded timeout rather than none).
|
|
105
132
|
*
|
|
106
133
|
* @remarks
|
|
107
|
-
*
|
|
108
|
-
* Functions: 25 s), comfortably above typical handler
|
|
109
|
-
* framework's clean `504` fires before the
|
|
134
|
+
* 24 000 ms — strictly below the tightest common edge-platform wall limit
|
|
135
|
+
* (Vercel Edge Functions: 25 s exactly), comfortably above typical handler
|
|
136
|
+
* durations, so the framework's clean `504` reliably fires before the
|
|
137
|
+
* platform terminates the isolate. A prior value of 25 000 ms sat exactly
|
|
138
|
+
* *at* that limit rather than below it, racing the platform's own kill with
|
|
139
|
+
* no margin — this constant is deliberately 1 000 ms under the tightest
|
|
140
|
+
* limit rather than per-platform-branched, since one shared constant is
|
|
141
|
+
* simpler to reason about than a platform switch inside a package whose
|
|
142
|
+
* whole point is one runner for every edge platform.
|
|
110
143
|
* Pass `timeout: 0` to disable the framework timeout entirely (platform limit
|
|
111
144
|
* alone applies) or a positive value to override the default.
|
|
112
145
|
*/
|
|
113
|
-
declare const DEFAULT_EDGE_TIMEOUT_MS =
|
|
146
|
+
declare const DEFAULT_EDGE_TIMEOUT_MS = 24000;
|
|
114
147
|
/**
|
|
115
148
|
* Options for the fetch handler
|
|
116
149
|
*/
|
|
@@ -125,19 +158,33 @@ interface FetchHandlerOptions {
|
|
|
125
158
|
* first, cancelling the still-running handler via `ctx.signal`.
|
|
126
159
|
*
|
|
127
160
|
* @remarks
|
|
128
|
-
* Defaults to {@link DEFAULT_EDGE_TIMEOUT_MS} (
|
|
129
|
-
* (F-07/ADR-0010) — below the tightest common edge-platform wall
|
|
130
|
-
* (Vercel Edge: 25 s), so the framework's clean `504` fires before
|
|
131
|
-
* platform terminates the isolate. Pass `0` to disable the framework
|
|
161
|
+
* Defaults to {@link DEFAULT_EDGE_TIMEOUT_MS} (24 000 ms) when omitted
|
|
162
|
+
* (F-07/ADR-0010) — strictly below the tightest common edge-platform wall
|
|
163
|
+
* limit (Vercel Edge: 25 s), so the framework's clean `504` fires before
|
|
164
|
+
* the platform terminates the isolate. Pass `0` to disable the framework
|
|
132
165
|
* timeout entirely (the platform's own limit still applies).
|
|
133
166
|
*
|
|
134
167
|
* Per-platform CPU/wall limits for context when overriding:
|
|
135
168
|
* - Cloudflare Workers: 30 000 (30 s CPU limit)
|
|
136
169
|
* - Vercel Edge: 25 000 (25 s wall limit)
|
|
137
170
|
*
|
|
138
|
-
* @default DEFAULT_EDGE_TIMEOUT_MS (
|
|
171
|
+
* @default DEFAULT_EDGE_TIMEOUT_MS (24000)
|
|
139
172
|
*/
|
|
140
173
|
timeout?: number;
|
|
174
|
+
/**
|
|
175
|
+
* Explicit named platform, overriding detection (RFC-026).
|
|
176
|
+
*
|
|
177
|
+
* @remarks
|
|
178
|
+
* Internal — set by `@nextrush/adapter-serverless`'s Tier-1 handlers
|
|
179
|
+
* (`createLambdaHandler`, `createGoogleHandler`, `createAzureHandler`),
|
|
180
|
+
* which already know their own platform identity unambiguously and pass it
|
|
181
|
+
* through rather than relying on detection. Application code calling
|
|
182
|
+
* `createFetchHandler`/`createCloudflareHandler`/etc. directly should not
|
|
183
|
+
* normally need to set this — the three named edge platforms
|
|
184
|
+
* (Cloudflare Workers, Vercel Edge, Netlify Edge) are still auto-detected
|
|
185
|
+
* when omitted.
|
|
186
|
+
*/
|
|
187
|
+
platform?: PlatformId;
|
|
141
188
|
}
|
|
142
189
|
/**
|
|
143
190
|
* Standard edge fetch handler type
|
|
@@ -258,6 +305,15 @@ declare function createVercelHandler(app: Application, options?: FetchHandlerOpt
|
|
|
258
305
|
* ```
|
|
259
306
|
*/
|
|
260
307
|
declare function createNetlifyHandler(app: Application, options?: FetchHandlerOptions): FetchHandler;
|
|
308
|
+
/**
|
|
309
|
+
* Alias of {@link createFetchHandler}.
|
|
310
|
+
*
|
|
311
|
+
* @deprecated Use {@link createFetchHandler} directly (P3-1) — two exported
|
|
312
|
+
* names for one function means autocomplete shows both with neither
|
|
313
|
+
* obviously canonical, on a package whose docs stress bundle discipline.
|
|
314
|
+
* `createFetchHandler` is the canonical name; this alias is kept for
|
|
315
|
+
* backwards compatibility and will be removed in a future major.
|
|
316
|
+
*/
|
|
261
317
|
declare const createHandler: typeof createFetchHandler;
|
|
262
318
|
|
|
263
319
|
/**
|
|
@@ -269,18 +325,15 @@ declare const createHandler: typeof createFetchHandler;
|
|
|
269
325
|
/**
|
|
270
326
|
* Get Content-Type header value
|
|
271
327
|
*
|
|
272
|
-
* @deprecated
|
|
273
|
-
*
|
|
274
|
-
* contract (deprecate-before-remove); will be removed in a future major.
|
|
328
|
+
* @deprecated Unused internally (superseded by `@nextrush/body-parser`'s own
|
|
329
|
+
* content-type parsing). Will be removed in `2.0.0`.
|
|
275
330
|
*/
|
|
276
331
|
declare function getContentType(headers: Headers): string | undefined;
|
|
277
332
|
/**
|
|
278
333
|
* Get Content-Length header as number
|
|
279
334
|
*
|
|
280
|
-
* @deprecated
|
|
281
|
-
*
|
|
282
|
-
* public-API contract (deprecate-before-remove); will be removed in a future
|
|
283
|
-
* major.
|
|
335
|
+
* @deprecated Unused internally (superseded by `@nextrush/body-parser`'s own
|
|
336
|
+
* content-length handling). Will be removed in `2.0.0`.
|
|
284
337
|
*/
|
|
285
338
|
declare function getContentLength(headers: Headers): number | undefined;
|
|
286
339
|
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
|
|
|
5
5
|
import { jsonErrorResponse } from "@nextrush/runtime";
|
|
6
6
|
|
|
7
7
|
// src/context.ts
|
|
8
|
-
import { detectEdgeRuntime, getEdgeClientIp, WebContextBase } from "@nextrush/runtime";
|
|
8
|
+
import { detectEdgeRuntime, detectPlatform, getEdgeClientIp, WebContextBase } from "@nextrush/runtime";
|
|
9
9
|
import { runNDJSONStream, runSSEStream, runTextStream } from "@nextrush/stream";
|
|
10
10
|
var EdgeContext = class extends WebContextBase {
|
|
11
11
|
static {
|
|
@@ -22,40 +22,86 @@ var EdgeContext = class extends WebContextBase {
|
|
|
22
22
|
* `undefined` on runtimes that do not supply bindings (e.g. Vercel Edge).
|
|
23
23
|
*/
|
|
24
24
|
env;
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
/** Whether {@link waitUntil} has already warned once on this context (P2-4). */
|
|
26
|
+
_waitUntilWarned = false;
|
|
27
|
+
/** Dev-mode gate for the {@link waitUntil} no-op warning (P2-4). */
|
|
28
|
+
_isProduction;
|
|
29
|
+
/** Whether the empty-`ctx.ip` warning has already fired once on this context (P2-3). */
|
|
30
|
+
_ipWarned = false;
|
|
31
|
+
/** Whether the caller opted into trusting proxy headers for IP resolution. */
|
|
32
|
+
_proxy;
|
|
33
|
+
constructor(request, executionContext, proxy = false, env, isProduction = true, platform) {
|
|
34
|
+
if (Array.isArray(proxy)) {
|
|
35
|
+
throw new Error("proxy: ['<cidr>', ...] is not supported on the Edge adapter \u2014 there is no socket peer address to validate the trusted-peer list against. Use `proxy: <hopCount>` instead (e.g. `proxy: 1` for one trusted reverse proxy such as Cloudflare).");
|
|
36
|
+
}
|
|
37
|
+
const ip = proxy !== false ? getEdgeClientIp(request, proxy) : "";
|
|
27
38
|
const runtime = detectEdgeRuntime().runtime;
|
|
39
|
+
const resolvedPlatform = platform ?? detectPlatform().platform;
|
|
28
40
|
super(request, ip, runtime, {
|
|
29
41
|
runTextStream,
|
|
30
42
|
runSSEStream,
|
|
31
43
|
runNDJSONStream
|
|
32
|
-
});
|
|
44
|
+
}, resolvedPlatform);
|
|
33
45
|
this.executionContext = executionContext;
|
|
34
46
|
this.env = env;
|
|
47
|
+
this._isProduction = isProduction;
|
|
48
|
+
this._proxy = proxy;
|
|
35
49
|
}
|
|
36
50
|
// ===========================================================================
|
|
37
51
|
// Edge-Specific Methods
|
|
38
52
|
// ===========================================================================
|
|
39
53
|
/**
|
|
54
|
+
* The resolved client IP.
|
|
55
|
+
*
|
|
56
|
+
* @remarks
|
|
57
|
+
* `''` when `trustProxy` is `false` (the default) — Edge has no socket to
|
|
58
|
+
* fall back to, unlike Node. Outside production, the first read of this
|
|
59
|
+
* getter in that state warns once per context (P2-3): a silent empty
|
|
60
|
+
* string is indistinguishable from "resolution ran and found nothing,"
|
|
61
|
+
* which silently degrades IP-keyed middleware (rate limiting, audit
|
|
62
|
+
* logging) with no signal that `trustProxy` is the fix.
|
|
63
|
+
*/
|
|
64
|
+
get ip() {
|
|
65
|
+
if (this._ip === "" && this._proxy === false && !this._isProduction && !this._ipWarned) {
|
|
66
|
+
this._ipWarned = true;
|
|
67
|
+
console.warn("[nextrush/edge] ctx.ip is an empty string because proxy is false (the default) \u2014 Edge has no socket to fall back to, so no proxy headers means no IP. If this app runs behind a trusted proxy (e.g. Cloudflare, which sets cf-connecting-ip), pass { proxy: 1 } to createApp() to resolve it from headers. (This message appears in development only.)");
|
|
68
|
+
}
|
|
69
|
+
return this._ip;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
40
72
|
* Extend request lifetime for async operations
|
|
41
73
|
*
|
|
42
74
|
* @remarks
|
|
43
75
|
* Use this for fire-and-forget operations that should complete
|
|
44
76
|
* after the response is sent (logging, analytics, etc.)
|
|
77
|
+
*
|
|
78
|
+
* Silently does nothing when the platform provides no execution context
|
|
79
|
+
* (e.g. Vercel Edge, or a serverless invocation with no `waitUntil` support)
|
|
80
|
+
* — the same as always. Outside production, the first such call additionally
|
|
81
|
+
* warns once per context, since a dropped promise here is otherwise
|
|
82
|
+
* undebuggable (P2-4).
|
|
45
83
|
*/
|
|
46
84
|
waitUntil(promise) {
|
|
47
85
|
if (this.executionContext?.waitUntil) {
|
|
48
86
|
this.executionContext.waitUntil(promise);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!this._isProduction && !this._waitUntilWarned) {
|
|
90
|
+
this._waitUntilWarned = true;
|
|
91
|
+
console.warn("[nextrush/edge] ctx.waitUntil() was called, but this platform provided no execution context \u2014 the promise was dropped without running. This is expected on platforms with no background-task support (e.g. Vercel Edge); on Cloudflare Workers it usually means `ctx` was not passed through to createCloudflareHandler. (This message appears in development only.)");
|
|
49
92
|
}
|
|
50
93
|
}
|
|
51
94
|
};
|
|
52
|
-
function createEdgeContext(request, executionContext,
|
|
53
|
-
return new EdgeContext(request, executionContext,
|
|
95
|
+
function createEdgeContext(request, executionContext, proxy = false, env, isProduction = true, platform) {
|
|
96
|
+
return new EdgeContext(request, executionContext, proxy, env, isProduction, platform);
|
|
54
97
|
}
|
|
55
98
|
__name(createEdgeContext, "createEdgeContext");
|
|
56
99
|
|
|
57
100
|
// src/adapter.ts
|
|
58
|
-
var DEFAULT_EDGE_TIMEOUT_MS =
|
|
101
|
+
var DEFAULT_EDGE_TIMEOUT_MS = 24e3;
|
|
102
|
+
var bootedApps = /* @__PURE__ */ new WeakSet();
|
|
103
|
+
var hasBootedAnyApp = false;
|
|
104
|
+
var warnedBootReuse = false;
|
|
59
105
|
function createFetchHandler(app, options = {}) {
|
|
60
106
|
const run = createRequestRunner(app, options);
|
|
61
107
|
return (request, executionContext) => run(request, executionContext);
|
|
@@ -63,11 +109,18 @@ function createFetchHandler(app, options = {}) {
|
|
|
63
109
|
__name(createFetchHandler, "createFetchHandler");
|
|
64
110
|
function createRequestRunner(app, options) {
|
|
65
111
|
const timeout = options.timeout ?? DEFAULT_EDGE_TIMEOUT_MS;
|
|
66
|
-
const
|
|
112
|
+
const timeoutSource = options.timeout === void 0 ? "default" : "explicit options.timeout";
|
|
113
|
+
const proxy = app.options.proxy ?? false;
|
|
67
114
|
const TIMEOUT_SENTINEL = /* @__PURE__ */ Symbol("timeout");
|
|
68
115
|
let bootPromise = null;
|
|
69
116
|
const ensureBooted = /* @__PURE__ */ __name(() => {
|
|
70
117
|
bootPromise ??= app.ready().then(() => {
|
|
118
|
+
if (!app.isProduction && !warnedBootReuse && hasBootedAnyApp && !bootedApps.has(app)) {
|
|
119
|
+
warnedBootReuse = true;
|
|
120
|
+
console.warn("[nextrush/edge] A different Application booted in this process/isolate than the one that booted first. This usually means createApp() (or createFetchHandler/createCloudflareHandler/createVercelHandler/createNetlifyHandler) is being called inside the exported handler instead of at module scope \u2014 rebuilding the app on every invocation defeats warm-instance reuse and increases cold-start-like latency on every request. Build the app once, at module scope, above the export. (This message appears in development only.)");
|
|
121
|
+
}
|
|
122
|
+
bootedApps.add(app);
|
|
123
|
+
hasBootedAnyApp = true;
|
|
71
124
|
const handler = app.callback();
|
|
72
125
|
app.start();
|
|
73
126
|
return handler;
|
|
@@ -76,7 +129,7 @@ function createRequestRunner(app, options) {
|
|
|
76
129
|
}, "ensureBooted");
|
|
77
130
|
return async (request, executionContext, env) => {
|
|
78
131
|
const handler = await ensureBooted();
|
|
79
|
-
const ctx = createEdgeContext(request, executionContext,
|
|
132
|
+
const ctx = createEdgeContext(request, executionContext, proxy, env, app.isProduction, options.platform);
|
|
80
133
|
try {
|
|
81
134
|
if (timeout > 0) {
|
|
82
135
|
let timerId;
|
|
@@ -91,6 +144,7 @@ function createRequestRunner(app, options) {
|
|
|
91
144
|
]);
|
|
92
145
|
if (result === TIMEOUT_SENTINEL) {
|
|
93
146
|
ctx.triggerTimeout();
|
|
147
|
+
app.logger.warn(`[nextrush/edge] Request timed out after ${String(timeout)}ms (${timeoutSource}) \u2014 returning 504. ${ctx.method} ${ctx.path}`);
|
|
94
148
|
return jsonErrorResponse(504, "Gateway Timeout");
|
|
95
149
|
}
|
|
96
150
|
} finally {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
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, PlatformId, ProxyTrust } 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 * 24 000 ms — strictly below the tightest common edge-platform wall limit\n * (Vercel Edge Functions: 25 s exactly), comfortably above typical handler\n * durations, so the framework's clean `504` reliably fires before the\n * platform terminates the isolate. A prior value of 25 000 ms sat exactly\n * *at* that limit rather than below it, racing the platform's own kill with\n * no margin — this constant is deliberately 1 000 ms under the tightest\n * limit rather than per-platform-branched, since one shared constant is\n * simpler to reason about than a platform switch inside a package whose\n * whole point is one runner for every edge platform.\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 = 24_000;\n\n/**\n * Module-level tracking for the boot-reuse warning (P1-3a).\n *\n * @remarks\n * `bootedApps` records every distinct `Application` that has completed at\n * least one request through this module instance; `warnedBootReuse` ensures\n * the warning fires at most once per module instance, not once per extra app.\n * Module-level (not per-closure) is deliberate: the mistake this catches\n * (calling `createApp()`/`createFetchHandler()` inside the exported handler)\n * produces a NEW `createRequestRunner` closure on every invocation, so\n * per-closure state could never observe a second app.\n */\nconst bootedApps = new WeakSet<Application>();\nlet hasBootedAnyApp = false;\nlet warnedBootReuse = false;\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} (24 000 ms) when omitted\n * (F-07/ADR-0010) — strictly below the tightest common edge-platform wall\n * limit (Vercel Edge: 25 s), so the framework's clean `504` fires before\n * the 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 (24000)\n */\n timeout?: number;\n\n /**\n * Explicit named platform, overriding detection (RFC-026).\n *\n * @remarks\n * Internal — set by `@nextrush/adapter-serverless`'s Tier-1 handlers\n * (`createLambdaHandler`, `createGoogleHandler`, `createAzureHandler`),\n * which already know their own platform identity unambiguously and pass it\n * through rather than relying on detection. Application code calling\n * `createFetchHandler`/`createCloudflareHandler`/etc. directly should not\n * normally need to set this — the three named edge platforms\n * (Cloudflare Workers, Vercel Edge, Netlify Edge) are still auto-detected\n * when omitted.\n */\n platform?: PlatformId;\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 timeoutSource = options.timeout === undefined ? 'default' : 'explicit options.timeout';\n const proxy = 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 // P1-3a: outside production, warn once per module instance if a\n // DIFFERENT Application already booted here — the mechanical signature\n // of building the app inside the exported handler instead of at module\n // scope (rebuilding the app on every invocation defeats warm reuse).\n if (!app.isProduction && !warnedBootReuse && hasBootedAnyApp && !bootedApps.has(app)) {\n warnedBootReuse = true;\n console.warn(\n '[nextrush/edge] A different Application booted in this process/isolate than the one ' +\n 'that booted first. This usually means createApp() (or createFetchHandler/createCloudflareHandler/' +\n 'createVercelHandler/createNetlifyHandler) is being called inside the exported handler ' +\n 'instead of at module scope — rebuilding the app on every invocation defeats warm-instance ' +\n 'reuse and increases cold-start-like latency on every request. Build the app once, at module ' +\n 'scope, above the export. (This message appears in development only.)'\n );\n }\n bootedApps.add(app);\n hasBootedAnyApp = true;\n\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, proxy, env, app.isProduction, options.platform);\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 // P1-3b: name the effective timeout and its source so a 504 is\n // attributable without reading this adapter's source.\n app.logger.warn(\n `[nextrush/edge] Request timed out after ${String(timeout)}ms (${timeoutSource}) — returning 504. ` +\n `${ctx.method} ${ctx.path}`\n );\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/**\n * Alias of {@link createFetchHandler}.\n *\n * @deprecated Use {@link createFetchHandler} directly (P3-1) — two exported\n * names for one function means autocomplete shows both with neither\n * obviously canonical, on a package whose docs stress bundle discipline.\n * `createFetchHandler` is the canonical name; this alias is kept for\n * backwards compatibility and will be removed in a future major.\n */\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?, ProxyTrust?, unknown?, boolean?, PlatformId?],\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, detectPlatform, getEdgeClientIp, WebContextBase } from '@nextrush/runtime';\nimport type { PlatformId, ProxyTrust } from '@nextrush/types';\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 /** Whether {@link waitUntil} has already warned once on this context (P2-4). */\n private _waitUntilWarned = false;\n /** Dev-mode gate for the {@link waitUntil} no-op warning (P2-4). */\n private readonly _isProduction: boolean;\n /** Whether the empty-`ctx.ip` warning has already fired once on this context (P2-3). */\n private _ipWarned = false;\n /** Whether the caller opted into trusting proxy headers for IP resolution. */\n private readonly _proxy: ProxyTrust;\n\n constructor(\n request: Request,\n executionContext?: EdgeExecutionContext,\n proxy: ProxyTrust = false,\n env?: Env,\n isProduction = true,\n platform?: PlatformId\n ) {\n // Get client IP from CF headers or standard headers.\n //\n // HP-1 trim: Edge has no socket, so when `proxy` 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 `''`). Otherwise\n // resolution still goes through `getEdgeClientIp`, preserving the Cloudflare\n // `cf-connecting-ip` → `x-forwarded-for` → `x-real-ip` precedence.\n //\n // A `string[]` (CIDR peer list) trust is rejected here rather than at\n // `createApp()` construction: Edge has no socket peer address to check\n // that list against (RFC-030 §8.6), so the constraint is specific to this\n // adapter, not the app-level `proxy` option in general.\n if (Array.isArray(proxy)) {\n throw new Error(\n \"proxy: ['<cidr>', ...] is not supported on the Edge adapter — there is no socket peer \" +\n 'address to validate the trusted-peer list against. Use `proxy: <hopCount>` instead ' +\n '(e.g. `proxy: 1` for one trusted reverse proxy such as Cloudflare).'\n );\n }\n const ip = proxy !== false ? getEdgeClientIp(request, proxy) : '';\n\n // Detect specific edge runtime\n const runtime = detectEdgeRuntime().runtime;\n\n // RFC-026: an explicitly-supplied platform (from a serverless Tier-1\n // handler, which already knows its own identity) always wins over\n // detection; otherwise fall back to the same three-branch edge-platform\n // probe `detectEdgeRuntime()` already performs.\n const resolvedPlatform = platform ?? detectPlatform().platform;\n\n super(request, ip, runtime, { runTextStream, runSSEStream, runNDJSONStream }, resolvedPlatform);\n\n this.executionContext = executionContext;\n this.env = env;\n this._isProduction = isProduction;\n this._proxy = proxy;\n }\n\n // ===========================================================================\n // Edge-Specific Methods\n // ===========================================================================\n\n /**\n * The resolved client IP.\n *\n * @remarks\n * `''` when `trustProxy` is `false` (the default) — Edge has no socket to\n * fall back to, unlike Node. Outside production, the first read of this\n * getter in that state warns once per context (P2-3): a silent empty\n * string is indistinguishable from \"resolution ran and found nothing,\"\n * which silently degrades IP-keyed middleware (rate limiting, audit\n * logging) with no signal that `trustProxy` is the fix.\n */\n override get ip(): string {\n if (this._ip === '' && this._proxy === false && !this._isProduction && !this._ipWarned) {\n this._ipWarned = true;\n console.warn(\n '[nextrush/edge] ctx.ip is an empty string because proxy is false (the default) — ' +\n 'Edge has no socket to fall back to, so no proxy headers means no IP. If this app runs ' +\n 'behind a trusted proxy (e.g. Cloudflare, which sets cf-connecting-ip), pass ' +\n \"{ proxy: 1 } to createApp() to resolve it from headers. (This message appears in \" +\n 'development only.)'\n );\n }\n return this._ip;\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 * Silently does nothing when the platform provides no execution context\n * (e.g. Vercel Edge, or a serverless invocation with no `waitUntil` support)\n * — the same as always. Outside production, the first such call additionally\n * warns once per context, since a dropped promise here is otherwise\n * undebuggable (P2-4).\n */\n waitUntil(promise: Promise<unknown>): void {\n if (this.executionContext?.waitUntil) {\n this.executionContext.waitUntil(promise);\n return;\n }\n if (!this._isProduction && !this._waitUntilWarned) {\n this._waitUntilWarned = true;\n console.warn(\n '[nextrush/edge] ctx.waitUntil() was called, but this platform provided no execution ' +\n 'context — the promise was dropped without running. This is expected on platforms with ' +\n 'no background-task support (e.g. Vercel Edge); on Cloudflare Workers it usually means ' +\n '`ctx` was not passed through to createCloudflareHandler. (This message appears in ' +\n 'development only.)'\n );\n }\n }\n}\n\n/**\n * Create a new EdgeContext\n */\nexport function createEdgeContext<Env = unknown>(\n request: Request,\n executionContext?: EdgeExecutionContext,\n proxy: ProxyTrust = false,\n env?: Env,\n isProduction = true,\n platform?: PlatformId\n): EdgeContext<Env> {\n return new EdgeContext<Env>(request, executionContext, proxy, env, isProduction, platform);\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 // eslint-disable-next-line @typescript-eslint/no-deprecated -- barrel re-export of a deprecated alias is not a usage site; kept for backward compatibility until the next major (P3-1).\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 Unused internally (superseded by `@nextrush/body-parser`'s own\n * content-type parsing). Will be removed in `2.0.0`.\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 Unused internally (superseded by `@nextrush/body-parser`'s own\n * content-length handling). Will be removed in `2.0.0`.\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","detectPlatform","getEdgeClientIp","WebContextBase","runNDJSONStream","runSSEStream","runTextStream","EdgeContext","WebContextBase","executionContext","env","_waitUntilWarned","_isProduction","_ipWarned","_proxy","request","proxy","isProduction","platform","Array","isArray","Error","ip","getEdgeClientIp","runtime","detectEdgeRuntime","resolvedPlatform","detectPlatform","runTextStream","runSSEStream","runNDJSONStream","_ip","console","warn","waitUntil","promise","createEdgeContext","DEFAULT_EDGE_TIMEOUT_MS","bootedApps","WeakSet","hasBootedAnyApp","warnedBootReuse","createFetchHandler","app","options","run","createRequestRunner","request","executionContext","timeout","timeoutSource","undefined","proxy","TIMEOUT_SENTINEL","Symbol","bootPromise","ensureBooted","ready","then","isProduction","has","console","warn","add","handler","callback","start","env","ctx","createEdgeContext","platform","timerId","result","Promise","race","resolve","setTimeout","triggerTimeout","logger","String","method","path","jsonErrorResponse","clearTimeout","responded","status","json","error","getResponse","onError","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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextrush/adapter-edge",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
4
|
"description": "Edge runtime adapter for NextRush (Cloudflare Workers, Vercel Edge, etc.)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
"README.md"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@nextrush/
|
|
21
|
-
"@nextrush/
|
|
22
|
-
"@nextrush/
|
|
23
|
-
"@nextrush/
|
|
24
|
-
"@nextrush/
|
|
20
|
+
"@nextrush/runtime": "^4.0.0-beta.1",
|
|
21
|
+
"@nextrush/stream": "^1.0.0-beta.1",
|
|
22
|
+
"@nextrush/core": "^4.0.0-beta.1",
|
|
23
|
+
"@nextrush/types": "^4.0.0-beta.1",
|
|
24
|
+
"@nextrush/errors": "^4.0.0-beta.1"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"tsup": "^8.5.1",
|