@dunx/http 3.2.0 → 3.3.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/index.js CHANGED
@@ -1,71 +1,122 @@
1
1
  // @bun
2
2
  import {
3
- ApiHidden,
4
- CompressionEncoding,
5
- CompressionOptions,
6
- Controller,
7
- DEFAULT_RELAY_CHANNEL,
8
- Delete,
9
- ErrorFilter,
10
- Get,
11
- HEALTH_REPORT_SCHEMA,
12
- HIDDEN,
3
+ HttpStatusCode2
4
+ } from "./chunk-bg0dr54z.js";
5
+ import {
6
+ defaultStatusFor2,
7
+ markRoute,
8
+ markController,
9
+ metaKey2,
10
+ meta2,
11
+ ROLES2,
12
+ PUBLIC2,
13
+ HIDDEN2,
14
+ UNMATCHED2,
15
+ Roles2,
16
+ Public2,
17
+ ApiHidden2,
18
+ UseGuards2,
19
+ metaOf2,
20
+ mergeMeta2,
21
+ joinPath2,
22
+ discoverRoutes2,
13
23
  HandlerKind,
14
- HealthController,
15
- HealthOptions,
16
- HealthRegistry,
17
- HiddenHealthController,
18
- HttpError,
19
- PUBLIC,
20
- Patch,
21
- Post,
22
- Public,
23
- Put,
24
- ROLES,
25
- RawBody,
26
- RedisRelay,
27
- Roles,
28
- StaticOptions,
29
- UNMATCHED,
30
- UseGuards,
31
- ValidationError,
32
- WsRelay,
33
- assertNoCollisions,
34
- assertNoGatewayCollisions,
35
- buildFallback,
36
- buildRoutes,
37
- buildWebSocket,
38
- decodeRelay,
39
- defaultErrorMapper,
40
- defaultRelayError,
41
- discoverGateways,
42
- discoverRoutes,
43
- encode,
44
- encodeRelay,
45
- errorMapper,
46
- joinPath,
47
- markGateway,
48
24
  markHandler,
49
- mergeMeta,
50
- meta,
51
- metaKey,
52
- metaOf,
53
- negotiate,
54
- observe,
55
- toErrorMapper,
56
- withUpgradeRoutes
57
- } from "./chunk-y85wcdhw.js";
25
+ markGateway,
26
+ discoverGateways,
27
+ buildContext2
28
+ } from "./chunk-8939brh2.js";
58
29
  import {
59
- HttpStatusCode,
60
- TRACEPARENT_HEADER,
61
- TRACERESPONSE_HEADER,
62
- TRACESTATE_HEADER,
63
- TraceContext,
64
- __decorateElement,
65
- __decoratorMetadata,
66
- __decoratorStart,
67
- __runInitializers
68
- } from "./chunk-9x3evk19.js";
30
+ TRACEPARENT_HEADER2,
31
+ TRACESTATE_HEADER2,
32
+ TRACERESPONSE_HEADER2,
33
+ TraceContext2
34
+ } from "./chunk-gmtwad7f.js";
35
+ var __create = Object.create;
36
+ var __defProp = Object.defineProperty;
37
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
38
+ var __name = (target, name) => {
39
+ Object.defineProperty(target, "name", {
40
+ value: name,
41
+ enumerable: false,
42
+ configurable: true
43
+ });
44
+ return target;
45
+ };
46
+ var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
47
+ var __typeError = (msg) => {
48
+ throw TypeError(msg);
49
+ };
50
+ var __defNormalProp = (obj, key, value) => (key in obj) ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
51
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
52
+ var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
53
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
54
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
55
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
56
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
57
+ var __decoratorStart = (base) => [, , , __create(base?.[__knownSymbol("metadata")] ?? null)];
58
+ var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
59
+ var __expectFn = (fn) => fn !== undefined && typeof fn !== "function" ? __typeError("Function expected") : fn;
60
+ var __decoratorContext = (kind, name, done, metadata, fns) => ({
61
+ kind: __decoratorStrings[kind],
62
+ name,
63
+ metadata,
64
+ addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null))
65
+ });
66
+ var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
67
+ var __runInitializers = (array, flags, self, value) => {
68
+ for (var i = 0, fns = array[flags >> 1], n = fns && fns.length;i < n; i++)
69
+ flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
70
+ return value;
71
+ };
72
+ var __decorateElement = (array, flags, name, decorators, target, extra) => {
73
+ var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
74
+ var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
75
+ var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
76
+ var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : {
77
+ get [name]() {
78
+ return __privateGet(this, extra);
79
+ },
80
+ set [name](x) {
81
+ __privateSet(this, extra, x);
82
+ }
83
+ }, name));
84
+ k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
85
+ for (var i = decorators.length - 1;i >= 0; i--) {
86
+ ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
87
+ if (k) {
88
+ ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => (name in x) };
89
+ if (k ^ 3)
90
+ access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
91
+ if (k > 2)
92
+ access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
93
+ }
94
+ it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? undefined : { get: desc.get, set: desc.set } : target, ctx);
95
+ done._ = 1;
96
+ if (k ^ 4 || it === undefined)
97
+ __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
98
+ else if (typeof it !== "object" || it === null)
99
+ __typeError("Object expected");
100
+ else
101
+ __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
102
+ }
103
+ return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
104
+ };
105
+
106
+ // src/route/decorators.ts
107
+ var Controller = (prefix = "") => (target) => {
108
+ markController(target, prefix);
109
+ return target;
110
+ };
111
+ var verb = (method) => (path = "/", options) => (value, _context) => {
112
+ markRoute(value, { method, path, options });
113
+ return value;
114
+ };
115
+ var Get = verb("GET");
116
+ var Post = verb("POST");
117
+ var Put = verb("PUT");
118
+ var Patch = verb("PATCH");
119
+ var Delete = verb("DELETE");
69
120
  // src/server/client-address.ts
70
121
  import { AppError } from "@dunx/core";
71
122
  var trustedHops = (setting) => {
@@ -87,7 +138,7 @@ class ClientAddress {
87
138
  }
88
139
  const hops = trustedHops(source.trustProxy);
89
140
  if (hops > 0) {
90
- const entries = (req.headers.get("x-forwarded-for") ?? "").split(",").map((entry2) => entry2.trim()).filter((entry2) => entry2.length > 0);
141
+ const entries = (req.headers.get("x-forwarded-for") ?? "").split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
91
142
  const entry = entries[Math.max(0, entries.length - hops)];
92
143
  if (entry)
93
144
  return unmap(entry);
@@ -98,10 +149,67 @@ class ClientAddress {
98
149
  var attachAddressSource = (target, source) => {
99
150
  sources.set(target, source);
100
151
  };
152
+ // src/server/errors.ts
153
+ import { AppError as AppError2, ConsoleLogger } from "@dunx/core";
154
+ class HttpError extends AppError2 {
155
+ status;
156
+ name = "HttpError";
157
+ headers;
158
+ constructor(status, message, options) {
159
+ super(message, options);
160
+ this.status = status;
161
+ this.headers = options?.headers;
162
+ }
163
+ }
164
+ Object.defineProperty(HttpError, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "override readonly status: number" }, { unresolved: "message: string" }, { unresolved: "options?: HttpErrorOptions" }] });
165
+
166
+ class ValidationError extends HttpError {
167
+ source;
168
+ issues;
169
+ name = "ValidationError";
170
+ constructor(source, issues) {
171
+ super(HttpStatusCode2.BAD_REQUEST, `Invalid ${source}`);
172
+ this.source = source;
173
+ this.issues = issues;
174
+ }
175
+ }
176
+ Object.defineProperty(ValidationError, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "readonly source: InputSource" }, { unresolved: "readonly issues: readonly ValidationIssue[]" }] });
177
+
178
+ class ErrorFilter {
179
+ }
180
+ var isErrorFilter = (handler) => typeof handler === "function" && typeof handler.prototype?.catch === "function";
181
+ var toErrorMapper = (handler, resolve) => isErrorFilter(handler) ? (error, req) => resolve(handler).catch(error, req) : handler;
182
+ var errorMapper = (logger) => (error) => {
183
+ if (error instanceof ValidationError) {
184
+ return Response.json({ error: error.message, status: error.status, issues: error.issues }, {
185
+ status: error.status,
186
+ ...error.headers && { headers: error.headers }
187
+ });
188
+ }
189
+ if (error instanceof HttpError) {
190
+ return Response.json({ error: error.message, status: error.status }, {
191
+ status: error.status,
192
+ ...error.headers && { headers: error.headers }
193
+ });
194
+ }
195
+ if (error instanceof AppError2 && isStatus(error.status)) {
196
+ if (error.status >= HttpStatusCode2.INTERNAL_SERVER_ERROR) {
197
+ logger.error("Unhandled error", error);
198
+ }
199
+ return Response.json({ error: error.message, status: error.status }, { status: error.status });
200
+ }
201
+ logger.error("Unhandled error", error);
202
+ return Response.json({
203
+ error: "Internal Server Error",
204
+ status: HttpStatusCode2.INTERNAL_SERVER_ERROR
205
+ }, { status: HttpStatusCode2.INTERNAL_SERVER_ERROR });
206
+ };
207
+ var isStatus = (value) => value !== undefined && Number.isInteger(value) && value >= 200 && value <= 599;
208
+ var defaultErrorMapper = errorMapper(new ConsoleLogger);
101
209
  // src/server/factory.ts
102
210
  import {
103
211
  collectModules,
104
- AppError as AppError4,
212
+ AppError as AppError8,
105
213
  AppFactory,
106
214
  Logger as Logger4,
107
215
  provide,
@@ -130,7 +238,7 @@ class RequestMetrics {
130
238
  observe(ctx, status, durationNs, traceId) {
131
239
  let series = this.#series.get(ctx);
132
240
  if (series === undefined) {
133
- if (ctx.get(UNMATCHED) === true) {
241
+ if (ctx.get(UNMATCHED2) === true) {
134
242
  series = this.#unmatched.get(ctx.method);
135
243
  if (series === undefined) {
136
244
  series = seriesFor(UNMATCHED_ROUTE, ctx.method);
@@ -194,13 +302,332 @@ class MetricsMiddleware {
194
302
  this.metrics.observe(ctx, response.status, Bun.nanoseconds() - started);
195
303
  return response;
196
304
  }, (error) => {
197
- this.metrics.observe(ctx, error instanceof HttpError ? error.status : HttpStatusCode.INTERNAL_SERVER_ERROR, Bun.nanoseconds() - started);
305
+ this.metrics.observe(ctx, error instanceof HttpError ? error.status : HttpStatusCode2.INTERNAL_SERVER_ERROR, Bun.nanoseconds() - started);
198
306
  throw error;
199
307
  });
200
308
  }
201
309
  }
202
310
  Object.defineProperty(MetricsMiddleware, Symbol.for("dunx.deps"), { value: () => [RequestMetrics] });
203
311
 
312
+ // src/ws/envelope.ts
313
+ var encode = (event, data) => JSON.stringify({ event, data });
314
+ var decode = (message) => {
315
+ if (typeof message !== "string")
316
+ return;
317
+ let parsed;
318
+ try {
319
+ parsed = JSON.parse(message);
320
+ } catch {
321
+ return;
322
+ }
323
+ if (typeof parsed !== "object" || parsed === null)
324
+ return;
325
+ const { event, data } = parsed;
326
+ return typeof event === "string" ? { event, data } : undefined;
327
+ };
328
+
329
+ // src/ws/middleware.ts
330
+ var composeSocket = (middleware, ctx) => middleware.reduceRight((next, current) => (frame, run) => current.handle(frame, ctx, () => next(frame, run)), (_frame, run) => run());
331
+ var observe = (next, done) => {
332
+ let result;
333
+ try {
334
+ result = next();
335
+ } catch (error) {
336
+ done(error, undefined);
337
+ throw error;
338
+ }
339
+ if (result instanceof Promise) {
340
+ return result.then((value) => {
341
+ done(undefined, value);
342
+ return value;
343
+ }, (error) => {
344
+ done(error, undefined);
345
+ throw error;
346
+ });
347
+ }
348
+ done(undefined, result);
349
+ return result;
350
+ };
351
+
352
+ // src/ws/runtime.ts
353
+ import { AppError as AppError3 } from "@dunx/core";
354
+ var slotOf = (handler) => handler.kind === HandlerKind.MESSAGE && handler.event !== undefined ? `message ${JSON.stringify(handler.event)}` : handler.kind;
355
+ var buildRuntime = (gateway) => {
356
+ if (gateway.handlers.length === 0) {
357
+ throw new AppError3(`${gateway.name} is registered as a gateway but declares no handlers. ` + "Add an @OnMessage/@OnOpen/... method, or drop the @Gateway decorator.");
358
+ }
359
+ const owners = new Map;
360
+ const events = new Map;
361
+ for (const handler of gateway.handlers) {
362
+ const slot = slotOf(handler);
363
+ const existing = owners.get(slot);
364
+ if (existing) {
365
+ throw new AppError3(`Handler collision in ${gateway.name}: ${slot} is claimed by ` + `${existing.method}() and by ${handler.method}(). One handler per event.`);
366
+ }
367
+ owners.set(slot, handler);
368
+ if (handler.kind === HandlerKind.MESSAGE && handler.event !== undefined) {
369
+ events.set(handler.event, handler.invoke);
370
+ }
371
+ }
372
+ const at = (slot) => owners.get(slot)?.invoke;
373
+ return {
374
+ name: gateway.name,
375
+ path: gateway.path,
376
+ upgrade: at(HandlerKind.UPGRADE),
377
+ open: at(HandlerKind.OPEN),
378
+ close: at(HandlerKind.CLOSE),
379
+ drain: at(HandlerKind.DRAIN),
380
+ ping: at(HandlerKind.PING),
381
+ pong: at(HandlerKind.PONG),
382
+ raw: at(HandlerKind.MESSAGE),
383
+ events
384
+ };
385
+ };
386
+ var buildGateways = (discovered) => {
387
+ const byPath = new Map;
388
+ for (const gateway of discovered) {
389
+ const existing = byPath.get(gateway.path);
390
+ if (existing) {
391
+ throw new AppError3(`Gateway path collision: ${gateway.path} is served by ${existing.name} ` + `and by ${gateway.name}. One gateway per path.`);
392
+ }
393
+ byPath.set(gateway.path, buildRuntime(gateway));
394
+ }
395
+ return byPath;
396
+ };
397
+ var someHandler = (gateways, pick) => {
398
+ for (const gateway of gateways)
399
+ if (pick(gateway) !== undefined)
400
+ return true;
401
+ return false;
402
+ };
403
+
404
+ // src/ws/adapter.ts
405
+ var RUNTIME = Symbol.for("dunx.ws.runtime");
406
+ var UNCLAIMED = Symbol.for("dunx.ws.unclaimed");
407
+ var defaultOnError = (error, socket) => {
408
+ console.error(`[dunx/http] ${socket.data.path} handler failed:`, error);
409
+ };
410
+ var reportedByMiddleware = () => {
411
+ return;
412
+ };
413
+ var unreported = (middleware) => "Socket middleware is installed and none of it sets reportsErrors, so a " + "throwing gateway handler is reported nowhere: the console fallback is off " + "whenever middleware wraps the handler. Set reportsErrors on the one that " + "records a failure, or pass websocket.onError. Installed: " + `${middleware.map((entry) => entry.constructor.name).join(", ")}.`;
414
+ var runtimeOf = (socket) => socket.data[RUNTIME];
415
+ var isBinary = (value) => value instanceof ArrayBuffer || ArrayBuffer.isView(value) || value instanceof Blob;
416
+ var replyRaw = (socket, value) => {
417
+ if (value === undefined)
418
+ return;
419
+ socket.send(typeof value === "string" || isBinary(value) ? value : JSON.stringify(value));
420
+ };
421
+ var settle = (result, socket, onError, then) => {
422
+ if (result instanceof Promise) {
423
+ result.then((value) => {
424
+ if (!then)
425
+ return;
426
+ try {
427
+ then(value);
428
+ } catch (error) {
429
+ onError(error, socket);
430
+ }
431
+ }, (error) => onError(error, socket));
432
+ return;
433
+ }
434
+ if (then)
435
+ then(result);
436
+ };
437
+ var framing = (kind) => {
438
+ if (kind === HandlerKind.CLOSE) {
439
+ return (args) => ({
440
+ socket: args[0],
441
+ data: { code: args[1], reason: args[2] }
442
+ });
443
+ }
444
+ if (kind === HandlerKind.OPEN || kind === HandlerKind.DRAIN) {
445
+ return (args) => ({ socket: args[0], data: undefined });
446
+ }
447
+ return (args) => ({ socket: args[1], data: args[0] });
448
+ };
449
+ var NOTHING = () => {
450
+ return;
451
+ };
452
+ var through = (gateway, middleware, kind, event, invoke) => {
453
+ const ctx = {
454
+ gateway: gateway.name,
455
+ path: gateway.path,
456
+ kind,
457
+ event
458
+ };
459
+ const dispatch = composeSocket(middleware, ctx);
460
+ const frameOf = framing(kind);
461
+ const run = invoke ?? NOTHING;
462
+ return (...args) => dispatch(frameOf(args), () => run(...args));
463
+ };
464
+ var withMiddleware = (gateway, middleware) => {
465
+ const wrap = (kind, event, invoke) => through(gateway, middleware, kind, event, invoke);
466
+ const optional = (kind, invoke) => invoke === undefined ? undefined : wrap(kind, undefined, invoke);
467
+ return {
468
+ ...gateway,
469
+ open: wrap(HandlerKind.OPEN, undefined, gateway.open),
470
+ close: wrap(HandlerKind.CLOSE, undefined, gateway.close),
471
+ drain: optional(HandlerKind.DRAIN, gateway.drain),
472
+ ping: optional(HandlerKind.PING, gateway.ping),
473
+ pong: optional(HandlerKind.PONG, gateway.pong),
474
+ raw: optional(HandlerKind.MESSAGE, gateway.raw),
475
+ events: new Map([...gateway.events].map(([event, invoke]) => [
476
+ event,
477
+ wrap(HandlerKind.MESSAGE, event, invoke)
478
+ ]))
479
+ };
480
+ };
481
+ var unclaimedDispatch = (gateway, middleware) => (frame, event) => composeSocket(middleware, {
482
+ gateway: gateway.name,
483
+ path: gateway.path,
484
+ kind: HandlerKind.MESSAGE,
485
+ event
486
+ })(frame, () => {
487
+ return;
488
+ });
489
+ var buildWebSocket = (discovered, options = {}, middleware = []) => {
490
+ const byPath = buildGateways(discovered);
491
+ const wrapped = middleware.length === 0 ? byPath : new Map([...byPath].map(([path, gateway]) => [
492
+ path,
493
+ withMiddleware(gateway, middleware)
494
+ ]));
495
+ const gateways = [...wrapped.values()];
496
+ const onError = options.onError ?? (middleware.length === 0 ? defaultOnError : reportedByMiddleware);
497
+ const reports = options.onError !== undefined || middleware.some((entry) => entry.reportsErrors === true);
498
+ const {
499
+ onError: _onError,
500
+ binaryType,
501
+ backpressureLimit,
502
+ closeOnBackpressureLimit,
503
+ idleTimeout,
504
+ maxPayloadLength,
505
+ perMessageDeflate,
506
+ publishToSelf,
507
+ sendPings,
508
+ ...rest
509
+ } = options;
510
+ const handlerOptions = {
511
+ ...backpressureLimit !== undefined && { backpressureLimit },
512
+ ...closeOnBackpressureLimit !== undefined && { closeOnBackpressureLimit },
513
+ ...idleTimeout !== undefined && { idleTimeout },
514
+ ...maxPayloadLength !== undefined && { maxPayloadLength },
515
+ ...perMessageDeflate !== undefined && { perMessageDeflate },
516
+ ...publishToSelf !== undefined && { publishToSelf },
517
+ ...sendPings !== undefined && { sendPings }
518
+ };
519
+ const run = (invoke, args, ws, then) => {
520
+ try {
521
+ settle(invoke(...args), ws, onError, then);
522
+ } catch (error) {
523
+ onError(error, ws);
524
+ }
525
+ };
526
+ const websocket = {
527
+ ...handlerOptions,
528
+ message(ws, message) {
529
+ const gateway = runtimeOf(ws);
530
+ let event;
531
+ if (gateway.events.size > 0) {
532
+ const envelope = decode(message);
533
+ const handler = envelope && gateway.events.get(envelope.event);
534
+ if (envelope && handler) {
535
+ run(handler, [envelope.data, ws], ws, (value) => {
536
+ if (value !== undefined)
537
+ ws.send(encode(envelope.event, value));
538
+ });
539
+ return;
540
+ }
541
+ event = envelope?.event;
542
+ }
543
+ if (gateway.raw) {
544
+ run(gateway.raw, [message, ws], ws, (value) => replyRaw(ws, value));
545
+ return;
546
+ }
547
+ const unclaimed = ws.data[UNCLAIMED];
548
+ if (!unclaimed)
549
+ return;
550
+ try {
551
+ settle(unclaimed({ socket: ws, data: message }, event), ws, onError, undefined);
552
+ } catch (error) {
553
+ onError(error, ws);
554
+ }
555
+ },
556
+ ...(someHandler(gateways, (g) => g.open) || binaryType !== undefined) && {
557
+ open(ws) {
558
+ if (binaryType !== undefined)
559
+ ws.binaryType = binaryType;
560
+ const { open } = runtimeOf(ws);
561
+ if (open)
562
+ run(open, [ws], ws, undefined);
563
+ }
564
+ },
565
+ ...someHandler(gateways, (g) => g.close) && {
566
+ close(ws, code, reason) {
567
+ const { close } = runtimeOf(ws);
568
+ if (close)
569
+ run(close, [ws, code, reason], ws, undefined);
570
+ }
571
+ },
572
+ ...someHandler(gateways, (g) => g.drain) && {
573
+ drain(ws) {
574
+ const { drain } = runtimeOf(ws);
575
+ if (drain)
576
+ run(drain, [ws], ws, undefined);
577
+ }
578
+ },
579
+ ...someHandler(gateways, (g) => g.ping) && {
580
+ ping(ws, data) {
581
+ const { ping } = runtimeOf(ws);
582
+ if (ping)
583
+ run(ping, [data, ws], ws, undefined);
584
+ }
585
+ },
586
+ ...someHandler(gateways, (g) => g.pong) && {
587
+ pong(ws, data) {
588
+ const { pong } = runtimeOf(ws);
589
+ if (pong)
590
+ run(pong, [data, ws], ws, undefined);
591
+ }
592
+ }
593
+ };
594
+ const unclaimed = new Map(middleware.length === 0 ? [] : gateways.map((gateway) => [
595
+ gateway,
596
+ unclaimedDispatch(gateway, middleware)
597
+ ]));
598
+ const accept = (req, server, gateway, context) => {
599
+ const fallback = unclaimed.get(gateway);
600
+ const data = {
601
+ path: gateway.path,
602
+ context,
603
+ id: crypto.randomUUID(),
604
+ [RUNTIME]: gateway,
605
+ ...fallback === undefined ? {} : { [UNCLAIMED]: fallback }
606
+ };
607
+ return server.upgrade(req, { data }) ? undefined : new Response("Expected a WebSocket upgrade", { status: 426 });
608
+ };
609
+ const upgradeHandler = (gateway) => (req, server) => {
610
+ if (!gateway.upgrade)
611
+ return accept(req, server, gateway, undefined);
612
+ const result = gateway.upgrade(req);
613
+ if (result instanceof Promise) {
614
+ return result.then((value) => value instanceof Response ? value : accept(req, server, gateway, value));
615
+ }
616
+ return result instanceof Response ? result : accept(req, server, gateway, result);
617
+ };
618
+ return {
619
+ websocket,
620
+ routes: new Map(gateways.map((gateway) => [gateway.path, upgradeHandler(gateway)])),
621
+ paths: [...byPath.keys()],
622
+ warnings: middleware.length > 0 && !reports ? [unreported(middleware)] : [],
623
+ gateways: gateways.map((gateway) => ({
624
+ name: gateway.name,
625
+ path: gateway.path,
626
+ events: [...gateway.events.keys()]
627
+ }))
628
+ };
629
+ };
630
+
204
631
  // src/ws/logging.ts
205
632
  import { Logger, LogLevel, RequestContext } from "@dunx/core";
206
633
  var LIFECYCLE_LABEL = {
@@ -292,6 +719,8 @@ class SocketLoggingMiddleware {
292
719
  if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
293
720
  return `[${data.byteLength} bytes]`;
294
721
  }
722
+ if (data instanceof Blob)
723
+ return `[${data.size} bytes]`;
295
724
  const text = JSON.stringify(data) ?? "";
296
725
  return text.length > this.#limit ? `[${text.length} chars]` : data;
297
726
  }
@@ -299,7 +728,64 @@ class SocketLoggingMiddleware {
299
728
  Object.defineProperty(SocketLoggingMiddleware, Symbol.for("dunx.deps"), { value: () => [Logger, RequestContext, { unresolved: "options: SocketLoggingOptions = {}", optional: true }] });
300
729
 
301
730
  // src/ws/pubsub.ts
302
- import { AppError as AppError2 } from "@dunx/core";
731
+ import { AppError as AppError5 } from "@dunx/core";
732
+
733
+ // src/ws/relay.ts
734
+ import { AppError as AppError4 } from "@dunx/core";
735
+ var assertRelayUrl = (url, protocols, example) => {
736
+ let parsed;
737
+ try {
738
+ parsed = new URL(url);
739
+ } catch {
740
+ throw new AppError4(`${JSON.stringify(url)} is not a valid URL for the websocket relay. ` + `Expected something like ${example}.`);
741
+ }
742
+ if (!protocols.includes(parsed.protocol)) {
743
+ throw new AppError4(`Unsupported protocol ${JSON.stringify(parsed.protocol)} in ` + `${JSON.stringify(url)}. Expected one of ${protocols.join(", ")}.`);
744
+ }
745
+ return url;
746
+ };
747
+ var redactUrl = (url) => {
748
+ const parsed = new URL(url);
749
+ if (parsed.password)
750
+ parsed.password = "***";
751
+ return parsed.toString();
752
+ };
753
+ var DEFAULT_RELAY_CHANNEL = "dunx:ws";
754
+ var defaultRelayError = (error, phase) => {
755
+ console.warn(`[dunx/http] the websocket relay could not ${phase}. Fan-out is local to ` + "this process until it recovers:", error);
756
+ };
757
+ var toBytes = (data) => ArrayBuffer.isView(data) ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : new Uint8Array(data);
758
+ var encodeRelay = (origin, topic, data) => typeof data === "string" ? JSON.stringify({ o: origin, t: topic, d: data }) : JSON.stringify({
759
+ o: origin,
760
+ t: topic,
761
+ d: Buffer.from(toBytes(data)).toString("base64"),
762
+ b: 1
763
+ });
764
+ var decodeRelay = (message) => {
765
+ let parsed;
766
+ try {
767
+ parsed = JSON.parse(message);
768
+ } catch {
769
+ return;
770
+ }
771
+ if (typeof parsed !== "object" || parsed === null)
772
+ return;
773
+ const { o, t, d, b } = parsed;
774
+ if (typeof o !== "string" || typeof t !== "string" || typeof d !== "string") {
775
+ return;
776
+ }
777
+ return { origin: o, topic: t, data: b ? Buffer.from(d, "base64") : d };
778
+ };
779
+
780
+ class WsRelay {
781
+ constructor() {
782
+ if (new.target === WsRelay) {
783
+ throw new AppError4("WsRelay is a contract, not an implementation. Bind one with " + "WsRelayModule.forRoot() for Redis or WsRelayModule.forPostgres() " + "for Postgres, or extend it with a relay of your own.");
784
+ }
785
+ }
786
+ }
787
+
788
+ // src/ws/pubsub.ts
303
789
  class PubSub {
304
790
  #origin = Bun.randomUUIDv7();
305
791
  #server;
@@ -324,7 +810,7 @@ class PubSub {
324
810
  }
325
811
  async relayThrough(relay, options = {}) {
326
812
  if (this.#relay) {
327
- throw new AppError2("PubSub already relays. Two subscriptions on one channel would deliver " + "every relayed message twice - pass HttpOptions.relay or call " + "relayThrough(), not both.");
813
+ throw new AppError5("PubSub already relays. Two subscriptions on one channel would deliver " + "every relayed message twice - pass HttpOptions.relay or call " + "relayThrough(), not both.");
328
814
  }
329
815
  this.#relay = relay;
330
816
  this.#channel = options.channel ?? DEFAULT_RELAY_CHANNEL;
@@ -420,7 +906,7 @@ class PubSub {
420
906
  }
421
907
  #live() {
422
908
  if (!this.#server) {
423
- throw new AppError2("PubSub has no server yet. Publish once the server is listening: " + "HttpApp.listen() is what attaches it.");
909
+ throw new AppError5("PubSub has no server yet. Publish once the server is listening: " + "HttpApp.listen() is what attaches it.");
424
910
  }
425
911
  return this.#server;
426
912
  }
@@ -428,10 +914,10 @@ class PubSub {
428
914
 
429
915
  // src/server/application.ts
430
916
  import {
431
- AppError as AppError3,
917
+ AppError as AppError7,
432
918
  Logger as Logger3,
433
919
  runtimeInfo,
434
- ShutdownHooks,
920
+ ShutdownAware,
435
921
  teardownError,
436
922
  teardownFailures as toFailures
437
923
  } from "@dunx/core";
@@ -441,6 +927,27 @@ import {
441
927
  Logger as Logger2,
442
928
  RequestContext as RequestContext2
443
929
  } from "@dunx/core";
930
+
931
+ // src/server/raw-body.ts
932
+ var WANTED = Symbol.for("dunx.http.rawBody.wanted");
933
+ var TEXT = Symbol.for("dunx.http.rawBody.text");
934
+
935
+ class RawBody {
936
+ static want(req) {
937
+ req[WANTED] = true;
938
+ }
939
+ static wanted(req) {
940
+ return req[WANTED] === true;
941
+ }
942
+ static record(req, text) {
943
+ req[TEXT] = text;
944
+ }
945
+ static read(req) {
946
+ return req[TEXT];
947
+ }
948
+ }
949
+
950
+ // src/server/request-logging.ts
444
951
  var parse = (text, limit) => {
445
952
  if (limit === 0)
446
953
  return;
@@ -509,7 +1016,7 @@ class RequestLoggingMiddleware {
509
1016
  context: `${ctx.controller}.${ctx.handler}`
510
1017
  };
511
1018
  if (this.#trace) {
512
- const trace = TraceContext.adopt(req, this.#traceResponse);
1019
+ const trace = TraceContext2.adopt(req, this.#traceResponse);
513
1020
  scope.traceId = trace.traceId;
514
1021
  scope.spanId = trace.spanId;
515
1022
  scope.traceFlags = trace.flags;
@@ -553,7 +1060,7 @@ class RequestLoggingMiddleware {
553
1060
  #ignoredWithMetrics(req, ctx, path, next) {
554
1061
  const started = Bun.nanoseconds();
555
1062
  const failed = (error) => {
556
- this.#observe(req, ctx, error instanceof HttpError ? error.status : HttpStatusCode.INTERNAL_SERVER_ERROR, started);
1063
+ this.#observe(req, ctx, error instanceof HttpError ? error.status : HttpStatusCode2.INTERNAL_SERVER_ERROR, started);
557
1064
  throw error;
558
1065
  };
559
1066
  let settled;
@@ -568,8 +1075,8 @@ class RequestLoggingMiddleware {
568
1075
  }, failed);
569
1076
  }
570
1077
  #correlated(req, ctx, path, next) {
571
- const trace = this.#trace ? TraceContext.adopt(req, this.#traceResponse) : undefined;
572
- const stamp = (response) => trace === undefined ? response : TraceContext.stamp(response, req);
1078
+ const trace = this.#trace ? TraceContext2.adopt(req, this.#traceResponse) : undefined;
1079
+ const stamp = (response) => trace === undefined ? response : TraceContext2.stamp(response, req);
573
1080
  if (!this.#correlate)
574
1081
  return next().then(stamp);
575
1082
  return this.context.runWithContext({
@@ -601,7 +1108,7 @@ class RequestLoggingMiddleware {
601
1108
  }
602
1109
  #failed(req, ctx, path, started, request, error, scope) {
603
1110
  this.#shared(req, request);
604
- const status = error instanceof HttpError ? error.status : HttpStatusCode.INTERNAL_SERVER_ERROR;
1111
+ const status = error instanceof HttpError ? error.status : HttpStatusCode2.INTERNAL_SERVER_ERROR;
605
1112
  const entry = {
606
1113
  ...scope,
607
1114
  request,
@@ -611,7 +1118,7 @@ class RequestLoggingMiddleware {
611
1118
  };
612
1119
  this.#observe(req, ctx, status, started);
613
1120
  const line = `${req.method} ${path} ${status}`;
614
- if (status < HttpStatusCode.INTERNAL_SERVER_ERROR) {
1121
+ if (status < HttpStatusCode2.INTERNAL_SERVER_ERROR) {
615
1122
  this.logger.warn(line, entry);
616
1123
  } else {
617
1124
  this.logger.error(line, entry);
@@ -628,7 +1135,7 @@ class RequestLoggingMiddleware {
628
1135
  statusCode: response.status,
629
1136
  elapsedMs: elapsedMs2(started)
630
1137
  });
631
- return TraceContext.stamp(response, req);
1138
+ return TraceContext2.stamp(response, req);
632
1139
  }
633
1140
  return body.then((value) => {
634
1141
  this.logger.info(`${req.method} ${path} ${response.status}`, {
@@ -638,13 +1145,13 @@ class RequestLoggingMiddleware {
638
1145
  ...value === undefined ? {} : { responseBody: value },
639
1146
  elapsedMs: elapsedMs2(started)
640
1147
  });
641
- return TraceContext.stamp(response, req);
1148
+ return TraceContext2.stamp(response, req);
642
1149
  });
643
1150
  }
644
1151
  #observe(req, ctx, status, started) {
645
1152
  if (this.#metrics === undefined)
646
1153
  return;
647
- this.#metrics.observe(ctx, status, Bun.nanoseconds() - started, TraceContext.of(req)?.traceId);
1154
+ this.#metrics.observe(ctx, status, Bun.nanoseconds() - started, TraceContext2.of(req)?.traceId);
648
1155
  }
649
1156
  #body(req, ctx) {
650
1157
  if (!this.#requestBody)
@@ -671,11 +1178,370 @@ class RequestLoggingMiddleware {
671
1178
  }
672
1179
  Object.defineProperty(RequestLoggingMiddleware, Symbol.for("dunx.deps"), { value: () => [Logger2, RequestContext2, { unresolved: "options: RequestLoggingOptions = {}", optional: true }, { unresolved: "metrics?: RequestMetrics", typeOnly: "RequestMetrics" }] });
673
1180
 
1181
+ // src/server/routes.ts
1182
+ import { AppError as AppError6 } from "@dunx/core";
1183
+
1184
+ // src/server/cors.ts
1185
+ var ORIGIN = "access-control-allow-origin";
1186
+ var allowedOrigin = (options, requested) => {
1187
+ const origin = options.origin ?? "*";
1188
+ if (typeof origin === "string") {
1189
+ if (origin !== "*")
1190
+ return origin === requested ? origin : undefined;
1191
+ if (!options.credentials)
1192
+ return "*";
1193
+ return requested ?? undefined;
1194
+ }
1195
+ if (requested === null)
1196
+ return;
1197
+ const allowed = typeof origin === "function" ? origin(requested) : origin.includes(requested);
1198
+ return allowed ? requested : undefined;
1199
+ };
1200
+ var applyCors = (options, req, response) => {
1201
+ const origin = allowedOrigin(options, req.headers.get("origin"));
1202
+ if (origin === undefined)
1203
+ return response;
1204
+ response.headers.set(ORIGIN, origin);
1205
+ if (origin !== "*")
1206
+ response.headers.append("vary", "Origin");
1207
+ if (options.credentials) {
1208
+ response.headers.set("access-control-allow-credentials", "true");
1209
+ }
1210
+ if (options.exposedHeaders?.length) {
1211
+ response.headers.set("access-control-expose-headers", options.exposedHeaders.join(", "));
1212
+ }
1213
+ return response;
1214
+ };
1215
+ var withCors = (options, handler) => {
1216
+ return async (req) => applyCors(options, req, await handler(req));
1217
+ };
1218
+ var preflight = (options, methods) => {
1219
+ const allowMethods = (options.methods ?? methods).join(", ");
1220
+ return async (req) => {
1221
+ const response = applyCors(options, req, new Response(null, { status: HttpStatusCode2.NO_CONTENT }));
1222
+ if (!response.headers.has(ORIGIN))
1223
+ return response;
1224
+ response.headers.set("access-control-allow-methods", allowMethods);
1225
+ const allowHeaders = options.allowedHeaders ?? (req.headers.get("access-control-request-headers") ?? "").split(",").map((header) => header.trim()).filter((header) => header.length > 0);
1226
+ if (allowHeaders.length > 0) {
1227
+ response.headers.set("access-control-allow-headers", allowHeaders.join(", "));
1228
+ }
1229
+ if (options.maxAge !== undefined) {
1230
+ response.headers.set("access-control-max-age", String(options.maxAge));
1231
+ }
1232
+ return response;
1233
+ };
1234
+ };
1235
+
1236
+ // src/server/input.ts
1237
+ var grouped = (entries) => {
1238
+ const collected = {};
1239
+ entries.forEach((value, key) => {
1240
+ const existing = collected[key];
1241
+ if (existing === undefined)
1242
+ collected[key] = value;
1243
+ else if (Array.isArray(existing))
1244
+ existing.push(value);
1245
+ else
1246
+ collected[key] = [existing, value];
1247
+ });
1248
+ return collected;
1249
+ };
1250
+ var asJson = (req) => req.json();
1251
+ var asUrlEncoded = async (req) => grouped(new URLSearchParams(await req.text()));
1252
+ var asMultipart = async (req) => grouped(await req.formData());
1253
+ var asText = (req) => req.text();
1254
+ var parserFor = (media) => {
1255
+ if (media === "application/json" || media.endsWith("+json"))
1256
+ return asJson;
1257
+ if (media === "application/x-www-form-urlencoded")
1258
+ return asUrlEncoded;
1259
+ if (media === "multipart/form-data")
1260
+ return asMultipart;
1261
+ if (media.startsWith("text/"))
1262
+ return asText;
1263
+ return;
1264
+ };
1265
+ var JSON_MEDIA = "application/json";
1266
+ var mediaTypeOf = (req) => {
1267
+ const header = req.headers.get("content-type");
1268
+ if (header === JSON_MEDIA || header === null)
1269
+ return JSON_MEDIA;
1270
+ const end = header.indexOf(";");
1271
+ const media = (end === -1 ? header : header.slice(0, end)).trim();
1272
+ return media === "" ? JSON_MEDIA : media.toLowerCase();
1273
+ };
1274
+ var flatten = (issue) => {
1275
+ const path = issue.path?.map((segment) => String(typeof segment === "object" ? segment.key : segment)).join(".");
1276
+ return path === undefined || path === "" ? { message: issue.message } : { message: issue.message, path };
1277
+ };
1278
+ var accept = (source, result) => {
1279
+ if (result.issues !== undefined) {
1280
+ throw new ValidationError(source, result.issues.map(flatten));
1281
+ }
1282
+ return result.value;
1283
+ };
1284
+ var fillWith = (draft, source, schema, value) => {
1285
+ const result = schema["~standard"].validate(value);
1286
+ if (result instanceof Promise) {
1287
+ return result.then((settled) => {
1288
+ draft[source] = accept(source, settled);
1289
+ return draft;
1290
+ });
1291
+ }
1292
+ draft[source] = accept(source, result);
1293
+ return draft;
1294
+ };
1295
+ var bodyFill = (schema) => (draft) => {
1296
+ const media = mediaTypeOf(draft.req);
1297
+ const parse = parserFor(media);
1298
+ if (parse === undefined) {
1299
+ throw new HttpError(HttpStatusCode2.UNSUPPORTED_MEDIA_TYPE, `Unsupported content type "${media}". Declared bodies accept ` + "application/json, application/x-www-form-urlencoded, multipart/form-data or text/*.");
1300
+ }
1301
+ const read = parse === asJson && RawBody.wanted(draft.req) ? draft.req.text().then((text) => {
1302
+ RawBody.record(draft.req, text);
1303
+ return JSON.parse(text);
1304
+ }) : parse(draft.req);
1305
+ return read.then((value) => fillWith(draft, "body", schema, value), (error) => {
1306
+ throw new HttpError(HttpStatusCode2.BAD_REQUEST, `Malformed ${media} body`, { cause: error });
1307
+ });
1308
+ };
1309
+ var searchOf = (url) => {
1310
+ const start = url.indexOf("?");
1311
+ if (start === -1)
1312
+ return "";
1313
+ const end = url.indexOf("#", start + 1);
1314
+ return end === -1 ? url.slice(start + 1) : url.slice(start + 1, end);
1315
+ };
1316
+ var queryFill = (schema) => (draft) => {
1317
+ const params = new URLSearchParams(searchOf(draft.req.url));
1318
+ return fillWith(draft, "query", schema, grouped(params));
1319
+ };
1320
+ var paramsFill = (schema) => (draft) => fillWith(draft, "params", schema, draft.req.params);
1321
+ var then = (first, second) => (draft) => {
1322
+ const started = first(draft);
1323
+ return started instanceof Promise ? started.then(second) : second(started);
1324
+ };
1325
+ var buildInputReader = (options) => {
1326
+ const fills = [];
1327
+ if (options?.body !== undefined)
1328
+ fills.push(bodyFill(options.body));
1329
+ if (options?.query !== undefined)
1330
+ fills.push(queryFill(options.query));
1331
+ if (options?.params !== undefined)
1332
+ fills.push(paramsFill(options.params));
1333
+ if (fills.length === 0)
1334
+ return (req) => ({ req });
1335
+ const fill = fills.reduce(then);
1336
+ return (req) => fill({ req });
1337
+ };
1338
+
1339
+ // src/server/middleware.ts
1340
+ var compose = (middleware, ctx, handler) => middleware.reduceRight((next, current) => (req) => current.handle(req, ctx, () => next(req)), handler);
1341
+
1342
+ // src/server/routes.ts
1343
+ var construct = (guard) => new guard;
1344
+ var toResponse = (value, status) => {
1345
+ if (value instanceof Response)
1346
+ return value;
1347
+ if (value === undefined || value === null) {
1348
+ return new Response(null, { status: HttpStatusCode2.NO_CONTENT });
1349
+ }
1350
+ return Response.json(value, { status });
1351
+ };
1352
+ var statusFor = (route) => route.options?.status ?? defaultStatusFor2(route.method);
1353
+ var assertNoCollisions = (discovered) => {
1354
+ const owners = new Map;
1355
+ for (const route of discovered) {
1356
+ const key = `${route.method} ${route.path}`;
1357
+ const owner = `${route.controller}.${route.handlerName}`;
1358
+ const existing = owners.get(key);
1359
+ if (existing !== undefined) {
1360
+ throw new AppError6(`Route collision: ${key} is declared by ${existing} and by ${owner}. ` + "Bun would keep only one of them.");
1361
+ }
1362
+ owners.set(key, owner);
1363
+ }
1364
+ };
1365
+ var assertNoGatewayCollisions = (discovered, gatewayPaths) => {
1366
+ const gateways = new Set(gatewayPaths);
1367
+ for (const route of discovered) {
1368
+ if (gateways.has(route.path)) {
1369
+ throw new AppError6(`Gateway path collision: ${route.path} is served by a gateway and by ` + `${route.controller}.${route.handlerName}(). The upgrade is a route too, ` + "so one of them would be dropped.");
1370
+ }
1371
+ }
1372
+ };
1373
+ var withUpgradeRoutes = (routes, gateways) => {
1374
+ const merged = { ...routes };
1375
+ for (const [path, upgrade] of gateways)
1376
+ merged[path] = { GET: upgrade };
1377
+ return merged;
1378
+ };
1379
+ var unmatchedContext = (req, isPublic) => Object.freeze({
1380
+ controller: "(unmatched)",
1381
+ handler: "(none)",
1382
+ method: req.method,
1383
+ path: new URL(req.url).pathname,
1384
+ parsesBody: false,
1385
+ get: (key) => {
1386
+ if (key.id === UNMATCHED2.id)
1387
+ return true;
1388
+ if (key.id === PUBLIC2.id && isPublic)
1389
+ return true;
1390
+ return;
1391
+ }
1392
+ });
1393
+ var buildFallback = (middleware = [], onError = defaultErrorMapper, cors, notFound = "guarded") => {
1394
+ const miss = () => {
1395
+ throw new HttpError(HttpStatusCode2.NOT_FOUND, "NOT_FOUND");
1396
+ };
1397
+ const run = async (req) => {
1398
+ try {
1399
+ return await compose(middleware, unmatchedContext(req, notFound === "public"), miss)(req);
1400
+ } catch (error) {
1401
+ return TraceContext2.stamp(onError(error, req), req);
1402
+ }
1403
+ };
1404
+ return cors ? withCors(cors, run) : run;
1405
+ };
1406
+ var directOr = (guarded, route, read, status, onError, noMiddleware) => {
1407
+ if (!noMiddleware)
1408
+ return guarded;
1409
+ const settle = (value, req) => {
1410
+ try {
1411
+ return toResponse(value, status);
1412
+ } catch (error) {
1413
+ return onError(error, req);
1414
+ }
1415
+ };
1416
+ const invoke = (input, req) => {
1417
+ try {
1418
+ const value = route.handler(input);
1419
+ return value instanceof Promise ? value.then((resolved) => settle(resolved, req), (error) => onError(error, req)) : settle(value, req);
1420
+ } catch (error) {
1421
+ return onError(error, req);
1422
+ }
1423
+ };
1424
+ return (req) => {
1425
+ try {
1426
+ const input = read(req);
1427
+ return input instanceof Promise ? input.then((resolved) => invoke(resolved, req), (error) => onError(error, req)) : invoke(input, req);
1428
+ } catch (error) {
1429
+ return onError(error, req);
1430
+ }
1431
+ };
1432
+ };
1433
+ var buildRoutes = (discovered, middleware = [], onError = defaultErrorMapper, cors, resolve = construct) => {
1434
+ assertNoCollisions(discovered);
1435
+ const routes = {};
1436
+ const instances = new Map;
1437
+ const guardOf = (guard, from) => {
1438
+ const existing = instances.get(guard);
1439
+ if (existing)
1440
+ return existing;
1441
+ const created = resolve(guard, from);
1442
+ instances.set(guard, created);
1443
+ return created;
1444
+ };
1445
+ for (const route of discovered) {
1446
+ const read = buildInputReader(route.options);
1447
+ const status = statusFor(route);
1448
+ const chain = [
1449
+ ...middleware,
1450
+ ...(route.moduleMiddleware ?? []).map((entry) => guardOf(entry, route.module)),
1451
+ ...(route.guards ?? []).map((guard) => guardOf(guard, route.module))
1452
+ ];
1453
+ const chained = compose(chain, buildContext2(route), async (req) => toResponse(await route.handler(await read(req)), status));
1454
+ const guarded = async (req) => {
1455
+ try {
1456
+ return await chained(req);
1457
+ } catch (error) {
1458
+ return TraceContext2.stamp(onError(error, req), req);
1459
+ }
1460
+ };
1461
+ const byMethod = routes[route.path] ??= {};
1462
+ byMethod[route.method] = cors ? withCors(cors, guarded) : directOr(guarded, route, read, status, onError, chain.length === 0);
1463
+ }
1464
+ if (cors) {
1465
+ for (const byMethod of Object.values(routes)) {
1466
+ byMethod.OPTIONS = preflight(cors, Object.keys(byMethod));
1467
+ }
1468
+ }
1469
+ return routes;
1470
+ };
1471
+
1472
+ // src/server/binding.ts
1473
+ class ServerBinding {
1474
+ #protocols;
1475
+ #main;
1476
+ #gateways;
1477
+ constructor(protocols) {
1478
+ this.#protocols = protocols;
1479
+ }
1480
+ bind(plan) {
1481
+ const { websocket: ws } = plan;
1482
+ const { http2, http1, gatewayPort } = this.#protocols;
1483
+ const split = ws !== undefined && gatewayPort !== undefined;
1484
+ const table = ws && !split ? {
1485
+ routes: withUpgradeRoutes(plan.routes, ws.routes),
1486
+ websocket: ws.websocket
1487
+ } : { routes: plan.routes };
1488
+ this.#main = Bun.serve({
1489
+ port: plan.port,
1490
+ fetch: plan.fetch,
1491
+ ...http2 !== undefined && { http2 },
1492
+ ...http1 !== undefined && { http1 },
1493
+ ...table
1494
+ });
1495
+ if (split) {
1496
+ try {
1497
+ this.#gateways = Bun.serve({
1498
+ port: gatewayPort,
1499
+ fetch: plan.fetch,
1500
+ routes: withUpgradeRoutes({}, ws.routes),
1501
+ websocket: ws.websocket
1502
+ });
1503
+ } catch (error) {
1504
+ this.#main.stop(true);
1505
+ this.#main = undefined;
1506
+ throw error;
1507
+ }
1508
+ }
1509
+ const main = this.#main;
1510
+ const gateways = this.#gateways;
1511
+ return {
1512
+ main,
1513
+ sockets: gateways ?? main,
1514
+ gauges: {
1515
+ get pendingRequests() {
1516
+ return main.pendingRequests + (gateways?.pendingRequests ?? 0);
1517
+ },
1518
+ get pendingWebSockets() {
1519
+ return main.pendingWebSockets + (gateways?.pendingWebSockets ?? 0);
1520
+ }
1521
+ }
1522
+ };
1523
+ }
1524
+ get gatewayUrl() {
1525
+ return this.#gateways?.url.href;
1526
+ }
1527
+ async stop(force) {
1528
+ const main = this.#main;
1529
+ const gateways = this.#gateways;
1530
+ this.#main = undefined;
1531
+ this.#gateways = undefined;
1532
+ await Promise.all([
1533
+ main?.stop(gateways === undefined && force),
1534
+ gateways?.stop(force)
1535
+ ]);
1536
+ }
1537
+ }
1538
+ Object.defineProperty(ServerBinding, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "protocols: BindingProtocols" }] });
1539
+
674
1540
  // src/server/settings.ts
675
1541
  var defaultSettings = () => ({ "trust proxy": false });
676
1542
 
677
1543
  // src/server/application.ts
678
- class HttpApplication {
1544
+ class HttpApplication extends ShutdownAware {
679
1545
  warnings;
680
1546
  #root;
681
1547
  closed;
@@ -692,14 +1558,15 @@ class HttpApplication {
692
1558
  #relayResubscribe;
693
1559
  #notFound;
694
1560
  #bootLogging;
1561
+ #binding;
1562
+ #split;
695
1563
  #globalPrefix = "";
696
1564
  #cors;
697
1565
  #started = false;
698
- #server;
699
1566
  #resolveClosed;
700
1567
  #shuttingDown;
701
- #hooks = new ShutdownHooks;
702
1568
  constructor(app, discovered, options, root, websocket) {
1569
+ super();
703
1570
  this.#app = app;
704
1571
  this.#root = root;
705
1572
  this.warnings = app.warnings;
@@ -717,6 +1584,12 @@ class HttpApplication {
717
1584
  this.#relayResubscribe = options.relayResubscribe;
718
1585
  this.#notFound = options.notFound ?? "public";
719
1586
  this.#bootLogging = options.bootLogging ?? true;
1587
+ this.#binding = new ServerBinding({
1588
+ http2: options.http2,
1589
+ http1: options.http1,
1590
+ gatewayPort: options.gatewayPort
1591
+ });
1592
+ this.#split = options.gatewayPort !== undefined && websocket !== undefined;
720
1593
  this.gatewayPaths = websocket?.paths ?? [];
721
1594
  this.closed = new Promise((resolve) => {
722
1595
  this.#resolveClosed = resolve;
@@ -775,23 +1648,17 @@ class HttpApplication {
775
1648
  const prefixed = this.#prefixed();
776
1649
  const routes = buildRoutes(prefixed, middleware, this.#onError, this.#cors, (guard, from) => from === undefined ? this.#app.get(guard) : this.#app.get(guard, from));
777
1650
  const ws = this.#websocket;
778
- if (ws)
1651
+ if (ws && !this.#split)
779
1652
  assertNoGatewayCollisions(prefixed, ws.paths);
780
1653
  const fetch = buildFallback(middleware, this.#onError, this.#cors, this.#notFound);
781
- const options = ws ? {
782
- port,
783
- fetch,
784
- routes: withUpgradeRoutes(routes, ws.routes),
785
- websocket: ws.websocket
786
- } : { port, fetch, routes };
787
- this.#server = Bun.serve(options);
1654
+ const bound = this.#binding.bind({ port, routes, fetch, websocket: ws });
788
1655
  attachAddressSource(this.#app.get(ClientAddress), {
789
- server: this.#server,
1656
+ server: bound.main,
790
1657
  trustProxy: this.#settings["trust proxy"]
791
1658
  });
792
- this.#app.get(RequestMetrics).attach(this.#server);
1659
+ this.#app.get(RequestMetrics).attach(bound.gauges);
793
1660
  const pubsub = this.#app.get(PubSub);
794
- pubsub.attach(this.#server);
1661
+ pubsub.attach(bound.sockets);
795
1662
  if (this.#relay) {
796
1663
  const logger = this.#app.get(Logger3);
797
1664
  await pubsub.relayThrough(this.#relay, {
@@ -807,7 +1674,10 @@ class HttpApplication {
807
1674
  });
808
1675
  }
809
1676
  this.#logServed(prefixed, ws);
810
- return this.#server.url.href;
1677
+ return bound.main.url.href;
1678
+ }
1679
+ get gatewayUrl() {
1680
+ return this.#binding.gatewayUrl;
811
1681
  }
812
1682
  #logServed(routes, ws) {
813
1683
  if (!this.#bootLogging)
@@ -819,6 +1689,7 @@ class HttpApplication {
819
1689
  ].join(" and ");
820
1690
  this.#app.get(Logger3).info(`Serving ${subject}`, {
821
1691
  ...runtimeInfo(),
1692
+ ...this.#binding.gatewayUrl === undefined ? {} : { gatewayUrl: this.#binding.gatewayUrl },
822
1693
  routes: routes.map((route) => `${route.method} ${route.path}`),
823
1694
  ...gateways.length === 0 ? {} : {
824
1695
  gateways: gateways.map((gateway) => ({
@@ -843,8 +1714,7 @@ class HttpApplication {
843
1714
  }
844
1715
  };
845
1716
  await step(() => this.#app.drain());
846
- await step(async () => this.#server?.stop(this.#websocket !== undefined));
847
- this.#server = undefined;
1717
+ await step(() => this.#binding.stop(this.#websocket !== undefined));
848
1718
  await step(() => this.#app.get(PubSub).close());
849
1719
  try {
850
1720
  await step(() => this.#app.shutdown());
@@ -856,25 +1726,21 @@ class HttpApplication {
856
1726
  })();
857
1727
  return this.#shuttingDown;
858
1728
  }
859
- enableShutdownHooks(signals = ["SIGTERM", "SIGINT"], options = {}) {
860
- this.#hooks.install(() => this.shutdown(), signals, options);
861
- return this;
862
- }
863
1729
  #prefixed() {
864
1730
  if (this.#globalPrefix === "")
865
1731
  return this.#discovered;
866
1732
  return this.#discovered.map((route) => ({
867
1733
  ...route,
868
- path: joinPath(this.#globalPrefix, route.path)
1734
+ path: joinPath2(this.#globalPrefix, route.path)
869
1735
  }));
870
1736
  }
871
1737
  #assertNotStarted(hook) {
872
1738
  if (!this.#started)
873
1739
  return;
874
- throw new AppError3(`${hook} must be called before listen(). The route table and the middleware ` + "chain are folded into one closure per route when the server binds, so " + "this call could not take effect.");
1740
+ throw new AppError7(`${hook} must be called before listen(). The route table and the middleware ` + "chain are folded into one closure per route when the server binds, so " + "this call could not take effect.");
875
1741
  }
876
1742
  }
877
- Object.defineProperty(HttpApplication, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "app: App", typeOnly: "App" }, { unresolved: "discovered: readonly DiscoveredRoute[]" }, { unresolved: "options: HttpOptions" }, { unresolved: "root: ModuleRef", typeOnly: "ModuleRef" }, { unresolved: "websocket?: WebSocketRuntime", typeOnly: "WebSocketRuntime" }] });
1743
+ Object.defineProperty(HttpApplication, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "app: App", typeOnly: "App" }, { unresolved: "discovered: readonly DiscoveredRoute[]" }, { unresolved: "options: HttpOptions", typeOnly: "HttpOptions" }, { unresolved: "root: ModuleRef", typeOnly: "ModuleRef" }, { unresolved: "websocket?: WebSocketRuntime", typeOnly: "WebSocketRuntime" }] });
878
1744
 
879
1745
  // src/server/options-provider.ts
880
1746
  class HttpOptionsProvider {
@@ -915,6 +1781,15 @@ class HttpOptionsProvider {
915
1781
  get relayResubscribe() {
916
1782
  return;
917
1783
  }
1784
+ get http2() {
1785
+ return;
1786
+ }
1787
+ get http1() {
1788
+ return;
1789
+ }
1790
+ get gatewayPort() {
1791
+ return;
1792
+ }
918
1793
  }
919
1794
 
920
1795
  class DefaultHttpOptions extends HttpOptionsProvider {
@@ -937,7 +1812,10 @@ function resolveHttpOptions(settings, given) {
937
1812
  onError: settings.onError,
938
1813
  websocket: settings.websocket,
939
1814
  relay: settings.relay,
940
- relayResubscribe: settings.relayResubscribe
1815
+ relayResubscribe: settings.relayResubscribe,
1816
+ http2: settings.http2,
1817
+ http1: settings.http1,
1818
+ gatewayPort: settings.gatewayPort
941
1819
  };
942
1820
  for (const key of Object.keys(given)) {
943
1821
  merged[key] = given[key];
@@ -991,9 +1869,9 @@ class HttpFactory {
991
1869
  for (const module of modules) {
992
1870
  const moduleMiddleware = module.options.middleware ?? [];
993
1871
  for (const controller of readControllers(module)) {
994
- const routes = discoverRoutes(app.get(controller, module.ref));
1872
+ const routes = discoverRoutes2(app.get(controller, module.ref));
995
1873
  if (routes.length === 0) {
996
- throw new AppError4(`${controller.name} is registered as a controller but declares no routes. ` + "Add a @Get/@Post/... method, or move it to providers.");
1874
+ throw new AppError8(`${controller.name} is registered as a controller but declares no routes. ` + "Add a @Get/@Post/... method, or move it to providers.");
997
1875
  }
998
1876
  discovered.push(...routes.map((route) => ({
999
1877
  ...route,
@@ -1010,6 +1888,15 @@ class HttpFactory {
1010
1888
  for (const warning of websocket?.warnings ?? []) {
1011
1889
  app.get(Logger4).warn(warning);
1012
1890
  }
1891
+ if (resolved.gatewayPort !== undefined && resolved.gatewayPort !== 0 && websocket === undefined) {
1892
+ app.get(Logger4).warn(`gatewayPort is set and no gateway is declared, so no second server ` + `binds and gatewayUrl stays undefined. Declare a @Gateway in a ` + `module's providers, or drop gatewayPort.`);
1893
+ }
1894
+ if (resolved.http1 === false && websocket !== undefined && resolved.gatewayPort === undefined) {
1895
+ await app.shutdown().catch(() => {
1896
+ return;
1897
+ });
1898
+ throw new AppError8(`http1: false refuses HTTP/1.x with a 505, and a websocket upgrade is an ` + `HTTP/1.1 request - so nothing could ever connect to ` + `${websocket.paths.join(", ")}. Set gatewayPort to serve the gateways ` + "from a second port that keeps HTTP/1.1, or drop http1: false.");
1899
+ }
1013
1900
  return new HttpApplication(app, discovered, resolved, root, websocket);
1014
1901
  }
1015
1902
  static #socketMiddleware(app, root, options) {
@@ -1020,6 +1907,27 @@ class HttpFactory {
1020
1907
  }
1021
1908
  // src/static/files.ts
1022
1909
  import { join, normalize, resolve } from "path";
1910
+
1911
+ // src/static/options.ts
1912
+ class StaticOptions {
1913
+ root;
1914
+ path;
1915
+ maxAge;
1916
+ immutable;
1917
+ constructor(init) {
1918
+ this.root = init.root;
1919
+ this.path = normalizePrefix(init.path ?? "/");
1920
+ this.maxAge = init.maxAge ?? 60;
1921
+ this.immutable = init.immutable ?? (() => false);
1922
+ }
1923
+ }
1924
+ Object.defineProperty(StaticOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: StaticOptionsInit" }] });
1925
+ var normalizePrefix = (path) => {
1926
+ const trimmed = path.split("/").filter(Boolean).join("/");
1927
+ return trimmed === "" ? "/" : `/${trimmed}`;
1928
+ };
1929
+
1930
+ // src/static/files.ts
1023
1931
  class StaticFiles {
1024
1932
  #options;
1025
1933
  #root;
@@ -1116,6 +2024,99 @@ StaticModule = __decorateElement(_init, 0, "StaticModule", _dec, StaticModule);
1116
2024
  __runInitializers(_init, 1, StaticModule);
1117
2025
  __decoratorMetadata(_init, StaticModule);
1118
2026
  let _StaticModule = StaticModule;
2027
+ // src/compression/negotiate.ts
2028
+ var quality = (params) => {
2029
+ for (const param of params) {
2030
+ const [key, value] = param.split("=");
2031
+ if (key?.trim().toLowerCase() !== "q")
2032
+ continue;
2033
+ const q = Number.parseFloat(value ?? "");
2034
+ return Number.isFinite(q) && q >= 0 && q <= 1 ? q : 1;
2035
+ }
2036
+ return 1;
2037
+ };
2038
+ var negotiate = (header, offered) => {
2039
+ if (header === null)
2040
+ return;
2041
+ const accepted = new Map;
2042
+ for (const element of header.split(",")) {
2043
+ const [name, ...params] = element.split(";");
2044
+ const token = name?.trim().toLowerCase();
2045
+ if (token === undefined || token === "")
2046
+ continue;
2047
+ accepted.set(token, quality(params));
2048
+ }
2049
+ const wildcard = accepted.get("*");
2050
+ let best;
2051
+ let bestQuality = 0;
2052
+ for (const encoding of offered) {
2053
+ const q = accepted.get(encoding) ?? wildcard ?? 0;
2054
+ if (q > bestQuality) {
2055
+ best = encoding;
2056
+ bestQuality = q;
2057
+ }
2058
+ }
2059
+ return best;
2060
+ };
2061
+
2062
+ // src/compression/options.ts
2063
+ var CompressionEncoding = Object.freeze({
2064
+ ZSTD: "zstd",
2065
+ GZIP: "gzip"
2066
+ });
2067
+ var COMPRESSIBLE = new Set([
2068
+ "application/graphql",
2069
+ "application/graphql-response+json",
2070
+ "application/javascript",
2071
+ "application/json",
2072
+ "application/manifest+json",
2073
+ "application/wasm",
2074
+ "application/x-javascript",
2075
+ "application/x-ndjson",
2076
+ "application/xml",
2077
+ "image/svg+xml"
2078
+ ]);
2079
+ var isCompressibleType = (contentType) => {
2080
+ if (contentType === null)
2081
+ return false;
2082
+ const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
2083
+ if (type.startsWith("text/"))
2084
+ return true;
2085
+ if (type.endsWith("+json") || type.endsWith("+xml"))
2086
+ return true;
2087
+ return COMPRESSIBLE.has(type);
2088
+ };
2089
+ var encodable = (encoding) => {
2090
+ const sync = encoding === CompressionEncoding.ZSTD ? Bun.zstdCompressSync : Bun.gzipSync;
2091
+ if (typeof sync !== "function")
2092
+ return false;
2093
+ try {
2094
+ new CompressionStream(encoding);
2095
+ return true;
2096
+ } catch {
2097
+ return false;
2098
+ }
2099
+ };
2100
+
2101
+ class CompressionOptions {
2102
+ encodings;
2103
+ threshold;
2104
+ filter;
2105
+ constructor(init = {}) {
2106
+ this.encodings = init.encodings ?? [
2107
+ CompressionEncoding.ZSTD,
2108
+ CompressionEncoding.GZIP
2109
+ ];
2110
+ const missing = this.encodings.filter((encoding) => !encodable(encoding));
2111
+ if (missing.length > 0) {
2112
+ throw new Error(`Bun ${Bun.version} cannot encode ${missing.join(", ")}. ` + "Pass `encodings` without it, or upgrade Bun.");
2113
+ }
2114
+ this.threshold = init.threshold ?? 1024;
2115
+ this.filter = init.filter ?? isCompressibleType;
2116
+ }
2117
+ }
2118
+ Object.defineProperty(CompressionOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: CompressionOptionsInit = {}", optional: true }] });
2119
+
1119
2120
  // src/compression/compression.ts
1120
2121
  var BODYLESS = new Set([204, 205, 304]);
1121
2122
  var BUFFER_LIMIT = 1024 * 1024;
@@ -1288,15 +2289,15 @@ __runInitializers(_init, 1, CompressionModule);
1288
2289
  __decoratorMetadata(_init, CompressionModule);
1289
2290
  let _CompressionModule = CompressionModule;
1290
2291
  // src/throttle/decorators.ts
1291
- var THROTTLE = metaKey("throttle");
1292
- var SKIP_THROTTLE = metaKey("skip-throttle");
1293
- var Throttle = (limit) => meta(THROTTLE, limit);
1294
- var SkipThrottle = () => meta(SKIP_THROTTLE, true);
2292
+ var THROTTLE = metaKey2("throttle");
2293
+ var SKIP_THROTTLE = metaKey2("skip-throttle");
2294
+ var Throttle = (limit) => meta2(THROTTLE, limit);
2295
+ var SkipThrottle = () => meta2(SKIP_THROTTLE, true);
1295
2296
  // src/throttle/guard.ts
1296
2297
  import { Logger as Logger5 } from "@dunx/core";
1297
2298
 
1298
2299
  // src/throttle/options.ts
1299
- import { AppError as AppError5 } from "@dunx/core";
2300
+ import { AppError as AppError9 } from "@dunx/core";
1300
2301
 
1301
2302
  class ThrottleOptions {
1302
2303
  limit;
@@ -1307,13 +2308,13 @@ class ThrottleOptions {
1307
2308
  store;
1308
2309
  constructor(init) {
1309
2310
  if (init.prefix.trim() === "") {
1310
- throw new AppError5("ThrottleModule needs a prefix naming this application, and it has no " + "default: two apps sharing one Redis with one throttle namespace each " + "spend the other's budget. Pass something like { prefix: 'orders-api' }.");
2311
+ throw new AppError9("ThrottleModule needs a prefix naming this application, and it has no " + "default: two apps sharing one Redis with one throttle namespace each " + "spend the other's budget. Pass something like { prefix: 'orders-api' }.");
1311
2312
  }
1312
2313
  if (!Number.isInteger(init.limit) || init.limit < 1) {
1313
- throw new AppError5(`ThrottleModule needs a limit of at least 1; got ${init.limit}.`);
2314
+ throw new AppError9(`ThrottleModule needs a limit of at least 1; got ${init.limit}.`);
1314
2315
  }
1315
2316
  if (!Number.isInteger(init.windowSeconds) || init.windowSeconds < 1) {
1316
- throw new AppError5("ThrottleModule needs a windowSeconds of at least 1; got " + `${init.windowSeconds}.`);
2317
+ throw new AppError9("ThrottleModule needs a windowSeconds of at least 1; got " + `${init.windowSeconds}.`);
1317
2318
  }
1318
2319
  this.limit = init.limit;
1319
2320
  this.windowSeconds = init.windowSeconds;
@@ -1326,12 +2327,12 @@ class ThrottleOptions {
1326
2327
  Object.defineProperty(ThrottleOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: ThrottleOptionsInit" }] });
1327
2328
 
1328
2329
  // src/throttle/store.ts
1329
- import { AppError as AppError6 } from "@dunx/core";
2330
+ import { AppError as AppError10 } from "@dunx/core";
1330
2331
 
1331
2332
  class ThrottleStore {
1332
2333
  constructor() {
1333
2334
  if (new.target === ThrottleStore) {
1334
- throw new AppError6("ThrottleStore is a contract, not an implementation. Bind one with " + "ThrottleModule.forRoot({ store: new RedisThrottleStore(redis) }), or " + "leave it out for the in-process MemoryThrottleStore.");
2335
+ throw new AppError10("ThrottleStore is a contract, not an implementation. Bind one with " + "ThrottleModule.forRoot({ store: new RedisThrottleStore(redis) }), or " + "leave it out for the in-process MemoryThrottleStore.");
1335
2336
  }
1336
2337
  }
1337
2338
  }
@@ -1406,7 +2407,7 @@ class ThrottleGuard {
1406
2407
  this.logger = logger;
1407
2408
  }
1408
2409
  async handle(req, ctx, next) {
1409
- if (ctx.get(UNMATCHED) === true)
2410
+ if (ctx.get(UNMATCHED2) === true)
1410
2411
  return next();
1411
2412
  if (ctx.get(SKIP_THROTTLE) === true)
1412
2413
  return next();
@@ -1417,7 +2418,7 @@ class ThrottleGuard {
1417
2418
  return next();
1418
2419
  if (used > limit.limit) {
1419
2420
  const after = await this.#ttl(key) ?? limit.windowSeconds;
1420
- throw new HttpError(HttpStatusCode.TOO_MANY_REQUESTS, `Rate limit exceeded: ${limit.limit} requests per ` + `${limit.windowSeconds}s`, this.options.headers ? {
2421
+ throw new HttpError(HttpStatusCode2.TOO_MANY_REQUESTS, `Rate limit exceeded: ${limit.limit} requests per ` + `${limit.windowSeconds}s`, this.options.headers ? {
1421
2422
  headers: {
1422
2423
  "retry-after": String(after),
1423
2424
  "ratelimit-limit": String(limit.limit),
@@ -1535,21 +2536,8 @@ var OnMessage = (event) => (value) => {
1535
2536
  return value;
1536
2537
  };
1537
2538
  // src/ws/postgres-relay.ts
1538
- import { AppError as AppError7 } from "@dunx/core";
1539
2539
  var PROTOCOLS = ["postgres:", "postgresql:"];
1540
2540
  var defaultPostgresRelayUrl = () => process.env["POSTGRES_URL"] ?? process.env["DATABASE_URL"] ?? "postgres://localhost:5432";
1541
- var assertUrl = (url) => {
1542
- let parsed;
1543
- try {
1544
- parsed = new URL(url);
1545
- } catch {
1546
- throw new AppError7(`${JSON.stringify(url)} is not a valid URL for the websocket relay. ` + "Expected something like postgres://localhost:5432/app.");
1547
- }
1548
- if (!PROTOCOLS.includes(parsed.protocol)) {
1549
- throw new AppError7(`Unsupported protocol ${JSON.stringify(parsed.protocol)} in ` + `${JSON.stringify(url)}. Expected one of ${PROTOCOLS.join(", ")}.`);
1550
- }
1551
- return url;
1552
- };
1553
2541
 
1554
2542
  class PostgresRelay extends WsRelay {
1555
2543
  #url;
@@ -1558,14 +2546,11 @@ class PostgresRelay extends WsRelay {
1558
2546
  #subscription;
1559
2547
  constructor(options = {}) {
1560
2548
  super();
1561
- this.#url = assertUrl(options.url ?? defaultPostgresRelayUrl());
2549
+ this.#url = assertRelayUrl(options.url ?? defaultPostgresRelayUrl(), PROTOCOLS, "postgres://localhost:5432/app");
1562
2550
  this.#max = options.max ?? 1;
1563
2551
  }
1564
2552
  get url() {
1565
- const parsed = new URL(this.#url);
1566
- if (parsed.password)
1567
- parsed.password = "***";
1568
- return parsed.toString();
2553
+ return redactUrl(this.#url);
1569
2554
  }
1570
2555
  #client() {
1571
2556
  return this.#sql ??= new Bun.SQL({ url: this.#url, max: this.#max });
@@ -1599,6 +2584,82 @@ class PostgresRelay extends WsRelay {
1599
2584
  }
1600
2585
  }
1601
2586
  Object.defineProperty(PostgresRelay, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "options: PostgresRelayOptions = {}", optional: true }] });
2587
+ // src/ws/redis-relay.ts
2588
+ var PROTOCOLS2 = [
2589
+ "redis:",
2590
+ "rediss:",
2591
+ "valkey:",
2592
+ "valkeys:",
2593
+ "redis+tls:",
2594
+ "redis+unix:",
2595
+ "redis+tls+unix:"
2596
+ ];
2597
+ var defaultRelayUrl = () => process.env["VALKEY_URL"] ?? process.env["REDIS_URL"] ?? "redis://localhost:6379";
2598
+
2599
+ class RedisRelay extends WsRelay {
2600
+ #url;
2601
+ #options;
2602
+ #pub;
2603
+ #sub;
2604
+ #channel;
2605
+ constructor(options = {}) {
2606
+ super();
2607
+ this.#url = assertRelayUrl(options.url ?? defaultRelayUrl(), PROTOCOLS2, "redis://localhost:6379");
2608
+ this.#options = {
2609
+ maxRetries: options.maxRetries ?? 0,
2610
+ ...options.connectionTimeout !== undefined && {
2611
+ connectionTimeout: options.connectionTimeout
2612
+ },
2613
+ ...options.tls !== undefined && { tls: options.tls }
2614
+ };
2615
+ }
2616
+ get url() {
2617
+ return redactUrl(this.#url);
2618
+ }
2619
+ async publish(channel, message) {
2620
+ const client = this.#pub ??= new Bun.RedisClient(this.#url, this.#options);
2621
+ try {
2622
+ return await client.publish(channel, message);
2623
+ } catch (error) {
2624
+ if (this.#pub === client) {
2625
+ this.#pub = undefined;
2626
+ client.close();
2627
+ }
2628
+ throw error;
2629
+ }
2630
+ }
2631
+ async subscribe(channel, listener) {
2632
+ const client = this.#sub ??= new Bun.RedisClient(this.#url, this.#options);
2633
+ try {
2634
+ await client.connect();
2635
+ await client.subscribe(channel, listener);
2636
+ this.#channel = channel;
2637
+ } catch (error) {
2638
+ if (this.#sub === client) {
2639
+ this.#sub = undefined;
2640
+ client.close();
2641
+ }
2642
+ throw error;
2643
+ }
2644
+ }
2645
+ async close() {
2646
+ const sub = this.#sub;
2647
+ const channel = this.#channel;
2648
+ this.#pub?.close();
2649
+ this.#pub = undefined;
2650
+ this.#sub = undefined;
2651
+ this.#channel = undefined;
2652
+ if (!sub)
2653
+ return;
2654
+ if (channel !== undefined) {
2655
+ try {
2656
+ await sub.unsubscribe(channel);
2657
+ } catch {}
2658
+ }
2659
+ sub.close();
2660
+ }
2661
+ }
2662
+ Object.defineProperty(RedisRelay, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "options: RedisRelayOptions = {}", optional: true }] });
1602
2663
  // src/ws/relay-module.ts
1603
2664
  import {
1604
2665
  provide as provide5
@@ -1740,6 +2801,201 @@ class PingProbe {
1740
2801
 
1741
2802
  class QueryProbe {
1742
2803
  }
2804
+ // src/health/controller.ts
2805
+ import { inject } from "@dunx/core";
2806
+
2807
+ // src/health/report-schema.ts
2808
+ var state = {
2809
+ type: "string",
2810
+ enum: ["up", "down", "unknown"],
2811
+ description: "`unknown` is not `down`: a probe that timed out has told you nothing."
2812
+ };
2813
+ var HEALTH_REPORT_SCHEMA = Object.freeze({
2814
+ $id: "HealthReport",
2815
+ type: "object",
2816
+ description: "What the probe found. `up` answers 200 and anything else answers 503.",
2817
+ properties: {
2818
+ status: state,
2819
+ draining: {
2820
+ type: "boolean",
2821
+ description: "The process is shutting down, or something holds it out."
2822
+ },
2823
+ uptimeMs: {
2824
+ type: "integer",
2825
+ description: "Measured on a monotonic clock, so it never goes backwards."
2826
+ },
2827
+ checks: {
2828
+ type: "array",
2829
+ items: {
2830
+ type: "object",
2831
+ properties: {
2832
+ name: { type: "string" },
2833
+ state,
2834
+ critical: {
2835
+ type: "boolean",
2836
+ description: "A failure here sheds traffic. Memory and disk do not."
2837
+ },
2838
+ ms: { type: "integer", description: "How long the check took." },
2839
+ detail: {
2840
+ type: "string",
2841
+ description: "A latency, a version, or a failure message."
2842
+ }
2843
+ },
2844
+ required: ["name", "state", "critical", "ms"]
2845
+ }
2846
+ }
2847
+ },
2848
+ required: ["status", "draining", "uptimeMs", "checks"]
2849
+ });
2850
+
2851
+ // src/health/registry.ts
2852
+ var bounded = async (indicator, timeoutMs) => {
2853
+ let timer;
2854
+ const timeout = new Promise((resolve) => {
2855
+ timer = setTimeout(() => resolve({ state: "unknown", detail: `no answer in ${timeoutMs} ms` }), timeoutMs);
2856
+ timer.unref?.();
2857
+ });
2858
+ try {
2859
+ return await Promise.race([
2860
+ Promise.resolve().then(() => indicator.check()).catch((error) => ({
2861
+ state: "down",
2862
+ detail: error instanceof Error ? error.message : String(error)
2863
+ })),
2864
+ timeout
2865
+ ]);
2866
+ } finally {
2867
+ if (timer)
2868
+ clearTimeout(timer);
2869
+ }
2870
+ };
2871
+ var worst = (checks) => {
2872
+ const critical = checks.filter((check) => check.critical);
2873
+ if (critical.some((check) => check.state === "down"))
2874
+ return "down";
2875
+ if (critical.some((check) => check.state === "unknown"))
2876
+ return "unknown";
2877
+ return "up";
2878
+ };
2879
+
2880
+ class HealthOptions {
2881
+ liveness;
2882
+ readiness;
2883
+ timeoutMs;
2884
+ routes;
2885
+ documented;
2886
+ drainDelayMs;
2887
+ constructor(init = {}) {
2888
+ this.liveness = init.liveness ?? [];
2889
+ this.readiness = init.readiness ?? [];
2890
+ this.timeoutMs = init.timeoutMs ?? 2000;
2891
+ this.routes = init.routes ?? true;
2892
+ this.documented = init.documented ?? true;
2893
+ this.drainDelayMs = Math.max(0, init.drainDelayMs ?? 0);
2894
+ }
2895
+ }
2896
+ Object.defineProperty(HealthOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: HealthOptionsInit = {}", optional: true }] });
2897
+
2898
+ class HealthRegistry {
2899
+ options;
2900
+ readiness_;
2901
+ #startedAt = performance.now();
2902
+ constructor(options, readiness_) {
2903
+ this.options = options;
2904
+ this.readiness_ = readiness_;
2905
+ }
2906
+ async report(indicators) {
2907
+ const checks = await Promise.all(indicators.map(async (indicator) => {
2908
+ const started = performance.now();
2909
+ const result = await bounded(indicator, this.options.timeoutMs);
2910
+ return {
2911
+ name: indicator.name,
2912
+ state: result.state,
2913
+ critical: indicator.critical,
2914
+ ms: Math.round(performance.now() - started),
2915
+ ...result.detail === undefined ? {} : { detail: result.detail }
2916
+ };
2917
+ }));
2918
+ return {
2919
+ status: worst(checks),
2920
+ draining: this.readiness_.draining,
2921
+ uptimeMs: Math.round(performance.now() - this.#startedAt),
2922
+ checks
2923
+ };
2924
+ }
2925
+ liveness() {
2926
+ return this.report(this.options.liveness);
2927
+ }
2928
+ async readiness() {
2929
+ const report = await this.report(this.options.readiness);
2930
+ if (!this.readiness_.draining)
2931
+ return report;
2932
+ return {
2933
+ ...report,
2934
+ status: "down",
2935
+ checks: [
2936
+ {
2937
+ name: "readiness",
2938
+ state: "down",
2939
+ critical: true,
2940
+ ms: 0,
2941
+ detail: this.readiness_.reason ?? "not accepting traffic"
2942
+ },
2943
+ ...report.checks
2944
+ ]
2945
+ };
2946
+ }
2947
+ }
2948
+ Object.defineProperty(HealthRegistry, Symbol.for("dunx.deps"), { value: () => [HealthOptions, { unresolved: "private readonly readiness_: Readiness", typeOnly: "Readiness" }] });
2949
+
2950
+ // src/health/controller.ts
2951
+ var probeResponses = {
2952
+ response: { 200: HEALTH_REPORT_SCHEMA, 503: HEALTH_REPORT_SCHEMA }
2953
+ };
2954
+ var answer = (report) => Response.json(report, { status: report.status === "up" ? 200 : 503 });
2955
+ var _dec = [
2956
+ Controller("health")
2957
+ ];
2958
+ var _dec2 = [
2959
+ Public2(),
2960
+ Get("/live", probeResponses)
2961
+ ];
2962
+ var _dec3 = [
2963
+ Public2(),
2964
+ Get("/ready", probeResponses)
2965
+ ];
2966
+ var _health = new WeakMap;
2967
+ var _init = __decoratorStart(undefined);
2968
+
2969
+ class HealthController {
2970
+ constructor() {
2971
+ __privateAdd(this, _health, inject(HealthRegistry));
2972
+ __runInitializers(_init, 5, this);
2973
+ }
2974
+ async live() {
2975
+ return answer(await __privateGet(this, _health).liveness());
2976
+ }
2977
+ async ready() {
2978
+ return answer(await __privateGet(this, _health).readiness());
2979
+ }
2980
+ }
2981
+ __decorateElement(_init, 1, "live", _dec2, HealthController);
2982
+ __decorateElement(_init, 1, "ready", _dec3, HealthController);
2983
+ HealthController = __decorateElement(_init, 0, "HealthController", _dec, HealthController);
2984
+ __runInitializers(_init, 1, HealthController);
2985
+ __decoratorMetadata(_init, HealthController);
2986
+ let _HealthController = HealthController;
2987
+ var _dec = [
2988
+ ApiHidden2()
2989
+ ];
2990
+ var _base = HealthController;
2991
+ var _init = __decoratorStart(_base);
2992
+
2993
+ class HiddenHealthController extends _base {
2994
+ }
2995
+ HiddenHealthController = __decorateElement(_init, 0, "HiddenHealthController", _dec, HiddenHealthController);
2996
+ __runInitializers(_init, 1, HiddenHealthController);
2997
+ __decoratorMetadata(_init, HiddenHealthController);
2998
+ let _HiddenHealthController = HiddenHealthController;
1743
2999
  // src/health/indicators.ts
1744
3000
  import { statfs } from "fs/promises";
1745
3001
  var ms = (started) => Math.round(performance.now() - started);
@@ -1926,7 +3182,7 @@ __runInitializers(_init, 1, HealthModule);
1926
3182
  __decoratorMetadata(_init, HealthModule);
1927
3183
  let _HealthModule = HealthModule;
1928
3184
  export {
1929
- ApiHidden,
3185
+ ApiHidden2 as ApiHidden,
1930
3186
  ClientAddress,
1931
3187
  Compression,
1932
3188
  CompressionEncoding,
@@ -1943,7 +3199,7 @@ export {
1943
3199
  Gateway,
1944
3200
  Get,
1945
3201
  HEALTH_REPORT_SCHEMA,
1946
- HIDDEN,
3202
+ HIDDEN2 as HIDDEN,
1947
3203
  HealthController,
1948
3204
  HealthIndicator,
1949
3205
  HealthModule,
@@ -1952,7 +3208,7 @@ export {
1952
3208
  HttpError,
1953
3209
  HttpFactory,
1954
3210
  HttpOptionsProvider,
1955
- HttpStatusCode,
3211
+ HttpStatusCode2 as HttpStatusCode,
1956
3212
  MemoryIndicator,
1957
3213
  MemoryOptions,
1958
3214
  MemoryThrottleStore,
@@ -1964,17 +3220,17 @@ export {
1964
3220
  OnPing,
1965
3221
  OnPong,
1966
3222
  OnUpgrade,
1967
- PUBLIC,
3223
+ PUBLIC2 as PUBLIC,
1968
3224
  Patch,
1969
3225
  PingProbe,
1970
3226
  Post,
1971
3227
  PostgresRelay,
1972
3228
  PostgresRelayConnectionOptions,
1973
3229
  PubSub,
1974
- Public,
3230
+ Public2 as Public,
1975
3231
  Put,
1976
3232
  QueryProbe,
1977
- ROLES,
3233
+ ROLES2 as ROLES,
1978
3234
  Readiness,
1979
3235
  ReadinessOptions,
1980
3236
  RedisIndicator,
@@ -1983,7 +3239,7 @@ export {
1983
3239
  RelayConnectionOptions,
1984
3240
  RequestLoggingMiddleware,
1985
3241
  RequestMetrics,
1986
- Roles,
3242
+ Roles2 as Roles,
1987
3243
  SKIP_THROTTLE,
1988
3244
  SkipThrottle,
1989
3245
  SocketLoggingMiddleware,
@@ -1991,25 +3247,25 @@ export {
1991
3247
  StaticModule,
1992
3248
  StaticOptions,
1993
3249
  THROTTLE,
1994
- TRACEPARENT_HEADER,
1995
- TRACERESPONSE_HEADER,
1996
- TRACESTATE_HEADER,
3250
+ TRACEPARENT_HEADER2 as TRACEPARENT_HEADER,
3251
+ TRACERESPONSE_HEADER2 as TRACERESPONSE_HEADER,
3252
+ TRACESTATE_HEADER2 as TRACESTATE_HEADER,
1997
3253
  Throttle,
1998
3254
  ThrottleGuard,
1999
3255
  ThrottleModule,
2000
3256
  ThrottleOptions,
2001
3257
  ThrottleStore,
2002
- TraceContext,
2003
- UNMATCHED,
3258
+ TraceContext2 as TraceContext,
3259
+ UNMATCHED2 as UNMATCHED,
2004
3260
  UNMATCHED_ROUTE,
2005
- UseGuards,
3261
+ UseGuards2 as UseGuards,
2006
3262
  ValidationError,
2007
3263
  WsRelay,
2008
3264
  WsRelayModule,
2009
3265
  defaultErrorMapper,
2010
3266
  errorMapper,
2011
- mergeMeta,
2012
- meta,
2013
- metaKey,
2014
- metaOf
3267
+ mergeMeta2 as mergeMeta,
3268
+ meta2 as meta,
3269
+ metaKey2 as metaKey,
3270
+ metaOf2 as metaOf
2015
3271
  };