@dunx/http 2.4.0 → 2.5.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/dist/{chunk-sz4pvqxy.js → chunk-jh7jk0bn.js} +56 -3
- package/dist/client/options.d.ts +12 -0
- package/dist/client.js +14 -5
- package/dist/compression/compression.d.ts +30 -0
- package/dist/compression/module.d.ts +20 -0
- package/dist/compression/negotiate.d.ts +12 -0
- package/dist/compression/options.d.ts +73 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +309 -20
- package/dist/route/decorators.d.ts +6 -6
- package/dist/route/marker.d.ts +13 -0
- package/dist/route/schema.d.ts +64 -6
- package/dist/server/request-logging.d.ts +14 -0
- package/dist/server/routes.d.ts +1 -1
- package/dist/server/trace-context.d.ts +51 -0
- package/package.json +3 -3
- package/dist/chunk-sz4pvqxy.js.map +0 -10
- package/dist/client.js.map +0 -15
- package/dist/index.js.map +0 -53
|
@@ -105,7 +105,60 @@ var HttpStatusCode = Object.freeze({
|
|
|
105
105
|
GATEWAY_TIMEOUT: 504
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
// src/server/trace-context.ts
|
|
109
|
+
var TRACEPARENT_HEADER = "traceparent";
|
|
110
|
+
var TRACESTATE_HEADER = "tracestate";
|
|
111
|
+
var HEX_32 = /^[0-9a-f]{32}$/;
|
|
112
|
+
var HEX_16 = /^[0-9a-f]{16}$/;
|
|
113
|
+
var HEX_2 = /^[0-9a-f]{2}$/;
|
|
114
|
+
var ZERO_TRACE = "0".repeat(32);
|
|
115
|
+
var ZERO_SPAN = "0".repeat(16);
|
|
116
|
+
var SAMPLED = 1;
|
|
117
|
+
var TRACE = Symbol.for("dunx.http.trace");
|
|
118
|
+
var mintSpanId = () => Buffer.from(crypto.getRandomValues(new Uint8Array(8))).toString("hex");
|
|
109
119
|
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
class TraceContext {
|
|
121
|
+
static adopt(req, requestId) {
|
|
122
|
+
const inbound = TraceContext.#parse(req.headers.get(TRACEPARENT_HEADER));
|
|
123
|
+
const state = req.headers.get(TRACESTATE_HEADER);
|
|
124
|
+
const trace = {
|
|
125
|
+
traceId: inbound?.traceId ?? requestId.replaceAll("-", ""),
|
|
126
|
+
spanId: mintSpanId(),
|
|
127
|
+
...inbound === undefined ? {} : { parentSpanId: inbound.spanId },
|
|
128
|
+
flags: inbound?.flags ?? "01",
|
|
129
|
+
...inbound !== undefined && state !== null ? { state } : {}
|
|
130
|
+
};
|
|
131
|
+
req[TRACE] = trace;
|
|
132
|
+
return trace;
|
|
133
|
+
}
|
|
134
|
+
static of(req) {
|
|
135
|
+
return req[TRACE];
|
|
136
|
+
}
|
|
137
|
+
static header(trace) {
|
|
138
|
+
return `00-${trace.traceId}-${trace.spanId}-${trace.flags}`;
|
|
139
|
+
}
|
|
140
|
+
static sampled(trace) {
|
|
141
|
+
return (Number.parseInt(trace.flags, 16) & SAMPLED) === SAMPLED;
|
|
142
|
+
}
|
|
143
|
+
static #parse(header) {
|
|
144
|
+
if (header === null)
|
|
145
|
+
return;
|
|
146
|
+
const parts = header.split("-");
|
|
147
|
+
if (parts.length < 4)
|
|
148
|
+
return;
|
|
149
|
+
const [version, traceId, spanId, flags] = parts;
|
|
150
|
+
if (!HEX_2.test(version) || version === "ff")
|
|
151
|
+
return;
|
|
152
|
+
if (version === "00" && parts.length !== 4)
|
|
153
|
+
return;
|
|
154
|
+
if (!HEX_32.test(traceId) || traceId === ZERO_TRACE)
|
|
155
|
+
return;
|
|
156
|
+
if (!HEX_16.test(spanId) || spanId === ZERO_SPAN)
|
|
157
|
+
return;
|
|
158
|
+
if (!HEX_2.test(flags))
|
|
159
|
+
return;
|
|
160
|
+
return { traceId, spanId, flags };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export { __privateGet, __privateAdd, __decoratorStart, __decoratorMetadata, __runInitializers, __decorateElement, HttpStatusCode, TRACEPARENT_HEADER, TRACESTATE_HEADER, TraceContext };
|
package/dist/client/options.d.ts
CHANGED
|
@@ -24,6 +24,17 @@ export interface HttpClientOptionsInit {
|
|
|
24
24
|
* @default true
|
|
25
25
|
*/
|
|
26
26
|
readonly propagateRequestId?: boolean | string;
|
|
27
|
+
/**
|
|
28
|
+
* Forward W3C Trace Context upstream as `traceparent`, so the callee's spans
|
|
29
|
+
* join this request's trace.
|
|
30
|
+
*
|
|
31
|
+
* Read from `RequestContext`, so it only carries when a trace is in scope -
|
|
32
|
+
* which means `requestLogging: { trace: true }` on the inbound side. With that
|
|
33
|
+
* off there is nothing to send and this costs one property read.
|
|
34
|
+
*
|
|
35
|
+
* @default true
|
|
36
|
+
*/
|
|
37
|
+
readonly propagateTrace?: boolean;
|
|
27
38
|
/** Bound as its own token, so a second client can be injected by name. */
|
|
28
39
|
readonly name?: string;
|
|
29
40
|
/**
|
|
@@ -51,6 +62,7 @@ export declare class HttpClientOptions {
|
|
|
51
62
|
readonly headers: Readonly<Record<string, string>>;
|
|
52
63
|
readonly retry: RetryOptions<unknown>;
|
|
53
64
|
readonly requestIdHeader: string | undefined;
|
|
65
|
+
readonly propagateTrace: boolean;
|
|
54
66
|
readonly name: string | undefined;
|
|
55
67
|
readonly fetchOptions: Readonly<Record<string, unknown>>;
|
|
56
68
|
constructor(init?: HttpClientOptionsInit);
|
package/dist/client.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
|
-
HttpStatusCode
|
|
4
|
-
|
|
3
|
+
HttpStatusCode,
|
|
4
|
+
TRACEPARENT_HEADER,
|
|
5
|
+
TraceContext
|
|
6
|
+
} from "./chunk-jh7jk0bn.js";
|
|
5
7
|
|
|
6
8
|
// src/client/errors.ts
|
|
7
9
|
import { AppError } from "@dunx/core";
|
|
@@ -75,6 +77,7 @@ class HttpClientOptions {
|
|
|
75
77
|
headers;
|
|
76
78
|
retry;
|
|
77
79
|
requestIdHeader;
|
|
80
|
+
propagateTrace;
|
|
78
81
|
name;
|
|
79
82
|
fetchOptions;
|
|
80
83
|
constructor(init = {}) {
|
|
@@ -83,6 +86,7 @@ class HttpClientOptions {
|
|
|
83
86
|
this.headers = init.headers ?? {};
|
|
84
87
|
this.retry = init.retry ?? {};
|
|
85
88
|
this.name = init.name;
|
|
89
|
+
this.propagateTrace = init.propagateTrace ?? true;
|
|
86
90
|
const propagate = init.propagateRequestId ?? true;
|
|
87
91
|
this.requestIdHeader = propagate === false ? undefined : propagate === true ? DEFAULT_REQUEST_ID_HEADER : propagate;
|
|
88
92
|
this.fetchOptions = Object.fromEntries([
|
|
@@ -328,11 +332,19 @@ class HttpService extends UrlHelper {
|
|
|
328
332
|
}
|
|
329
333
|
async send(config, url, body, serialised, accept = "application/json") {
|
|
330
334
|
const requestId = this.options.requestIdHeader === undefined ? undefined : this.requestContext.getContext().requestId;
|
|
335
|
+
const trace = this.options.propagateTrace ? this.requestContext.getContext() : undefined;
|
|
331
336
|
const headers = {
|
|
332
337
|
accept,
|
|
333
338
|
...serialised === "" ? {} : { "content-type": "application/json" },
|
|
334
339
|
...this.options.headers,
|
|
335
340
|
...requestId === undefined || this.options.requestIdHeader === undefined ? {} : { [this.options.requestIdHeader]: requestId },
|
|
341
|
+
...typeof trace?.traceId === "string" && typeof trace.spanId === "string" ? {
|
|
342
|
+
[TRACEPARENT_HEADER]: TraceContext.header({
|
|
343
|
+
traceId: trace.traceId,
|
|
344
|
+
spanId: trace.spanId,
|
|
345
|
+
flags: "01"
|
|
346
|
+
})
|
|
347
|
+
} : {},
|
|
336
348
|
...config.headerFactory?.({
|
|
337
349
|
timestamp: Math.floor(Date.now() / 1000),
|
|
338
350
|
method: config.method,
|
|
@@ -460,6 +472,3 @@ export {
|
|
|
460
472
|
retryAfterMs,
|
|
461
473
|
safeStringify
|
|
462
474
|
};
|
|
463
|
-
|
|
464
|
-
//# debugId=9448CCC7AF6FC82264756E2164756E21
|
|
465
|
-
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { BunRequest } from 'bun';
|
|
2
|
+
import type { RouteContext } from '../server/context.js';
|
|
3
|
+
import type { Middleware, Next } from '../server/middleware.js';
|
|
4
|
+
import { CompressionOptions } from './options.js';
|
|
5
|
+
/**
|
|
6
|
+
* Response compression, on Bun's own compressors.
|
|
7
|
+
*
|
|
8
|
+
* **Not installed by default, and registered by the app rather than by a module:**
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* const app = await HttpFactory.create(AppModule, { imports: [CompressionModule.forRoot()] });
|
|
12
|
+
* app.use(Compression);
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* An app that never registers it pays nothing - there is no branch in the request
|
|
16
|
+
* path to skip. Position is the app's decision for the same reason `StaticFiles`
|
|
17
|
+
* leaves it open: compression belongs inside request logging, so the logged status
|
|
18
|
+
* is the real one, and outside anything that wants to read the body it produced.
|
|
19
|
+
*
|
|
20
|
+
* Two encoders rather than one. A body whose `content-length` is known and under
|
|
21
|
+
* `BUFFER_LIMIT` goes through `Bun.zstdCompressSync`/`gzipSync`, which is faster
|
|
22
|
+
* than the stream and leaves an accurate `content-length` on the response; a
|
|
23
|
+
* streamed or oversized body goes through `CompressionStream` and loses the header,
|
|
24
|
+
* as it must.
|
|
25
|
+
*/
|
|
26
|
+
export declare class Compression implements Middleware {
|
|
27
|
+
#private;
|
|
28
|
+
constructor(options: CompressionOptions);
|
|
29
|
+
handle(req: BunRequest, _ctx: RouteContext, next: Next): Promise<Response>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Deps, type DynamicModule, type FactoryProvider } from '@dunx/core';
|
|
2
|
+
import { type CompressionOptionsInit } from './options.js';
|
|
3
|
+
/**
|
|
4
|
+
* Binds `Compression` and its options. Like `StaticModule`, importing it does not
|
|
5
|
+
* install anything - the app decides where in the chain it goes:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* const app = await HttpFactory.create(AppModule, {
|
|
9
|
+
* imports: [CompressionModule.forRoot({ threshold: 2048 })],
|
|
10
|
+
* });
|
|
11
|
+
* app.use(Compression);
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export declare class CompressionModule {
|
|
15
|
+
static forRoot(init?: CompressionOptionsInit): DynamicModule;
|
|
16
|
+
/** `forRoot` with the options read off the container - a config value, usually. */
|
|
17
|
+
static forRootAsync<const D extends Deps>(config: FactoryProvider<CompressionOptionsInit, D> & {
|
|
18
|
+
readonly imports?: DynamicModule['imports'];
|
|
19
|
+
}): DynamicModule;
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CompressionEncoding } from './options.js';
|
|
2
|
+
/**
|
|
3
|
+
* The coding to encode with, or `undefined` for none.
|
|
4
|
+
*
|
|
5
|
+
* `offered` is the server's preference order and breaks a tie, which is what
|
|
6
|
+
* makes `['zstd', 'gzip']` meaningful against a browser that sends both at the
|
|
7
|
+
* same q. An explicit `q=0` refuses a coding, `*` supplies a default for the ones
|
|
8
|
+
* not named, and an absent header means the client said nothing - answered with
|
|
9
|
+
* no encoding, because a client that cannot decode is worse than one that reads
|
|
10
|
+
* a few more bytes.
|
|
11
|
+
*/
|
|
12
|
+
export declare const negotiate: (header: string | null, offered: readonly CompressionEncoding[]) => CompressionEncoding | undefined;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The content codings this package produces.
|
|
3
|
+
*
|
|
4
|
+
* **Brotli is absent, and that is a measurement rather than an oversight.** Bun
|
|
5
|
+
* implements it - `new CompressionStream('brotli')` works - but at 6,344 us to
|
|
6
|
+
* encode a 6.4 KB JSON body against gzip's 23 us and zstd's 24 us, roughly 275x.
|
|
7
|
+
* The `level` argument that would fix it is accepted and ignored: `{ level: 4 }`
|
|
8
|
+
* encodes in 6,345 us and produces the same 339 bytes as the default. Brotli
|
|
9
|
+
* belongs on a build artefact compressed once, not on a response encoded per
|
|
10
|
+
* request. Note also that the `CompressionStream` format is spelled `brotli`
|
|
11
|
+
* while the HTTP token is `br`, so the two never lined up anyway.
|
|
12
|
+
*
|
|
13
|
+
* **`deflate` is absent for a second and worse reason: Bun's two encoders disagree
|
|
14
|
+
* about what it means.** `Bun.deflateSync` emits raw DEFLATE (first bytes
|
|
15
|
+
* `cb 48`), while `CompressionStream('deflate')` emits zlib (`78 9c`), which is
|
|
16
|
+
* what `Content-Encoding: deflate` is defined as. No option reconciles them -
|
|
17
|
+
* `library`, `windowBits` and `level` all leave `deflateSync` raw - so offering
|
|
18
|
+
* the coding would flip wire format at the buffering threshold and serve bytes a
|
|
19
|
+
* strict client rejects. `gzip` is taken by everything that would have accepted
|
|
20
|
+
* `deflate`. Measured on Bun 1.4.0; see docs/bun-apis.md.
|
|
21
|
+
*/
|
|
22
|
+
export declare const CompressionEncoding: Readonly<{
|
|
23
|
+
readonly ZSTD: 'zstd';
|
|
24
|
+
readonly GZIP: 'gzip';
|
|
25
|
+
}>;
|
|
26
|
+
export type CompressionEncoding = (typeof CompressionEncoding)[keyof typeof CompressionEncoding];
|
|
27
|
+
/**
|
|
28
|
+
* Whether a `content-type` is worth encoding.
|
|
29
|
+
*
|
|
30
|
+
* An already-compressed payload - a JPEG, an MP4, a zip - comes out of a second
|
|
31
|
+
* pass slightly larger, having spent the CPU to get there. The default answers
|
|
32
|
+
* no to anything it does not recognise, so a new binary type is skipped rather
|
|
33
|
+
* than wasted on.
|
|
34
|
+
*/
|
|
35
|
+
export declare const isCompressibleType: (contentType: string | null) => boolean;
|
|
36
|
+
export interface CompressionOptionsInit {
|
|
37
|
+
/**
|
|
38
|
+
* The codings offered, most preferred first. A tie in the client's q-values is
|
|
39
|
+
* broken by this order, so it is a real preference and not just a filter.
|
|
40
|
+
*
|
|
41
|
+
* The default puts `zstd` first for speed. On a 6.4 KB JSON body zstd encodes to
|
|
42
|
+
* 372 bytes in 7.7 us where gzip takes 16.1 us to reach 576; on a 116 KB OpenAPI
|
|
43
|
+
* document the two land within 0.2% of each other (9,603 against 9,587), so the
|
|
44
|
+
* size advantage narrows with the body while the time one does not. A client
|
|
45
|
+
* that does not send `zstd` in `accept-encoding` gets gzip, so the order costs
|
|
46
|
+
* nothing to state.
|
|
47
|
+
*
|
|
48
|
+
* @default ['zstd', 'gzip']
|
|
49
|
+
*/
|
|
50
|
+
readonly encodings?: readonly CompressionEncoding[];
|
|
51
|
+
/**
|
|
52
|
+
* Bodies below this many bytes are sent as they are.
|
|
53
|
+
*
|
|
54
|
+
* Only applied when the response declares a `content-length`. A short JSON body
|
|
55
|
+
* grows under gzip - the header and trailer alone are 18 bytes - and the round
|
|
56
|
+
* trip through a compressor is time spent to send more.
|
|
57
|
+
*
|
|
58
|
+
* @default 1024
|
|
59
|
+
*/
|
|
60
|
+
readonly threshold?: number;
|
|
61
|
+
/** Which `content-type`s to encode. @default isCompressibleType */
|
|
62
|
+
readonly filter?: (contentType: string | null) => boolean;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A class, not an interface, so it is a runtime value `@dunx/transform` can record
|
|
66
|
+
* at an injection site - the same reason `StaticOptions` and `ThrottleOptions` are.
|
|
67
|
+
*/
|
|
68
|
+
export declare class CompressionOptions {
|
|
69
|
+
readonly encodings: readonly CompressionEncoding[];
|
|
70
|
+
readonly threshold: number;
|
|
71
|
+
readonly filter: (contentType: string | null) => boolean;
|
|
72
|
+
constructor(init?: CompressionOptionsInit);
|
|
73
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { Controller, Delete, Get, Patch, Post, Put, } from './route/decorators.js';
|
|
2
2
|
export { discoverRoutes, joinPath, type DiscoveredRoute, } from './route/discover.js';
|
|
3
|
-
export type
|
|
3
|
+
export { defaultStatusFor, type DefaultStatus, type HttpMethod, type RouteMeta, type RoutePath, } from './route/marker.js';
|
|
4
4
|
export { ApiHidden, guardsOf, HIDDEN, meta, metaKey, metaOf, mergeMeta, Public, PUBLIC, Roles, ROLES, UNMATCHED, UseGuards, type MetaKey, type MetaRecord, } from './route/metadata.js';
|
|
5
|
-
export type { InferOutput, Input, JsonSchema, RouteInput, RouteSchemas, StandardSchemaIssue, StandardSchemaResult, StandardSchemaV1, } from './route/schema.js';
|
|
5
|
+
export type { InferOutput, Input, JsonSchema, ResponseMap, Returns, RouteInput, RouteSchemas, StandardSchemaIssue, StandardSchemaResult, StandardSchemaV1, } from './route/schema.js';
|
|
6
6
|
export { gatewaysOf, routesOf, type GatewayHandler, type GatewayNode, type RouteInputs, type RouteNode, } from './inspect.js';
|
|
7
7
|
export { ClientAddress } from './server/client-address.js';
|
|
8
8
|
export { buildContext, type RouteContext } from './server/context.js';
|
|
@@ -10,6 +10,7 @@ export { preflight, withCors, type CorsOptions, type CorsOrigin, } from './serve
|
|
|
10
10
|
export { defaultErrorMapper, ErrorFilter, errorMapper, HttpError, isErrorFilter, toErrorMapper, ValidationError, type ErrorHandler, type ErrorMapper, type HttpErrorOptions, type InputSource, type ValidationIssue, } from './server/errors.js';
|
|
11
11
|
export { HttpFactory, type HttpApp, type HttpOptions, } from './server/factory.js';
|
|
12
12
|
export { REQUEST_ID_HEADER } from './server/request-id.js';
|
|
13
|
+
export { TRACEPARENT_HEADER, TRACESTATE_HEADER, TraceContext, type Trace, } from './server/trace-context.js';
|
|
13
14
|
export { RequestLoggingMiddleware, type RequestLoggingOptions, } from './server/request-logging.js';
|
|
14
15
|
export { compose, type Middleware, type Next, type RouteHandler, } from './server/middleware.js';
|
|
15
16
|
export { assertNoCollisions, assertNoGatewayCollisions, buildRoutes, withUpgradeRoutes, type BunRoutes, type GuardResolver, type RouteMethod, type ServeRoutes, } from './server/routes.js';
|
|
@@ -17,6 +18,10 @@ export type { AppSettings } from './server/settings.js';
|
|
|
17
18
|
export { StaticFiles } from './static/files.js';
|
|
18
19
|
export { StaticModule } from './static/module.js';
|
|
19
20
|
export { normalizePrefix, StaticOptions, type StaticOptionsInit, } from './static/options.js';
|
|
21
|
+
export { Compression } from './compression/compression.js';
|
|
22
|
+
export { CompressionModule } from './compression/module.js';
|
|
23
|
+
export { negotiate } from './compression/negotiate.js';
|
|
24
|
+
export { CompressionEncoding, CompressionOptions, isCompressibleType, type CompressionOptionsInit, } from './compression/options.js';
|
|
20
25
|
export { SKIP_THROTTLE, SkipThrottle, THROTTLE, Throttle, type ThrottleLimit, } from './throttle/decorators.js';
|
|
21
26
|
export { ThrottleGuard } from './throttle/guard.js';
|
|
22
27
|
export { ThrottleModule } from './throttle/module.js';
|