@rapidrest/service-core 2.1.1 → 2.2.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.
@@ -1,4 +1,5 @@
1
1
  import { JWTUser } from "@rapidrest/core";
2
+ import type { Readable } from "stream";
2
3
  /**
3
4
  * Framework-agnostic HTTP request interface. Mirrors the Express `Request` surface used throughout
4
5
  * this codebase so that route handlers, middleware, and utilities require no changes when the
@@ -13,6 +14,25 @@ export interface HttpRequest {
13
14
  query: Record<string, string | string[]>;
14
15
  body: any;
15
16
  rawBody?: Buffer;
17
+ /**
18
+ * The raw request body as a Node `Readable` stream, populated instead of `body`/`rawBody` for a
19
+ * route registered with `{ streamingBody: true }` (see `HttpRouteOptions`) — e.g. via the
20
+ * `@StreamingBody()` decorator. `undefined` for every ordinary route, where `body`/`rawBody` are
21
+ * populated as before.
22
+ *
23
+ * The stream yields `Buffer` chunks and supports `for await (const chunk of req.bodyStream)` as
24
+ * well as `.pipe()`. Backpressure is enforced end-to-end by both adapters — a slow consumer
25
+ * throttles how fast bytes are read off the underlying connection rather than buffering
26
+ * unbounded data in memory — which is the entire point of opting into streaming (e.g. a
27
+ * multi-GB file upload). The stream is destroyed (with an error) if the client disconnects
28
+ * mid-upload; a handler consuming it via `for await` sees that as a thrown error and should
29
+ * clean up (e.g. delete a partially-written temp file) in a `catch`/`finally`.
30
+ *
31
+ * A streaming route does NOT get the framework's default `maxBodySize` enforcement — that check
32
+ * only runs as part of the ordinary buffering path. The handler is responsible for enforcing
33
+ * whatever size limit makes sense for the route itself.
34
+ */
35
+ bodyStream?: Readable;
16
36
  cookies: Record<string, string>;
17
37
  signedCookies: Record<string, string>;
18
38
  /**
@@ -83,6 +103,25 @@ export type NextFunction = (err?: any) => void;
83
103
  export type RequestHandler = (req: HttpRequest, res: HttpResponse, next: NextFunction) => void | Promise<void>;
84
104
  /** Standard 4-param error-handling middleware function. */
85
105
  export type ErrorHandler = (err: any, req: HttpRequest, res: HttpResponse, next: NextFunction) => void | Promise<void>;
106
+ /**
107
+ * Per-route registration options, optionally passed as the first element of a route's handler list
108
+ * (e.g. `app.post(path, { streamingBody: true }, ...handlers)`). Detected at runtime by both router
109
+ * implementations: a non-function first argument is treated as `HttpRouteOptions` rather than a
110
+ * `RequestHandler`, so omitting it entirely (the overwhelmingly common case) is unaffected — every
111
+ * existing call site that only ever passes handler functions keeps its exact current behavior.
112
+ */
113
+ export interface HttpRouteOptions {
114
+ /**
115
+ * When `true`, the router does not buffer this route's request body into `req.body`/`req.rawBody`
116
+ * before running its middleware/handler chain. Instead the raw body is exposed as a stream on
117
+ * `req.bodyStream` (see its doc comment on `HttpRequest`), and the route's own handler is
118
+ * responsible for consuming it and enforcing any size limit it needs — the framework's
119
+ * `maxBodySize` 413 rejection does not apply to a streaming route. Set by the `@StreamingBody()`
120
+ * route decorator; see `RouteUtils.registerRoute()`.
121
+ */
122
+ streamingBody?: boolean;
123
+ [key: string]: any;
124
+ }
86
125
  /**
87
126
  * Public surface shared by every HTTP router implementation (uWS-backed, Bun-backed, ...).
88
127
  * `Server.ts` depends only on this interface, never on a concrete router class, so the
@@ -90,13 +129,13 @@ export type ErrorHandler = (err: any, req: HttpRequest, res: HttpResponse, next:
90
129
  */
91
130
  export interface IHttpRouter {
92
131
  use(...handlers: RequestHandler[]): this;
93
- get(path: string, ...handlers: RequestHandler[]): this;
94
- post(path: string, ...handlers: RequestHandler[]): this;
95
- put(path: string, ...handlers: RequestHandler[]): this;
96
- delete(path: string, ...handlers: RequestHandler[]): this;
97
- patch(path: string, ...handlers: RequestHandler[]): this;
98
- head(path: string, ...handlers: RequestHandler[]): this;
99
- options(path: string, ...handlers: RequestHandler[]): this;
132
+ get(path: string, ...handlers: Array<RequestHandler | HttpRouteOptions>): this;
133
+ post(path: string, ...handlers: Array<RequestHandler | HttpRouteOptions>): this;
134
+ put(path: string, ...handlers: Array<RequestHandler | HttpRouteOptions>): this;
135
+ delete(path: string, ...handlers: Array<RequestHandler | HttpRouteOptions>): this;
136
+ patch(path: string, ...handlers: Array<RequestHandler | HttpRouteOptions>): this;
137
+ head(path: string, ...handlers: Array<RequestHandler | HttpRouteOptions>): this;
138
+ options(path: string, ...handlers: Array<RequestHandler | HttpRouteOptions>): this;
100
139
  /** `true` if the application has registered its own literal `OPTIONS` route at `path` (as
101
140
  * opposed to the framework's own internal `/*` CORS-preflight fallback registered in `listen()`).
102
141
  * Consulted by `Server.ts`'s global CORS middleware so a real app-defined `OPTIONS` handler gets a
@@ -1,5 +1,6 @@
1
1
  import type { HttpRequest, HttpResponse } from "../types.js";
2
2
  import type { HttpRequest as UWSHttpRequest, HttpResponse as UWSHttpResponse } from "uWebSockets.js";
3
+ import { Readable } from "stream";
3
4
  /** Parses a `cookie` header string into a key/value map. */
4
5
  export declare function parseCookies(cookieHeader: string): Record<string, string>;
5
6
  /**
@@ -136,3 +137,50 @@ export declare const DEFAULT_MAX_BODY_SIZE: number;
136
137
  export declare function readBody(uwsRes: UWSHttpResponse, req: UWSRequest, maxBodySize?: number, res?: {
137
138
  onAbort: (callback: () => void) => void;
138
139
  }): Promise<boolean>;
140
+ /**
141
+ * Internal buffer size (in bytes) at which `makeBodyStream()`'s stream applies backpressure — i.e.
142
+ * pauses pulling more bytes off the uWS connection until its consumer catches up. Chosen much larger
143
+ * than a Node `Readable`'s 16 KiB default: a streaming route exists specifically to move large
144
+ * payloads (e.g. a multi-GB mailbox import) efficiently, and throttling every 16 KiB would make
145
+ * pause()/resume() churn dominate. 1 MiB bounds worst-case extra memory per in-flight streaming
146
+ * request to a small, predictable amount while still giving a slow consumer (e.g. writing to disk)
147
+ * plenty of headroom before the network is throttled.
148
+ */
149
+ export declare const STREAMING_BODY_HIGH_WATER_MARK: number;
150
+ /**
151
+ * Exposes a uWS request body as a Node `Readable` stream instead of buffering it into memory, for a
152
+ * route registered with `{ streamingBody: true }` (see `HttpRouteOptions`). Does NOT populate
153
+ * `req.body`/`req.rawBody` and does NOT enforce any `maxBodySize` limit — this is the entire point:
154
+ * unlike `readBody()`, the caller has opted out of buffering specifically so an arbitrarily large
155
+ * body (e.g. a 20 GB PST/mbox import) never needs to fit in memory at once. Enforcing a size limit,
156
+ * if wanted, is the route handler's own responsibility while consuming the stream.
157
+ *
158
+ * Backpressure is real, not simulated: a chunk that overflows the stream's internal buffer
159
+ * (`push()` returning `false`) calls `uwsRes.pause()` (uWS suspends delivering more `onData`
160
+ * chunks), and the returned `Readable`'s `_read()` calls `uwsRes.resume()` — but ONLY when this
161
+ * function itself previously paused it. uWS's `pause()`/`resume()` are not simple idempotent
162
+ * throttle toggles: empirically, calling `resume()` when the connection was never paused (e.g. from
163
+ * `_read()`'s very first call, before any backpressure has ever been applied) stops any further
164
+ * `onData` delivery for the rest of the request, hanging it forever. A local `paused` flag makes
165
+ * `resume()` a no-op unless a matching `pause()` was actually issued, matching the exact
166
+ * pause/resume pairing uWS expects. A slow consumer (a route piping to a slow disk, or one simply
167
+ * not reading yet) still throttles how fast bytes are pulled off the client connection rather than
168
+ * piling up unbounded data in process memory — the failure mode this whole feature exists to avoid.
169
+ *
170
+ * The stream is destroyed with an error if the client disconnects mid-upload (wired through the
171
+ * existing single `onAborted` fan-out via `res.onAbort()`, matching `readBody()`'s own abort
172
+ * handling) so a handler awaiting `for await (const chunk of req.bodyStream)` sees a thrown error
173
+ * instead of hanging forever, and can clean up (e.g. delete a partial temp file) in its own
174
+ * `catch`/`finally`.
175
+ *
176
+ * Must be called synchronously, before any `await`, in the same tick as request handling begins —
177
+ * uWS requires `onData`/`onAborted` to be registered before any asynchronous operation, matching the
178
+ * exact constraint `readBody()` above is already subject to.
179
+ *
180
+ * @param uwsRes The uWS HttpResponse to read the body from.
181
+ * @param res Used only to register the abort callback through the framework's existing single
182
+ * `onAborted` slot (see `UWSResponse.onAbort()`) — never written to otherwise.
183
+ */
184
+ export declare function makeBodyStream(uwsRes: UWSHttpResponse, res: {
185
+ onAbort: (callback: () => void) => void;
186
+ }): Readable;
@@ -1,5 +1,5 @@
1
1
  import uWS from "uWebSockets.js";
2
- import { type IHttpRouter, type RequestHandler, type WebSocketOptions } from "../types.js";
2
+ import { type HttpRouteOptions, type IHttpRouter, type RequestHandler, type WebSocketOptions } from "../types.js";
3
3
  import { type WsUpgradeAuth } from "../MiddlewareChain.js";
4
4
  export { runChain, extractParamNames } from "../MiddlewareChain.js";
5
5
  export type { WsUpgradeAuth, WsUpgradeAuthResult } from "../MiddlewareChain.js";
@@ -55,13 +55,13 @@ export declare class HttpRouter implements IHttpRouter {
55
55
  private capturePreRouteCount;
56
56
  /** Builds the uWS handler for a route registered at `routePattern` (`undefined` for the router's own fallbacks). */
57
57
  private makeHandler;
58
- get(routePath: string, ...handlers: RequestHandler[]): this;
59
- post(routePath: string, ...handlers: RequestHandler[]): this;
60
- put(routePath: string, ...handlers: RequestHandler[]): this;
61
- delete(routePath: string, ...handlers: RequestHandler[]): this;
62
- patch(routePath: string, ...handlers: RequestHandler[]): this;
63
- head(routePath: string, ...handlers: RequestHandler[]): this;
64
- options(routePath: string, ...handlers: RequestHandler[]): this;
58
+ get(routePath: string, ...handlersOrOptions: Array<RequestHandler | HttpRouteOptions>): this;
59
+ post(routePath: string, ...handlersOrOptions: Array<RequestHandler | HttpRouteOptions>): this;
60
+ put(routePath: string, ...handlersOrOptions: Array<RequestHandler | HttpRouteOptions>): this;
61
+ delete(routePath: string, ...handlersOrOptions: Array<RequestHandler | HttpRouteOptions>): this;
62
+ patch(routePath: string, ...handlersOrOptions: Array<RequestHandler | HttpRouteOptions>): this;
63
+ head(routePath: string, ...handlersOrOptions: Array<RequestHandler | HttpRouteOptions>): this;
64
+ options(routePath: string, ...handlersOrOptions: Array<RequestHandler | HttpRouteOptions>): this;
65
65
  /** Returns `true` if the application has registered its own literal `OPTIONS` route at `path`
66
66
  * (not the framework's own `/*` CORS-preflight fallback). See `explicitOptionsPaths`'s own doc
67
67
  * comment. `path` is normalized the same way registered routes are, so a trailing-slash mismatch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rapidrest/service-core",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "Provides all core functionality for RapidREST based backend services.",
5
5
  "repository": "https://github.com/rapidrest/service-core.git",
6
6
  "author": "RapidREST <rapidrests@gmail.com>",