@dunx/http 3.1.3 → 3.2.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/dist/inspect.d.ts CHANGED
@@ -1,4 +1,13 @@
1
1
  import { type Dependency, type ModuleRef } from '@dunx/core';
2
+ /**
3
+ * Routes and gateways read off the module graph, constructing nothing. The
4
+ * traversal is `@dunx/core`'s; here is the half needing this package's metadata -
5
+ * route markers, guards, `@Roles`/`@Public`, the gateway marker.
6
+ *
7
+ * `discoverRoutes` walks a prototype chain, and
8
+ * `Object.create(Controller.prototype)` is that chain with nothing behind it, so
9
+ * no constructor or dependency of one has to exist.
10
+ */
2
11
  export interface RouteInputs {
3
12
  readonly body?: string;
4
13
  readonly query?: string;
@@ -3,34 +3,17 @@
3
3
  *
4
4
  * The barrel was a semver promise 173 symbols wide, which is more than this
5
5
  * package meant to make. What stays public there is the surface an app writes
6
- * against:
7
- * decorators, options, contracts, errors, modules and the metadata helpers a
8
- * user's own guard reads. What is here is route-table construction, the
9
- * middleware fold, the relay codec and the discovery readers - things
10
- * `@dunx/dashboard`, `@dunx/mcp` and `@dunx/openapi` need and an app does not.
6
+ * against. What is here is the discovery readers and route metadata that
7
+ * `@dunx/dashboard`, `@dunx/mcp`, `@dunx/openapi` and `@dunx/testing` import.
8
+ *
9
+ * It held 62 symbols and 50 had no importer, all of them reachable from inside
10
+ * this package by relative import. Add one back when a sibling needs it.
11
11
  *
12
12
  * No stability promise attaches to this subpath.
13
13
  */
14
14
  export { discoverRoutes, joinPath, type DiscoveredRoute, } from './route/discover.js';
15
- export { defaultStatusFor, type DefaultStatus, type RouteMeta, } from './route/marker.js';
16
- export { guardsOf } from './route/metadata.js';
15
+ export { defaultStatusFor } from './route/marker.js';
17
16
  export { gatewaysOf, routesOf, type GatewayHandler, type GatewayNode, type RouteInputs, type RouteNode, } from './inspect.js';
18
17
  export { buildContext } from './server/context.js';
19
- export { preflight, withCors } from './server/cors.js';
20
- export { isErrorFilter, toErrorMapper } from './server/errors.js';
21
- export { compose } from './server/middleware.js';
22
- export { assertNoCollisions, assertNoGatewayCollisions, buildRoutes, withUpgradeRoutes, type BunRoutes, type GuardResolver, type RouteMethod, type ServeRoutes, } from './server/routes.js';
23
- export { normalizePrefix } from './static/options.js';
24
- export { negotiate } from './compression/negotiate.js';
25
- export { isCompressibleType } from './compression/options.js';
26
- export { buildWebSocket, type UpgradeHandler, type WebSocketRuntime, } from './ws/adapter.js';
27
- export { discoverGateway, discoverGateways, normalizePath, type DiscoveredGateway, type DiscoveredHandler, type Invoke, } from './ws/discover.js';
28
- export { decode, encode } from './ws/envelope.js';
29
- export { composeSocket, observe } from './ws/middleware.js';
30
- export { HandlerKind, isGateway, type HandlerMeta } from './ws/marker.js';
31
- export { defaultRelayUrl } from './ws/redis-relay.js';
32
- export { decodeRelay, encodeRelay, type RelayFrame, type RelayPhase, } from './ws/relay.js';
33
- export { buildGateways, buildRuntime, type GatewayRuntime, } from './ws/runtime.js';
34
- export { HiddenHealthController } from './health/controller.js';
35
- export { backoffDelay, executeWithRetry, isRetryableStatus, retryAfterMs, } from './client/retry.js';
36
- export { isJsonBody, isPlainObject, safeStringify } from './client/json.js';
18
+ export { embedJson } from './server/html.js';
19
+ export { isGateway } from './ws/marker.js';
package/dist/internal.js CHANGED
@@ -1,56 +1,22 @@
1
1
  // @bun
2
2
  import {
3
3
  HIDDEN,
4
- HandlerKind,
5
- HiddenHealthController,
6
4
  PUBLIC,
7
5
  ROLES,
8
- assertNoCollisions,
9
- assertNoGatewayCollisions,
10
6
  buildContext,
11
- buildGateways,
12
- buildRoutes,
13
- buildRuntime,
14
- buildWebSocket,
15
- compose,
16
- composeSocket,
17
- decode,
18
- decodeRelay,
19
- defaultRelayUrl,
20
7
  defaultStatusFor,
21
8
  discoverGateway,
22
- discoverGateways,
23
9
  discoverRoutes,
24
- encode,
25
- encodeRelay,
26
- guardsOf,
27
- isCompressibleType,
28
- isErrorFilter,
29
10
  isGateway,
30
- joinPath,
31
- negotiate,
32
- normalizePath,
33
- normalizePrefix,
34
- observe,
35
- preflight,
36
- toErrorMapper,
37
- withCors,
38
- withUpgradeRoutes
39
- } from "./chunk-53cs6qek.js";
40
- import {
41
- backoffDelay,
42
- executeWithRetry,
43
- isJsonBody,
44
- isPlainObject,
45
- isRetryableStatus,
46
- retryAfterMs,
47
- safeStringify
48
- } from "./chunk-7xrtwbx3.js";
11
+ joinPath
12
+ } from "./chunk-3nbj06q8.js";
49
13
  import"./chunk-sz4pvqxy.js";
50
14
  // src/inspect.ts
51
15
  import {
16
+ classOf,
52
17
  collectModules,
53
18
  dependenciesOf,
19
+ inertInstance,
54
20
  readControllers
55
21
  } from "@dunx/core";
56
22
  var vendorOf = (schema) => schema?.["~standard"]?.vendor;
@@ -85,11 +51,10 @@ var nodeFor = (route, module) => ({
85
51
  responses: Object.keys(route.options?.response ?? {}).map(Number)
86
52
  });
87
53
  var routesOf = (root) => collectModules(root).flatMap((module) => readControllers(module).flatMap((controller) => {
88
- const { prototype } = controller;
89
- return discoverRoutes(Object.create(prototype)).map((route) => nodeFor(route, module.name));
54
+ return discoverRoutes(inertInstance(controller)).map((route) => nodeFor(route, module.name));
90
55
  }));
91
56
  var gatewayFor = (ctor, module) => {
92
- const { name, path, handlers } = discoverGateway(Object.create(ctor.prototype));
57
+ const { name, path, handlers } = discoverGateway(inertInstance(ctor));
93
58
  return {
94
59
  name,
95
60
  path,
@@ -102,53 +67,16 @@ var gatewayFor = (ctor, module) => {
102
67
  }))
103
68
  };
104
69
  };
105
- var classOf = (entry) => {
106
- if (typeof entry === "function")
107
- return entry;
108
- return entry.provider.kind === "class" ? entry.provider.ctor : undefined;
109
- };
110
- var gatewaysOf = (root) => collectModules(root).flatMap((module) => (module.options.providers ?? []).map(classOf).filter((ctor) => ctor !== undefined).filter(isGateway).map((ctor) => gatewayFor(ctor, module.name)));
70
+ var gatewaysOf = (root) => collectModules(root).flatMap((module) => (module.options.providers ?? []).map((entry) => classOf(entry)?.ctor).filter((ctor) => ctor !== undefined).filter(isGateway).map((ctor) => gatewayFor(ctor, module.name)));
71
+ // src/server/html.ts
72
+ var embedJson = (value) => JSON.stringify(value).replaceAll("<", "\\u003c");
111
73
  export {
112
- HandlerKind,
113
- HiddenHealthController,
114
- assertNoCollisions,
115
- assertNoGatewayCollisions,
116
- backoffDelay,
117
74
  buildContext,
118
- buildGateways,
119
- buildRoutes,
120
- buildRuntime,
121
- buildWebSocket,
122
- compose,
123
- composeSocket,
124
- decode,
125
- decodeRelay,
126
- defaultRelayUrl,
127
75
  defaultStatusFor,
128
- discoverGateway,
129
- discoverGateways,
130
76
  discoverRoutes,
131
- encode,
132
- encodeRelay,
133
- executeWithRetry,
77
+ embedJson,
134
78
  gatewaysOf,
135
- guardsOf,
136
- isCompressibleType,
137
- isErrorFilter,
138
79
  isGateway,
139
- isJsonBody,
140
- isPlainObject,
141
- isRetryableStatus,
142
80
  joinPath,
143
- negotiate,
144
- normalizePath,
145
- normalizePrefix,
146
- observe,
147
- preflight,
148
- retryAfterMs,
149
- routesOf,
150
- safeStringify,
151
- toErrorMapper,
152
- withCors,
153
- withUpgradeRoutes
81
+ routesOf
154
82
  };
@@ -1,5 +1,5 @@
1
1
  import type { BunRequest } from 'bun';
2
- import { type App, type AppOptions, type Ctor, type InjectionToken, type ModuleRef, type ShutdownHookOptions, type ShutdownSignal } from '@dunx/core';
2
+ import { ShutdownAware, type App, type AppOptions, type Ctor, type InjectionToken, type ModuleRef, type ShutdownHookOptions, type ShutdownSignal } from '@dunx/core';
3
3
  import { type DiscoveredRoute } from '../route/discover.js';
4
4
  import type { WebSocketRuntime } from '../ws/adapter.js';
5
5
  import type { SocketLoggingOptions } from '../ws/logging.js';
@@ -50,6 +50,14 @@ export interface HttpOptions extends AppOptions {
50
50
  * See {@link RequestLoggingMiddleware}.
51
51
  */
52
52
  readonly requestLogging?: boolean | RequestLoggingOptions;
53
+ /**
54
+ * Count requests and time them per route, readable through
55
+ * {@link RequestMetrics}. Off by default; `+35.2 ns` per request when
56
+ * `requestLogging` is on, because the entry it already builds shares the
57
+ * timing. With `requestLogging: false` a `MetricsMiddleware` pays for its own
58
+ * `.then` instead, at +175.9 ns.
59
+ */
60
+ readonly metrics?: boolean;
53
61
  /**
54
62
  * One entry at `listen()` naming every route and gateway served. On by default,
55
63
  * and switched separately from `requestLogging`: one is per process, the other
@@ -112,7 +120,7 @@ export interface HttpApp extends App {
112
120
  readonly gatewayPaths: readonly string[];
113
121
  listen(port?: number): Promise<string>;
114
122
  }
115
- export declare class HttpApplication implements HttpApp {
123
+ export declare class HttpApplication extends ShutdownAware implements HttpApp {
116
124
  #private;
117
125
  /** Forwarded from the container so an app can log scope warnings at boot. */
118
126
  readonly warnings: readonly string[];
@@ -138,5 +146,4 @@ export declare class HttpApplication implements HttpApp {
138
146
  * `closed` unresolved; failures are collected and thrown at the end.
139
147
  */
140
148
  shutdown(): Promise<void>;
141
- enableShutdownHooks(signals?: readonly ShutdownSignal[], options?: ShutdownHookOptions): this;
142
149
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Serialise a value for a `<script type="application/json">` block.
3
+ *
4
+ * `<` is the only character that can end the data block early, and escaping it as
5
+ * `\u003c` keeps the text valid JSON - the parser sees the same document either
6
+ * way. `@dunx/dashboard` and `@dunx/openapi` both inline a model into a page they
7
+ * serve, and both had a copy of this; if the escaping ever proves insufficient,
8
+ * one fix should cover both pages.
9
+ */
10
+ export declare const embedJson: (value: unknown) => string;
@@ -0,0 +1,80 @@
1
+ import { type HistogramSnapshot } from '@dunx/core';
2
+ import type { BunRequest, Server } from 'bun';
3
+ import type { RouteContext } from './context.js';
4
+ import type { Middleware, Next } from './middleware.js';
5
+ /** Every path Bun matched nothing for, collapsed into one series. */
6
+ export declare const UNMATCHED_ROUTE = "(unmatched)";
7
+ export interface RouteStats {
8
+ /** The route pattern, so `/users/1` and `/users/2` share one series. */
9
+ readonly route: string;
10
+ readonly method: string;
11
+ readonly count: number;
12
+ /** Keyed by status code as a string, because that is what JSON gives back. */
13
+ readonly byStatus: Readonly<Record<string, number>>;
14
+ /** Nanoseconds. */
15
+ readonly duration: HistogramSnapshot;
16
+ /**
17
+ * The trace of the slowest request on this route so far, which is the only
18
+ * question a p99 provokes: which request was it, and where are its logs.
19
+ */
20
+ readonly slowestTraceId?: string;
21
+ }
22
+ export interface HttpStatsReport {
23
+ readonly routes: readonly RouteStats[];
24
+ /** Read off `Bun.serve` at 14.7 ns rather than counted, so dunx counts nothing. */
25
+ readonly inFlight: number;
26
+ readonly pendingWebSockets: number;
27
+ /** When the counters were last reset, or boot. */
28
+ readonly since: string;
29
+ }
30
+ /**
31
+ * One series per route, keyed on the frozen `RouteContext` that `buildContext`
32
+ * makes once at boot. That object identity is the label set: a `Map` lookup on it
33
+ * is 8.8 ns, where building `${method} ${path}` and hashing it is 206.6 ns.
34
+ *
35
+ * Series count is bounded by the handler count, because `ctx.path` is the route
36
+ * pattern rather than the request's path.
37
+ *
38
+ * Bound by `HttpFactory`'s global wrapper, like `PubSub` and `ClientAddress`: an
39
+ * unbound class self-binds into whichever scope asks first, so a second consumer
40
+ * would be a boot error.
41
+ *
42
+ * `observe` takes everything as parameters and reads no ambient store, which is
43
+ * what keeps it at 35.2 ns folded into the `.then` request logging already
44
+ * allocates.
45
+ */
46
+ export declare class RequestMetrics {
47
+ #private;
48
+ observe(ctx: RouteContext, status: number, durationNs: number, traceId?: string): void;
49
+ snapshot(): HttpStatsReport;
50
+ /**
51
+ * Drops every series rather than zeroing them, so a route that stopped being
52
+ * called stops being reported. A cumulative histogram over a week has a p99
53
+ * reflecting a deploy three days ago; who calls this is the app's decision.
54
+ */
55
+ reset(): void;
56
+ /** Internal: `listen()` hands the bound server to the resolved singleton. */
57
+ attach(server: Server<unknown>): void;
58
+ }
59
+ /**
60
+ * Whether `MetricsMiddleware` is the thing doing the observing.
61
+ *
62
+ * With request logging on - the default - `RequestLoggingMiddleware` observes
63
+ * from the `.then` it already allocates and this middleware would double-count.
64
+ * Exported because `HttpFactory` binds it and `HttpApplication` installs it, and
65
+ * the two disagreeing would mean either no metrics or twice as many.
66
+ */
67
+ export declare const usesMetricsMiddleware: (options: {
68
+ readonly metrics?: boolean;
69
+ readonly requestLogging?: unknown;
70
+ }) => boolean;
71
+ /**
72
+ * Installed by `HttpFactory` only when `requestLogging: false`. With logging on -
73
+ * the default - `RequestLoggingMiddleware` calls `observe` from the `.then` it
74
+ * already allocates, at 35.2 ns against this middleware's 175.9 ns standalone.
75
+ */
76
+ export declare class MetricsMiddleware implements Middleware {
77
+ private readonly metrics;
78
+ constructor(metrics: RequestMetrics);
79
+ handle(_req: BunRequest, ctx: RouteContext, next: Next): Promise<Response>;
80
+ }
@@ -75,6 +75,8 @@ export declare abstract class HttpOptionsProvider {
75
75
  /** `false` removes the middleware from the chain; an object tunes it. */
76
76
  get requestLogging(): boolean | RequestLoggingOptions;
77
77
  get socketLogging(): boolean | SocketLoggingOptions;
78
+ /** Per-route counts and timings. Off by default; see {@link RequestMetrics}. */
79
+ get metrics(): boolean;
78
80
  /**
79
81
  * Replaces the default mapper. Prefer an `ErrorFilter` class over a bare
80
82
  * function: a class is resolved from the container and can inject.
@@ -2,6 +2,7 @@ import { Logger, RequestContext } from '@dunx/core';
2
2
  import type { BunRequest } from 'bun';
3
3
  import type { RouteContext } from './context.js';
4
4
  import type { Middleware, Next } from './middleware.js';
5
+ import type { RequestMetrics } from './metrics.js';
5
6
  export interface RequestLoggingOptions {
6
7
  /** Bodies past this many characters are logged as a size. Default 2048. `0` omits them. */
7
8
  readonly maxBodyLength?: number;
@@ -17,7 +18,7 @@ export interface RequestLoggingOptions {
17
18
  * materialised string by the time this clones it. */
18
19
  readonly responseBody?: boolean;
19
20
  /**
20
- * Paths to skip entirely: no entry, no `x-request-id`, and no
21
+ * Paths to skip entirely: no entry, no trace, no `traceresponse`, and no
21
22
  * `AsyncLocalStorage` scope, so anything the handler logs is uncorrelated.
22
23
  * `correlateIgnored` buys the correlation back.
23
24
  */
@@ -33,24 +34,37 @@ export interface RequestLoggingOptions {
33
34
  */
34
35
  readonly ignorePrefix?: readonly string[];
35
36
  /**
36
- * Keep the request id and the async scope on an `ignore`d path. Default
37
- * `false`. The path still writes no entry; it gets an id on the response and
38
- * everything the handler logs carries it. Costs ~2.2 us of the ~5.4 us the
39
- * default path spends.
37
+ * Keep the trace and the async scope on an `ignore`d path. Default `false`.
38
+ * The path still writes no entry; it gets a `traceresponse` and everything the
39
+ * handler logs carries the trace. Costs ~2.2 us of the ~5.4 us the default path
40
+ * spends.
40
41
  */
41
42
  readonly correlateIgnored?: boolean;
42
43
  /**
43
44
  * Wrap every request in an `AsyncLocalStorage` scope. Default `true`, +0.91 us.
44
- * It is what lets a service four frames down log `requestId` without being
45
+ * It is what lets a service four frames down log `traceId` without being
45
46
  * handed a request. `correlate: false` skips it; this middleware's own entry is
46
- * unchanged, but every other line the request writes loses its id.
47
+ * unchanged, but every other line the request writes loses its trace.
47
48
  */
48
49
  readonly correlate?: boolean;
49
50
  /**
50
- * Adopt W3C Trace Context, so `traceId`, `spanId` and `parentSpanId` join
51
- * `requestId`. Default `false`: it costs a header read and 8 random bytes, and
52
- * `requestId` already spans two dunx services. `@dunx/http/client` sends the
53
- * adopted trace upstream.
51
+ * Put `traceresponse` on the response. Default `true`, and ~500 ns of the 4.7 us
52
+ * the path costs, which is the largest thing here that can go without losing a
53
+ * field from a line.
54
+ *
55
+ * `false` keeps the trace on this middleware's own lines, in the async scope and
56
+ * on the metrics exemplar, and withholds the header from every response
57
+ * including a failure's: the error mapper stamps from what `TraceContext.adopt`
58
+ * marked, and this stops it marking.
59
+ */
60
+ readonly traceResponse?: boolean;
61
+ /**
62
+ * Adopt W3C Trace Context, putting `traceId`, `spanId`, `parentSpanId` and
63
+ * `traceFlags` on every line the request writes and `traceresponse` on its
64
+ * response. Default `true`, at 49.2 ns to mint both ids plus one header read.
65
+ * `@dunx/http/client` sends the adopted trace upstream.
66
+ *
67
+ * `false` removes it, and a request then carries no correlation id at all.
54
68
  */
55
69
  readonly trace?: boolean;
56
70
  }
@@ -70,6 +84,6 @@ export declare class RequestLoggingMiddleware implements Middleware {
70
84
  #private;
71
85
  private readonly logger;
72
86
  private readonly context;
73
- constructor(logger: Logger, context: RequestContext, options?: RequestLoggingOptions);
87
+ constructor(logger: Logger, context: RequestContext, options?: RequestLoggingOptions, metrics?: RequestMetrics);
74
88
  handle(req: BunRequest, ctx: RouteContext, next: Next): Promise<Response>;
75
89
  }
@@ -1,5 +1,17 @@
1
1
  export declare const TRACEPARENT_HEADER = "traceparent";
2
2
  export declare const TRACESTATE_HEADER = "tracestate";
3
+ /**
4
+ * The span that answered, sent back so a caller can record which of the callee's
5
+ * spans its own span points at. Same four fields as `traceparent`, and the
6
+ * the one correlation id a response carries.
7
+ *
8
+ * A W3C Distributed Tracing Working Group proposal rather than a ratified
9
+ * standard: `traceparent` and `tracestate` are the Recommendation, and the
10
+ * published Trace Context Level 2 Candidate Recommendation Draft covers those two
11
+ * request headers and not this response one. The format is specified and stable,
12
+ * and adoption is thin, so treat a caller reading it as a bonus.
13
+ */
14
+ export declare const TRACERESPONSE_HEADER = "traceresponse";
3
15
  export interface Trace {
4
16
  /** 32 hex digits, shared by every span in the trace. */
5
17
  readonly traceId: string;
@@ -15,14 +27,18 @@ export interface Trace {
15
27
  /**
16
28
  * W3C Trace Context, propagated across services.
17
29
  *
18
- * The whole of it is one header parsed and one header written. There is no
19
- * exporter, no sampler and no dependency: what this buys is that every log line a
20
- * request writes carries the same `traceId` the service upstream logged, so the
21
- * two can be joined without either of them running a collector.
30
+ * The whole of it is one header parsed and two written. There is no exporter, no
31
+ * sampler and no dependency: every log line a request writes carries the same
32
+ * `traceId` the service upstream logged, so the two join without either of them
33
+ * running a collector.
34
+ *
35
+ * `traceId`, `spanId` and `parentSpanId` are the OpenTelemetry log data model's
36
+ * own fields, so a collector that ingests these lines correlates them with spans
37
+ * emitted by anything else speaking the standard. Bun 1.4.0 runs OpenTelemetry's
38
+ * Node instrumentation, and a trace adopted here is the trace those spans join.
22
39
  *
23
- * `@dunx/http` does not turn this on by itself - `requestLogging: { trace: true }`
24
- * does. Adopting a trace costs a header read and 8 random bytes on every request,
25
- * which is not worth spending in a service that has nothing to correlate with.
40
+ * On by default. `requestLogging: { trace: false }` removes it, at which point a
41
+ * request carries no correlation id at all.
26
42
  */
27
43
  export declare class TraceContext {
28
44
  #private;
@@ -32,10 +48,15 @@ export declare class TraceContext {
32
48
  * higher version keeps its first four fields, so a future format still
33
49
  * propagates.
34
50
  *
35
- * With nothing inbound, `traceId` is the request id minus its hyphens - a UUID
36
- * is 16 bytes, exactly a trace id, so there is no second `crypto` call.
51
+ * A trace that arrived is continued with a span of this server's own, and the
52
+ * caller's sampling decision is kept rather than overridden.
53
+ *
54
+ * `expose: false` adopts the trace without marking it for {@link stamp}, so no
55
+ * `traceresponse` is written - by this middleware or by the error mapper, which
56
+ * builds its own `Response` from what was recorded here. Everything inward is
57
+ * unchanged: the scope, the log lines, the metrics exemplar.
37
58
  */
38
- static adopt(req: Request, requestId: string): Trace;
59
+ static adopt(req: Request, expose?: boolean): Trace;
39
60
  /** The trace adopted for this request, if one was. */
40
61
  static of(req: Request): Trace | undefined;
41
62
  /**
@@ -43,5 +64,15 @@ export declare class TraceContext {
43
64
  * parent, so the two link without inventing a span nothing logged.
44
65
  */
45
66
  static header(trace: Pick<Trace, 'traceId' | 'spanId' | 'flags'>): string;
67
+ /**
68
+ * The response, carrying `traceresponse` if this request adopted a trace.
69
+ *
70
+ * The logging middleware sets the header on a response it returns, and a
71
+ * failure is never one: the error mapper builds a fresh `Response` outside the
72
+ * chain, so a guard's 401, a validation 400 and every unmatched 404 would go out
73
+ * bare. Read back from the request rather than threaded through the mapper,
74
+ * which an app writes its own of.
75
+ */
76
+ static stamp(response: Response, req: Request): Response;
46
77
  static sampled(trace: Pick<Trace, 'flags'>): boolean;
47
78
  }
@@ -1,5 +1,5 @@
1
+ import type { HandlerMethod } from '@dunx/core';
1
2
  type GatewayTarget = abstract new (...args: never[]) => object;
2
- type HandlerMethod = (...args: never[]) => unknown;
3
3
  export declare const Gateway: (path?: string) => <T extends GatewayTarget>(target: T) => T;
4
4
  /** Runs before the socket exists. Return a `Response` to refuse the upgrade. */
5
5
  export declare const OnUpgrade: () => <T extends HandlerMethod>(value: T) => T;
@@ -1,6 +1,4 @@
1
1
  import { WsRelay } from './relay.js';
2
- /** The same fallback chain `Bun.SQL` uses when given no URL. */
3
- export declare const defaultPostgresRelayUrl: () => string;
4
2
  export interface PostgresRelayOptions {
5
3
  /** @default `$POSTGRES_URL`, `$DATABASE_URL`, then `postgres://localhost:5432` */
6
4
  readonly url?: string;
@@ -27,6 +27,14 @@ export interface PubSubRelay {
27
27
  */
28
28
  close?(): unknown;
29
29
  }
30
+ /**
31
+ * Both relays take a URL, and a wrong scheme has to fail here rather than at
32
+ * connect time: an absence-tolerant relay swallows the connection error, so a
33
+ * typo would degrade silently to single-node fan-out.
34
+ */
35
+ export declare const assertRelayUrl: (url: string, protocols: readonly string[], example: string) => string;
36
+ /** The URL with any password removed, for logs and error messages. */
37
+ export declare const redactUrl: (url: string) => string;
30
38
  /** Which relay call failed, so one message can say what degraded. */
31
39
  export type RelayPhase = 'publish' | 'subscribe' | 'close';
32
40
  export interface RelayOptions {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dunx/http",
3
- "version": "3.1.3",
3
+ "version": "3.2.1",
4
4
  "description": "Bun.serve adapter for the dunx framework: controllers, middleware and WebSocket gateways",
5
5
  "keywords": [
6
6
  "bun",
@@ -56,13 +56,16 @@
56
56
  "typecheck": "tsc --noEmit"
57
57
  },
58
58
  "dependencies": {
59
- "@arkv/shared": "^0.8.0"
59
+ "@arkv/shared": "0.8.0"
60
60
  },
61
61
  "devDependencies": {
62
- "@dunx/core": "workspace:*"
62
+ "@dunx/core": "workspace:*",
63
+ "@opentelemetry/api": "1.9.1",
64
+ "@opentelemetry/core": "2.11.0",
65
+ "@opentelemetry/sdk-trace-node": "2.11.0"
63
66
  },
64
67
  "peerDependencies": {
65
- "@dunx/core": "^3.1.3",
68
+ "@dunx/core": "^3.2.1",
66
69
  "@types/bun": ">=1.3.0"
67
70
  },
68
71
  "peerDependenciesMeta": {