@dunx/http 3.2.0 → 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/index.js CHANGED
@@ -1,71 +1,58 @@
1
1
  // @bun
2
+ import {
3
+ TRACEPARENT_HEADER,
4
+ TRACERESPONSE_HEADER,
5
+ TRACESTATE_HEADER,
6
+ TraceContext
7
+ } from "./chunk-3j2n1n11.js";
2
8
  import {
3
9
  ApiHidden,
4
- CompressionEncoding,
5
- CompressionOptions,
6
- Controller,
7
- DEFAULT_RELAY_CHANNEL,
8
- Delete,
9
- ErrorFilter,
10
- Get,
11
- HEALTH_REPORT_SCHEMA,
12
10
  HIDDEN,
13
11
  HandlerKind,
14
- HealthController,
15
- HealthOptions,
16
- HealthRegistry,
17
- HiddenHealthController,
18
- HttpError,
19
12
  PUBLIC,
20
- Patch,
21
- Post,
22
13
  Public,
23
- Put,
24
14
  ROLES,
25
- RawBody,
26
- RedisRelay,
27
15
  Roles,
28
- StaticOptions,
29
16
  UNMATCHED,
30
17
  UseGuards,
31
- ValidationError,
32
- WsRelay,
33
- assertNoCollisions,
34
- assertNoGatewayCollisions,
35
- buildFallback,
36
- buildRoutes,
37
- buildWebSocket,
38
- decodeRelay,
39
- defaultErrorMapper,
40
- defaultRelayError,
18
+ buildContext,
19
+ defaultStatusFor,
41
20
  discoverGateways,
42
21
  discoverRoutes,
43
- encode,
44
- encodeRelay,
45
- errorMapper,
46
22
  joinPath,
23
+ markController,
47
24
  markGateway,
48
25
  markHandler,
26
+ markRoute,
49
27
  mergeMeta,
50
28
  meta,
51
29
  metaKey,
52
- metaOf,
53
- negotiate,
54
- observe,
55
- toErrorMapper,
56
- withUpgradeRoutes
57
- } from "./chunk-y85wcdhw.js";
30
+ metaOf
31
+ } from "./chunk-3nbj06q8.js";
58
32
  import {
59
33
  HttpStatusCode,
60
- TRACEPARENT_HEADER,
61
- TRACERESPONSE_HEADER,
62
- TRACESTATE_HEADER,
63
- TraceContext,
64
34
  __decorateElement,
65
35
  __decoratorMetadata,
66
36
  __decoratorStart,
37
+ __privateAdd,
38
+ __privateGet,
67
39
  __runInitializers
68
- } from "./chunk-9x3evk19.js";
40
+ } from "./chunk-sz4pvqxy.js";
41
+
42
+ // src/route/decorators.ts
43
+ var Controller = (prefix = "") => (target) => {
44
+ markController(target, prefix);
45
+ return target;
46
+ };
47
+ var verb = (method) => (path = "/", options) => (value, _context) => {
48
+ markRoute(value, { method, path, options });
49
+ return value;
50
+ };
51
+ var Get = verb("GET");
52
+ var Post = verb("POST");
53
+ var Put = verb("PUT");
54
+ var Patch = verb("PATCH");
55
+ var Delete = verb("DELETE");
69
56
  // src/server/client-address.ts
70
57
  import { AppError } from "@dunx/core";
71
58
  var trustedHops = (setting) => {
@@ -98,10 +85,67 @@ class ClientAddress {
98
85
  var attachAddressSource = (target, source) => {
99
86
  sources.set(target, source);
100
87
  };
88
+ // src/server/errors.ts
89
+ import { AppError as AppError2, ConsoleLogger } from "@dunx/core";
90
+ class HttpError extends AppError2 {
91
+ status;
92
+ name = "HttpError";
93
+ headers;
94
+ constructor(status, message, options) {
95
+ super(message, options);
96
+ this.status = status;
97
+ this.headers = options?.headers;
98
+ }
99
+ }
100
+ Object.defineProperty(HttpError, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "override readonly status: number" }, { unresolved: "message: string" }, { unresolved: "options?: HttpErrorOptions" }] });
101
+
102
+ class ValidationError extends HttpError {
103
+ source;
104
+ issues;
105
+ name = "ValidationError";
106
+ constructor(source, issues) {
107
+ super(HttpStatusCode.BAD_REQUEST, `Invalid ${source}`);
108
+ this.source = source;
109
+ this.issues = issues;
110
+ }
111
+ }
112
+ Object.defineProperty(ValidationError, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "readonly source: InputSource" }, { unresolved: "readonly issues: readonly ValidationIssue[]" }] });
113
+
114
+ class ErrorFilter {
115
+ }
116
+ var isErrorFilter = (handler) => typeof handler === "function" && typeof handler.prototype?.catch === "function";
117
+ var toErrorMapper = (handler, resolve) => isErrorFilter(handler) ? (error, req) => resolve(handler).catch(error, req) : handler;
118
+ var errorMapper = (logger) => (error) => {
119
+ if (error instanceof ValidationError) {
120
+ return Response.json({ error: error.message, status: error.status, issues: error.issues }, {
121
+ status: error.status,
122
+ ...error.headers && { headers: error.headers }
123
+ });
124
+ }
125
+ if (error instanceof HttpError) {
126
+ return Response.json({ error: error.message, status: error.status }, {
127
+ status: error.status,
128
+ ...error.headers && { headers: error.headers }
129
+ });
130
+ }
131
+ if (error instanceof AppError2 && isStatus(error.status)) {
132
+ if (error.status >= HttpStatusCode.INTERNAL_SERVER_ERROR) {
133
+ logger.error("Unhandled error", error);
134
+ }
135
+ return Response.json({ error: error.message, status: error.status }, { status: error.status });
136
+ }
137
+ logger.error("Unhandled error", error);
138
+ return Response.json({
139
+ error: "Internal Server Error",
140
+ status: HttpStatusCode.INTERNAL_SERVER_ERROR
141
+ }, { status: HttpStatusCode.INTERNAL_SERVER_ERROR });
142
+ };
143
+ var isStatus = (value) => value !== undefined && Number.isInteger(value) && value >= 200 && value <= 599;
144
+ var defaultErrorMapper = errorMapper(new ConsoleLogger);
101
145
  // src/server/factory.ts
102
146
  import {
103
147
  collectModules,
104
- AppError as AppError4,
148
+ AppError as AppError8,
105
149
  AppFactory,
106
150
  Logger as Logger4,
107
151
  provide,
@@ -201,6 +245,303 @@ class MetricsMiddleware {
201
245
  }
202
246
  Object.defineProperty(MetricsMiddleware, Symbol.for("dunx.deps"), { value: () => [RequestMetrics] });
203
247
 
248
+ // src/ws/envelope.ts
249
+ var encode = (event, data) => JSON.stringify({ event, data });
250
+ var decode = (message) => {
251
+ if (typeof message !== "string")
252
+ return;
253
+ let parsed;
254
+ try {
255
+ parsed = JSON.parse(message);
256
+ } catch {
257
+ return;
258
+ }
259
+ if (typeof parsed !== "object" || parsed === null)
260
+ return;
261
+ const { event, data } = parsed;
262
+ return typeof event === "string" ? { event, data } : undefined;
263
+ };
264
+
265
+ // src/ws/middleware.ts
266
+ var composeSocket = (middleware, ctx) => middleware.reduceRight((next, current) => (frame, run) => current.handle(frame, ctx, () => next(frame, run)), (_frame, run) => run());
267
+ var observe = (next, done) => {
268
+ let result;
269
+ try {
270
+ result = next();
271
+ } catch (error) {
272
+ done(error, undefined);
273
+ throw error;
274
+ }
275
+ if (result instanceof Promise) {
276
+ return result.then((value) => {
277
+ done(undefined, value);
278
+ return value;
279
+ }, (error) => {
280
+ done(error, undefined);
281
+ throw error;
282
+ });
283
+ }
284
+ done(undefined, result);
285
+ return result;
286
+ };
287
+
288
+ // src/ws/runtime.ts
289
+ import { AppError as AppError3 } from "@dunx/core";
290
+ var slotOf = (handler) => handler.kind === HandlerKind.MESSAGE && handler.event !== undefined ? `message ${JSON.stringify(handler.event)}` : handler.kind;
291
+ var buildRuntime = (gateway) => {
292
+ if (gateway.handlers.length === 0) {
293
+ throw new AppError3(`${gateway.name} is registered as a gateway but declares no handlers. ` + "Add an @OnMessage/@OnOpen/... method, or drop the @Gateway decorator.");
294
+ }
295
+ const owners = new Map;
296
+ const events = new Map;
297
+ for (const handler of gateway.handlers) {
298
+ const slot = slotOf(handler);
299
+ const existing = owners.get(slot);
300
+ if (existing) {
301
+ throw new AppError3(`Handler collision in ${gateway.name}: ${slot} is claimed by ` + `${existing.method}() and by ${handler.method}(). One handler per event.`);
302
+ }
303
+ owners.set(slot, handler);
304
+ if (handler.kind === HandlerKind.MESSAGE && handler.event !== undefined) {
305
+ events.set(handler.event, handler.invoke);
306
+ }
307
+ }
308
+ const at = (slot) => owners.get(slot)?.invoke;
309
+ return {
310
+ name: gateway.name,
311
+ path: gateway.path,
312
+ upgrade: at(HandlerKind.UPGRADE),
313
+ open: at(HandlerKind.OPEN),
314
+ close: at(HandlerKind.CLOSE),
315
+ drain: at(HandlerKind.DRAIN),
316
+ ping: at(HandlerKind.PING),
317
+ pong: at(HandlerKind.PONG),
318
+ raw: at(HandlerKind.MESSAGE),
319
+ events
320
+ };
321
+ };
322
+ var buildGateways = (discovered) => {
323
+ const byPath = new Map;
324
+ for (const gateway of discovered) {
325
+ const existing = byPath.get(gateway.path);
326
+ if (existing) {
327
+ throw new AppError3(`Gateway path collision: ${gateway.path} is served by ${existing.name} ` + `and by ${gateway.name}. One gateway per path.`);
328
+ }
329
+ byPath.set(gateway.path, buildRuntime(gateway));
330
+ }
331
+ return byPath;
332
+ };
333
+ var someHandler = (gateways, pick) => {
334
+ for (const gateway of gateways)
335
+ if (pick(gateway) !== undefined)
336
+ return true;
337
+ return false;
338
+ };
339
+
340
+ // src/ws/adapter.ts
341
+ var RUNTIME = Symbol.for("dunx.ws.runtime");
342
+ var UNCLAIMED = Symbol.for("dunx.ws.unclaimed");
343
+ var defaultOnError = (error, socket) => {
344
+ console.error(`[dunx/http] ${socket.data.path} handler failed:`, error);
345
+ };
346
+ var reportedByMiddleware = () => {
347
+ return;
348
+ };
349
+ 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(", ")}.`;
350
+ var runtimeOf = (socket) => socket.data[RUNTIME];
351
+ var isBinary = (value) => value instanceof ArrayBuffer || ArrayBuffer.isView(value);
352
+ var replyRaw = (socket, value) => {
353
+ if (value === undefined)
354
+ return;
355
+ socket.send(typeof value === "string" || isBinary(value) ? value : JSON.stringify(value));
356
+ };
357
+ var settle = (result, socket, onError, then) => {
358
+ if (result instanceof Promise) {
359
+ result.then((value) => {
360
+ if (!then)
361
+ return;
362
+ try {
363
+ then(value);
364
+ } catch (error) {
365
+ onError(error, socket);
366
+ }
367
+ }, (error) => onError(error, socket));
368
+ return;
369
+ }
370
+ if (then)
371
+ then(result);
372
+ };
373
+ var framing = (kind) => {
374
+ if (kind === HandlerKind.CLOSE) {
375
+ return (args) => ({
376
+ socket: args[0],
377
+ data: { code: args[1], reason: args[2] }
378
+ });
379
+ }
380
+ if (kind === HandlerKind.OPEN || kind === HandlerKind.DRAIN) {
381
+ return (args) => ({ socket: args[0], data: undefined });
382
+ }
383
+ return (args) => ({ socket: args[1], data: args[0] });
384
+ };
385
+ var NOTHING = () => {
386
+ return;
387
+ };
388
+ var through = (gateway, middleware, kind, event, invoke) => {
389
+ const ctx = {
390
+ gateway: gateway.name,
391
+ path: gateway.path,
392
+ kind,
393
+ event
394
+ };
395
+ const dispatch = composeSocket(middleware, ctx);
396
+ const frameOf = framing(kind);
397
+ const run = invoke ?? NOTHING;
398
+ return (...args) => dispatch(frameOf(args), () => run(...args));
399
+ };
400
+ var withMiddleware = (gateway, middleware) => {
401
+ const wrap = (kind, event, invoke) => through(gateway, middleware, kind, event, invoke);
402
+ const optional = (kind, invoke) => invoke === undefined ? undefined : wrap(kind, undefined, invoke);
403
+ return {
404
+ ...gateway,
405
+ open: wrap(HandlerKind.OPEN, undefined, gateway.open),
406
+ close: wrap(HandlerKind.CLOSE, undefined, gateway.close),
407
+ drain: optional(HandlerKind.DRAIN, gateway.drain),
408
+ ping: optional(HandlerKind.PING, gateway.ping),
409
+ pong: optional(HandlerKind.PONG, gateway.pong),
410
+ raw: optional(HandlerKind.MESSAGE, gateway.raw),
411
+ events: new Map([...gateway.events].map(([event, invoke]) => [
412
+ event,
413
+ wrap(HandlerKind.MESSAGE, event, invoke)
414
+ ]))
415
+ };
416
+ };
417
+ var unclaimedDispatch = (gateway, middleware) => (frame, event) => composeSocket(middleware, {
418
+ gateway: gateway.name,
419
+ path: gateway.path,
420
+ kind: HandlerKind.MESSAGE,
421
+ event
422
+ })(frame, () => {
423
+ return;
424
+ });
425
+ var buildWebSocket = (discovered, options = {}, middleware = []) => {
426
+ const byPath = buildGateways(discovered);
427
+ const wrapped = middleware.length === 0 ? byPath : new Map([...byPath].map(([path, gateway]) => [
428
+ path,
429
+ withMiddleware(gateway, middleware)
430
+ ]));
431
+ const gateways = [...wrapped.values()];
432
+ const onError = options.onError ?? (middleware.length === 0 ? defaultOnError : reportedByMiddleware);
433
+ const reports = options.onError !== undefined || middleware.some((entry) => entry.reportsErrors === true);
434
+ const { onError: _onError, ...socketOptions } = options;
435
+ const run = (invoke, args, ws, then) => {
436
+ try {
437
+ settle(invoke(...args), ws, onError, then);
438
+ } catch (error) {
439
+ onError(error, ws);
440
+ }
441
+ };
442
+ const websocket = {
443
+ ...socketOptions,
444
+ message(ws, message) {
445
+ const gateway = runtimeOf(ws);
446
+ let event;
447
+ if (gateway.events.size > 0) {
448
+ const envelope = decode(message);
449
+ const handler = envelope && gateway.events.get(envelope.event);
450
+ if (envelope && handler) {
451
+ run(handler, [envelope.data, ws], ws, (value) => {
452
+ if (value !== undefined)
453
+ ws.send(encode(envelope.event, value));
454
+ });
455
+ return;
456
+ }
457
+ event = envelope?.event;
458
+ }
459
+ if (gateway.raw) {
460
+ run(gateway.raw, [message, ws], ws, (value) => replyRaw(ws, value));
461
+ return;
462
+ }
463
+ const unclaimed2 = ws.data[UNCLAIMED];
464
+ if (!unclaimed2)
465
+ return;
466
+ try {
467
+ settle(unclaimed2({ socket: ws, data: message }, event), ws, onError, undefined);
468
+ } catch (error) {
469
+ onError(error, ws);
470
+ }
471
+ },
472
+ ...someHandler(gateways, (g) => g.open) && {
473
+ open(ws) {
474
+ const { open } = runtimeOf(ws);
475
+ if (open)
476
+ run(open, [ws], ws, undefined);
477
+ }
478
+ },
479
+ ...someHandler(gateways, (g) => g.close) && {
480
+ close(ws, code, reason) {
481
+ const { close } = runtimeOf(ws);
482
+ if (close)
483
+ run(close, [ws, code, reason], ws, undefined);
484
+ }
485
+ },
486
+ ...someHandler(gateways, (g) => g.drain) && {
487
+ drain(ws) {
488
+ const { drain } = runtimeOf(ws);
489
+ if (drain)
490
+ run(drain, [ws], ws, undefined);
491
+ }
492
+ },
493
+ ...someHandler(gateways, (g) => g.ping) && {
494
+ ping(ws, data) {
495
+ const { ping } = runtimeOf(ws);
496
+ if (ping)
497
+ run(ping, [data, ws], ws, undefined);
498
+ }
499
+ },
500
+ ...someHandler(gateways, (g) => g.pong) && {
501
+ pong(ws, data) {
502
+ const { pong } = runtimeOf(ws);
503
+ if (pong)
504
+ run(pong, [data, ws], ws, undefined);
505
+ }
506
+ }
507
+ };
508
+ const unclaimed = new Map(middleware.length === 0 ? [] : gateways.map((gateway) => [
509
+ gateway,
510
+ unclaimedDispatch(gateway, middleware)
511
+ ]));
512
+ const accept = (req, server, gateway, context) => {
513
+ const fallback = unclaimed.get(gateway);
514
+ const data = {
515
+ path: gateway.path,
516
+ context,
517
+ id: crypto.randomUUID(),
518
+ [RUNTIME]: gateway,
519
+ ...fallback === undefined ? {} : { [UNCLAIMED]: fallback }
520
+ };
521
+ return server.upgrade(req, { data }) ? undefined : new Response("Expected a WebSocket upgrade", { status: 426 });
522
+ };
523
+ const upgradeHandler = (gateway) => (req, server) => {
524
+ if (!gateway.upgrade)
525
+ return accept(req, server, gateway, undefined);
526
+ const result = gateway.upgrade(req);
527
+ if (result instanceof Promise) {
528
+ return result.then((value) => value instanceof Response ? value : accept(req, server, gateway, value));
529
+ }
530
+ return result instanceof Response ? result : accept(req, server, gateway, result);
531
+ };
532
+ return {
533
+ websocket,
534
+ routes: new Map(gateways.map((gateway) => [gateway.path, upgradeHandler(gateway)])),
535
+ paths: [...byPath.keys()],
536
+ warnings: middleware.length > 0 && !reports ? [unreported(middleware)] : [],
537
+ gateways: gateways.map((gateway) => ({
538
+ name: gateway.name,
539
+ path: gateway.path,
540
+ events: [...gateway.events.keys()]
541
+ }))
542
+ };
543
+ };
544
+
204
545
  // src/ws/logging.ts
205
546
  import { Logger, LogLevel, RequestContext } from "@dunx/core";
206
547
  var LIFECYCLE_LABEL = {
@@ -299,7 +640,64 @@ class SocketLoggingMiddleware {
299
640
  Object.defineProperty(SocketLoggingMiddleware, Symbol.for("dunx.deps"), { value: () => [Logger, RequestContext, { unresolved: "options: SocketLoggingOptions = {}", optional: true }] });
300
641
 
301
642
  // src/ws/pubsub.ts
302
- import { AppError as AppError2 } from "@dunx/core";
643
+ import { AppError as AppError5 } from "@dunx/core";
644
+
645
+ // src/ws/relay.ts
646
+ import { AppError as AppError4 } from "@dunx/core";
647
+ var assertRelayUrl = (url, protocols, example) => {
648
+ let parsed;
649
+ try {
650
+ parsed = new URL(url);
651
+ } catch {
652
+ throw new AppError4(`${JSON.stringify(url)} is not a valid URL for the websocket relay. ` + `Expected something like ${example}.`);
653
+ }
654
+ if (!protocols.includes(parsed.protocol)) {
655
+ throw new AppError4(`Unsupported protocol ${JSON.stringify(parsed.protocol)} in ` + `${JSON.stringify(url)}. Expected one of ${protocols.join(", ")}.`);
656
+ }
657
+ return url;
658
+ };
659
+ var redactUrl = (url) => {
660
+ const parsed = new URL(url);
661
+ if (parsed.password)
662
+ parsed.password = "***";
663
+ return parsed.toString();
664
+ };
665
+ var DEFAULT_RELAY_CHANNEL = "dunx:ws";
666
+ var defaultRelayError = (error, phase) => {
667
+ console.warn(`[dunx/http] the websocket relay could not ${phase}. Fan-out is local to ` + "this process until it recovers:", error);
668
+ };
669
+ var toBytes = (data) => ArrayBuffer.isView(data) ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : new Uint8Array(data);
670
+ var encodeRelay = (origin, topic, data) => typeof data === "string" ? JSON.stringify({ o: origin, t: topic, d: data }) : JSON.stringify({
671
+ o: origin,
672
+ t: topic,
673
+ d: Buffer.from(toBytes(data)).toString("base64"),
674
+ b: 1
675
+ });
676
+ var decodeRelay = (message) => {
677
+ let parsed;
678
+ try {
679
+ parsed = JSON.parse(message);
680
+ } catch {
681
+ return;
682
+ }
683
+ if (typeof parsed !== "object" || parsed === null)
684
+ return;
685
+ const { o, t, d, b } = parsed;
686
+ if (typeof o !== "string" || typeof t !== "string" || typeof d !== "string") {
687
+ return;
688
+ }
689
+ return { origin: o, topic: t, data: b ? Buffer.from(d, "base64") : d };
690
+ };
691
+
692
+ class WsRelay {
693
+ constructor() {
694
+ if (new.target === WsRelay) {
695
+ 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.");
696
+ }
697
+ }
698
+ }
699
+
700
+ // src/ws/pubsub.ts
303
701
  class PubSub {
304
702
  #origin = Bun.randomUUIDv7();
305
703
  #server;
@@ -324,7 +722,7 @@ class PubSub {
324
722
  }
325
723
  async relayThrough(relay, options = {}) {
326
724
  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.");
725
+ 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
726
  }
329
727
  this.#relay = relay;
330
728
  this.#channel = options.channel ?? DEFAULT_RELAY_CHANNEL;
@@ -420,7 +818,7 @@ class PubSub {
420
818
  }
421
819
  #live() {
422
820
  if (!this.#server) {
423
- throw new AppError2("PubSub has no server yet. Publish once the server is listening: " + "HttpApp.listen() is what attaches it.");
821
+ throw new AppError5("PubSub has no server yet. Publish once the server is listening: " + "HttpApp.listen() is what attaches it.");
424
822
  }
425
823
  return this.#server;
426
824
  }
@@ -428,10 +826,10 @@ class PubSub {
428
826
 
429
827
  // src/server/application.ts
430
828
  import {
431
- AppError as AppError3,
829
+ AppError as AppError7,
432
830
  Logger as Logger3,
433
831
  runtimeInfo,
434
- ShutdownHooks,
832
+ ShutdownAware,
435
833
  teardownError,
436
834
  teardownFailures as toFailures
437
835
  } from "@dunx/core";
@@ -441,6 +839,27 @@ import {
441
839
  Logger as Logger2,
442
840
  RequestContext as RequestContext2
443
841
  } from "@dunx/core";
842
+
843
+ // src/server/raw-body.ts
844
+ var WANTED = Symbol.for("dunx.http.rawBody.wanted");
845
+ var TEXT = Symbol.for("dunx.http.rawBody.text");
846
+
847
+ class RawBody {
848
+ static want(req) {
849
+ req[WANTED] = true;
850
+ }
851
+ static wanted(req) {
852
+ return req[WANTED] === true;
853
+ }
854
+ static record(req, text) {
855
+ req[TEXT] = text;
856
+ }
857
+ static read(req) {
858
+ return req[TEXT];
859
+ }
860
+ }
861
+
862
+ // src/server/request-logging.ts
444
863
  var parse = (text, limit) => {
445
864
  if (limit === 0)
446
865
  return;
@@ -671,11 +1090,302 @@ class RequestLoggingMiddleware {
671
1090
  }
672
1091
  Object.defineProperty(RequestLoggingMiddleware, Symbol.for("dunx.deps"), { value: () => [Logger2, RequestContext2, { unresolved: "options: RequestLoggingOptions = {}", optional: true }, { unresolved: "metrics?: RequestMetrics", typeOnly: "RequestMetrics" }] });
673
1092
 
1093
+ // src/server/routes.ts
1094
+ import { AppError as AppError6 } from "@dunx/core";
1095
+
1096
+ // src/server/cors.ts
1097
+ var ORIGIN = "access-control-allow-origin";
1098
+ var allowedOrigin = (options, requested) => {
1099
+ const origin = options.origin ?? "*";
1100
+ if (typeof origin === "string") {
1101
+ if (origin !== "*")
1102
+ return origin === requested ? origin : undefined;
1103
+ if (!options.credentials)
1104
+ return "*";
1105
+ return requested ?? undefined;
1106
+ }
1107
+ if (requested === null)
1108
+ return;
1109
+ const allowed = typeof origin === "function" ? origin(requested) : origin.includes(requested);
1110
+ return allowed ? requested : undefined;
1111
+ };
1112
+ var applyCors = (options, req, response) => {
1113
+ const origin = allowedOrigin(options, req.headers.get("origin"));
1114
+ if (origin === undefined)
1115
+ return response;
1116
+ response.headers.set(ORIGIN, origin);
1117
+ if (origin !== "*")
1118
+ response.headers.append("vary", "Origin");
1119
+ if (options.credentials) {
1120
+ response.headers.set("access-control-allow-credentials", "true");
1121
+ }
1122
+ if (options.exposedHeaders?.length) {
1123
+ response.headers.set("access-control-expose-headers", options.exposedHeaders.join(", "));
1124
+ }
1125
+ return response;
1126
+ };
1127
+ var withCors = (options, handler) => {
1128
+ return async (req) => applyCors(options, req, await handler(req));
1129
+ };
1130
+ var preflight = (options, methods) => {
1131
+ const allowMethods = (options.methods ?? methods).join(", ");
1132
+ return async (req) => {
1133
+ const response = applyCors(options, req, new Response(null, { status: HttpStatusCode.NO_CONTENT }));
1134
+ if (!response.headers.has(ORIGIN))
1135
+ return response;
1136
+ response.headers.set("access-control-allow-methods", allowMethods);
1137
+ const allowHeaders = options.allowedHeaders ?? (req.headers.get("access-control-request-headers") ?? "").split(",").map((header) => header.trim()).filter((header) => header.length > 0);
1138
+ if (allowHeaders.length > 0) {
1139
+ response.headers.set("access-control-allow-headers", allowHeaders.join(", "));
1140
+ }
1141
+ if (options.maxAge !== undefined) {
1142
+ response.headers.set("access-control-max-age", String(options.maxAge));
1143
+ }
1144
+ return response;
1145
+ };
1146
+ };
1147
+
1148
+ // src/server/input.ts
1149
+ var grouped = (entries) => {
1150
+ const collected = {};
1151
+ entries.forEach((value, key) => {
1152
+ const existing = collected[key];
1153
+ if (existing === undefined)
1154
+ collected[key] = value;
1155
+ else if (Array.isArray(existing))
1156
+ existing.push(value);
1157
+ else
1158
+ collected[key] = [existing, value];
1159
+ });
1160
+ return collected;
1161
+ };
1162
+ var asJson = (req) => req.json();
1163
+ var asUrlEncoded = async (req) => grouped(new URLSearchParams(await req.text()));
1164
+ var asMultipart = async (req) => grouped(await req.formData());
1165
+ var asText = (req) => req.text();
1166
+ var parserFor = (media) => {
1167
+ if (media === "application/json" || media.endsWith("+json"))
1168
+ return asJson;
1169
+ if (media === "application/x-www-form-urlencoded")
1170
+ return asUrlEncoded;
1171
+ if (media === "multipart/form-data")
1172
+ return asMultipart;
1173
+ if (media.startsWith("text/"))
1174
+ return asText;
1175
+ return;
1176
+ };
1177
+ var JSON_MEDIA = "application/json";
1178
+ var mediaTypeOf = (req) => {
1179
+ const header = req.headers.get("content-type");
1180
+ if (header === JSON_MEDIA || header === null)
1181
+ return JSON_MEDIA;
1182
+ const end = header.indexOf(";");
1183
+ const media = (end === -1 ? header : header.slice(0, end)).trim();
1184
+ return media === "" ? JSON_MEDIA : media.toLowerCase();
1185
+ };
1186
+ var flatten = (issue) => {
1187
+ const path = issue.path?.map((segment) => String(typeof segment === "object" ? segment.key : segment)).join(".");
1188
+ return path === undefined || path === "" ? { message: issue.message } : { message: issue.message, path };
1189
+ };
1190
+ var accept = (source, result) => {
1191
+ if (result.issues !== undefined) {
1192
+ throw new ValidationError(source, result.issues.map(flatten));
1193
+ }
1194
+ return result.value;
1195
+ };
1196
+ var fillWith = (draft, source, schema, value) => {
1197
+ const result = schema["~standard"].validate(value);
1198
+ if (result instanceof Promise) {
1199
+ return result.then((settled) => {
1200
+ draft[source] = accept(source, settled);
1201
+ return draft;
1202
+ });
1203
+ }
1204
+ draft[source] = accept(source, result);
1205
+ return draft;
1206
+ };
1207
+ var bodyFill = (schema) => (draft) => {
1208
+ const media = mediaTypeOf(draft.req);
1209
+ const parse2 = parserFor(media);
1210
+ if (parse2 === undefined) {
1211
+ throw new HttpError(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE, `Unsupported content type "${media}". Declared bodies accept ` + "application/json, application/x-www-form-urlencoded, multipart/form-data or text/*.");
1212
+ }
1213
+ const read = parse2 === asJson && RawBody.wanted(draft.req) ? draft.req.text().then((text) => {
1214
+ RawBody.record(draft.req, text);
1215
+ return JSON.parse(text);
1216
+ }) : parse2(draft.req);
1217
+ return read.then((value) => fillWith(draft, "body", schema, value), (error) => {
1218
+ throw new HttpError(HttpStatusCode.BAD_REQUEST, `Malformed ${media} body`, { cause: error });
1219
+ });
1220
+ };
1221
+ var searchOf = (url) => {
1222
+ const start = url.indexOf("?");
1223
+ if (start === -1)
1224
+ return "";
1225
+ const end = url.indexOf("#", start + 1);
1226
+ return end === -1 ? url.slice(start + 1) : url.slice(start + 1, end);
1227
+ };
1228
+ var queryFill = (schema) => (draft) => {
1229
+ const params = new URLSearchParams(searchOf(draft.req.url));
1230
+ return fillWith(draft, "query", schema, grouped(params));
1231
+ };
1232
+ var paramsFill = (schema) => (draft) => fillWith(draft, "params", schema, draft.req.params);
1233
+ var then = (first, second) => (draft) => {
1234
+ const started = first(draft);
1235
+ return started instanceof Promise ? started.then(second) : second(started);
1236
+ };
1237
+ var buildInputReader = (options) => {
1238
+ const fills = [];
1239
+ if (options?.body !== undefined)
1240
+ fills.push(bodyFill(options.body));
1241
+ if (options?.query !== undefined)
1242
+ fills.push(queryFill(options.query));
1243
+ if (options?.params !== undefined)
1244
+ fills.push(paramsFill(options.params));
1245
+ if (fills.length === 0)
1246
+ return (req) => ({ req });
1247
+ const fill = fills.reduce(then);
1248
+ return (req) => fill({ req });
1249
+ };
1250
+
1251
+ // src/server/middleware.ts
1252
+ var compose = (middleware, ctx, handler) => middleware.reduceRight((next, current) => (req) => current.handle(req, ctx, () => next(req)), handler);
1253
+
1254
+ // src/server/routes.ts
1255
+ var construct = (guard) => new guard;
1256
+ var toResponse = (value, status) => {
1257
+ if (value instanceof Response)
1258
+ return value;
1259
+ if (value === undefined || value === null) {
1260
+ return new Response(null, { status: HttpStatusCode.NO_CONTENT });
1261
+ }
1262
+ return Response.json(value, { status });
1263
+ };
1264
+ var statusFor = (route) => route.options?.status ?? defaultStatusFor(route.method);
1265
+ var assertNoCollisions = (discovered) => {
1266
+ const owners = new Map;
1267
+ for (const route of discovered) {
1268
+ const key = `${route.method} ${route.path}`;
1269
+ const owner = `${route.controller}.${route.handlerName}`;
1270
+ const existing = owners.get(key);
1271
+ if (existing !== undefined) {
1272
+ throw new AppError6(`Route collision: ${key} is declared by ${existing} and by ${owner}. ` + "Bun would keep only one of them.");
1273
+ }
1274
+ owners.set(key, owner);
1275
+ }
1276
+ };
1277
+ var assertNoGatewayCollisions = (discovered, gatewayPaths) => {
1278
+ const gateways = new Set(gatewayPaths);
1279
+ for (const route of discovered) {
1280
+ if (gateways.has(route.path)) {
1281
+ 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.");
1282
+ }
1283
+ }
1284
+ };
1285
+ var withUpgradeRoutes = (routes, gateways) => {
1286
+ const merged = { ...routes };
1287
+ for (const [path, upgrade] of gateways)
1288
+ merged[path] = { GET: upgrade };
1289
+ return merged;
1290
+ };
1291
+ var unmatchedContext = (req, isPublic) => Object.freeze({
1292
+ controller: "(unmatched)",
1293
+ handler: "(none)",
1294
+ method: req.method,
1295
+ path: new URL(req.url).pathname,
1296
+ parsesBody: false,
1297
+ get: (key) => {
1298
+ if (key.id === UNMATCHED.id)
1299
+ return true;
1300
+ if (key.id === PUBLIC.id && isPublic)
1301
+ return true;
1302
+ return;
1303
+ }
1304
+ });
1305
+ var buildFallback = (middleware = [], onError = defaultErrorMapper, cors, notFound = "guarded") => {
1306
+ const miss = () => {
1307
+ throw new HttpError(HttpStatusCode.NOT_FOUND, "NOT_FOUND");
1308
+ };
1309
+ const run = async (req) => {
1310
+ try {
1311
+ return await compose(middleware, unmatchedContext(req, notFound === "public"), miss)(req);
1312
+ } catch (error) {
1313
+ return TraceContext.stamp(onError(error, req), req);
1314
+ }
1315
+ };
1316
+ return cors ? withCors(cors, run) : run;
1317
+ };
1318
+ var directOr = (guarded, route, read, status, onError, noMiddleware) => {
1319
+ if (!noMiddleware)
1320
+ return guarded;
1321
+ const settle2 = (value, req) => {
1322
+ try {
1323
+ return toResponse(value, status);
1324
+ } catch (error) {
1325
+ return onError(error, req);
1326
+ }
1327
+ };
1328
+ const invoke = (input, req) => {
1329
+ try {
1330
+ const value = route.handler(input);
1331
+ return value instanceof Promise ? value.then((resolved) => settle2(resolved, req), (error) => onError(error, req)) : settle2(value, req);
1332
+ } catch (error) {
1333
+ return onError(error, req);
1334
+ }
1335
+ };
1336
+ return (req) => {
1337
+ try {
1338
+ const input = read(req);
1339
+ return input instanceof Promise ? input.then((resolved) => invoke(resolved, req), (error) => onError(error, req)) : invoke(input, req);
1340
+ } catch (error) {
1341
+ return onError(error, req);
1342
+ }
1343
+ };
1344
+ };
1345
+ var buildRoutes = (discovered, middleware = [], onError = defaultErrorMapper, cors, resolve = construct) => {
1346
+ assertNoCollisions(discovered);
1347
+ const routes = {};
1348
+ const instances = new Map;
1349
+ const guardOf = (guard, from) => {
1350
+ const existing = instances.get(guard);
1351
+ if (existing)
1352
+ return existing;
1353
+ const created = resolve(guard, from);
1354
+ instances.set(guard, created);
1355
+ return created;
1356
+ };
1357
+ for (const route of discovered) {
1358
+ const read = buildInputReader(route.options);
1359
+ const status = statusFor(route);
1360
+ const chain = [
1361
+ ...middleware,
1362
+ ...(route.moduleMiddleware ?? []).map((entry) => guardOf(entry, route.module)),
1363
+ ...(route.guards ?? []).map((guard) => guardOf(guard, route.module))
1364
+ ];
1365
+ const chained = compose(chain, buildContext(route), async (req) => toResponse(await route.handler(await read(req)), status));
1366
+ const guarded = async (req) => {
1367
+ try {
1368
+ return await chained(req);
1369
+ } catch (error) {
1370
+ return TraceContext.stamp(onError(error, req), req);
1371
+ }
1372
+ };
1373
+ const byMethod = routes[route.path] ??= {};
1374
+ byMethod[route.method] = cors ? withCors(cors, guarded) : directOr(guarded, route, read, status, onError, chain.length === 0);
1375
+ }
1376
+ if (cors) {
1377
+ for (const byMethod of Object.values(routes)) {
1378
+ byMethod.OPTIONS = preflight(cors, Object.keys(byMethod));
1379
+ }
1380
+ }
1381
+ return routes;
1382
+ };
1383
+
674
1384
  // src/server/settings.ts
675
1385
  var defaultSettings = () => ({ "trust proxy": false });
676
1386
 
677
1387
  // src/server/application.ts
678
- class HttpApplication {
1388
+ class HttpApplication extends ShutdownAware {
679
1389
  warnings;
680
1390
  #root;
681
1391
  closed;
@@ -698,8 +1408,8 @@ class HttpApplication {
698
1408
  #server;
699
1409
  #resolveClosed;
700
1410
  #shuttingDown;
701
- #hooks = new ShutdownHooks;
702
1411
  constructor(app, discovered, options, root, websocket) {
1412
+ super();
703
1413
  this.#app = app;
704
1414
  this.#root = root;
705
1415
  this.warnings = app.warnings;
@@ -856,10 +1566,6 @@ class HttpApplication {
856
1566
  })();
857
1567
  return this.#shuttingDown;
858
1568
  }
859
- enableShutdownHooks(signals = ["SIGTERM", "SIGINT"], options = {}) {
860
- this.#hooks.install(() => this.shutdown(), signals, options);
861
- return this;
862
- }
863
1569
  #prefixed() {
864
1570
  if (this.#globalPrefix === "")
865
1571
  return this.#discovered;
@@ -871,7 +1577,7 @@ class HttpApplication {
871
1577
  #assertNotStarted(hook) {
872
1578
  if (!this.#started)
873
1579
  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.");
1580
+ 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
1581
  }
876
1582
  }
877
1583
  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" }] });
@@ -993,7 +1699,7 @@ class HttpFactory {
993
1699
  for (const controller of readControllers(module)) {
994
1700
  const routes = discoverRoutes(app.get(controller, module.ref));
995
1701
  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.");
1702
+ 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
1703
  }
998
1704
  discovered.push(...routes.map((route) => ({
999
1705
  ...route,
@@ -1020,6 +1726,27 @@ class HttpFactory {
1020
1726
  }
1021
1727
  // src/static/files.ts
1022
1728
  import { join, normalize, resolve } from "path";
1729
+
1730
+ // src/static/options.ts
1731
+ class StaticOptions {
1732
+ root;
1733
+ path;
1734
+ maxAge;
1735
+ immutable;
1736
+ constructor(init) {
1737
+ this.root = init.root;
1738
+ this.path = normalizePrefix(init.path ?? "/");
1739
+ this.maxAge = init.maxAge ?? 60;
1740
+ this.immutable = init.immutable ?? (() => false);
1741
+ }
1742
+ }
1743
+ Object.defineProperty(StaticOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: StaticOptionsInit" }] });
1744
+ var normalizePrefix = (path) => {
1745
+ const trimmed = path.split("/").filter(Boolean).join("/");
1746
+ return trimmed === "" ? "/" : `/${trimmed}`;
1747
+ };
1748
+
1749
+ // src/static/files.ts
1023
1750
  class StaticFiles {
1024
1751
  #options;
1025
1752
  #root;
@@ -1116,6 +1843,99 @@ StaticModule = __decorateElement(_init, 0, "StaticModule", _dec, StaticModule);
1116
1843
  __runInitializers(_init, 1, StaticModule);
1117
1844
  __decoratorMetadata(_init, StaticModule);
1118
1845
  let _StaticModule = StaticModule;
1846
+ // src/compression/negotiate.ts
1847
+ var quality = (params) => {
1848
+ for (const param of params) {
1849
+ const [key, value] = param.split("=");
1850
+ if (key?.trim().toLowerCase() !== "q")
1851
+ continue;
1852
+ const q = Number.parseFloat(value ?? "");
1853
+ return Number.isFinite(q) && q >= 0 && q <= 1 ? q : 1;
1854
+ }
1855
+ return 1;
1856
+ };
1857
+ var negotiate = (header, offered) => {
1858
+ if (header === null)
1859
+ return;
1860
+ const accepted = new Map;
1861
+ for (const element of header.split(",")) {
1862
+ const [name, ...params] = element.split(";");
1863
+ const token = name?.trim().toLowerCase();
1864
+ if (token === undefined || token === "")
1865
+ continue;
1866
+ accepted.set(token, quality(params));
1867
+ }
1868
+ const wildcard = accepted.get("*");
1869
+ let best;
1870
+ let bestQuality = 0;
1871
+ for (const encoding of offered) {
1872
+ const q = accepted.get(encoding) ?? wildcard ?? 0;
1873
+ if (q > bestQuality) {
1874
+ best = encoding;
1875
+ bestQuality = q;
1876
+ }
1877
+ }
1878
+ return best;
1879
+ };
1880
+
1881
+ // src/compression/options.ts
1882
+ var CompressionEncoding = Object.freeze({
1883
+ ZSTD: "zstd",
1884
+ GZIP: "gzip"
1885
+ });
1886
+ var COMPRESSIBLE = new Set([
1887
+ "application/graphql",
1888
+ "application/graphql-response+json",
1889
+ "application/javascript",
1890
+ "application/json",
1891
+ "application/manifest+json",
1892
+ "application/wasm",
1893
+ "application/x-javascript",
1894
+ "application/x-ndjson",
1895
+ "application/xml",
1896
+ "image/svg+xml"
1897
+ ]);
1898
+ var isCompressibleType = (contentType) => {
1899
+ if (contentType === null)
1900
+ return false;
1901
+ const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
1902
+ if (type.startsWith("text/"))
1903
+ return true;
1904
+ if (type.endsWith("+json") || type.endsWith("+xml"))
1905
+ return true;
1906
+ return COMPRESSIBLE.has(type);
1907
+ };
1908
+ var encodable = (encoding) => {
1909
+ const sync = encoding === CompressionEncoding.ZSTD ? Bun.zstdCompressSync : Bun.gzipSync;
1910
+ if (typeof sync !== "function")
1911
+ return false;
1912
+ try {
1913
+ new CompressionStream(encoding);
1914
+ return true;
1915
+ } catch {
1916
+ return false;
1917
+ }
1918
+ };
1919
+
1920
+ class CompressionOptions {
1921
+ encodings;
1922
+ threshold;
1923
+ filter;
1924
+ constructor(init = {}) {
1925
+ this.encodings = init.encodings ?? [
1926
+ CompressionEncoding.ZSTD,
1927
+ CompressionEncoding.GZIP
1928
+ ];
1929
+ const missing = this.encodings.filter((encoding) => !encodable(encoding));
1930
+ if (missing.length > 0) {
1931
+ throw new Error(`Bun ${Bun.version} cannot encode ${missing.join(", ")}. ` + "Pass `encodings` without it, or upgrade Bun.");
1932
+ }
1933
+ this.threshold = init.threshold ?? 1024;
1934
+ this.filter = init.filter ?? isCompressibleType;
1935
+ }
1936
+ }
1937
+ Object.defineProperty(CompressionOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: CompressionOptionsInit = {}", optional: true }] });
1938
+
1119
1939
  // src/compression/compression.ts
1120
1940
  var BODYLESS = new Set([204, 205, 304]);
1121
1941
  var BUFFER_LIMIT = 1024 * 1024;
@@ -1296,7 +2116,7 @@ var SkipThrottle = () => meta(SKIP_THROTTLE, true);
1296
2116
  import { Logger as Logger5 } from "@dunx/core";
1297
2117
 
1298
2118
  // src/throttle/options.ts
1299
- import { AppError as AppError5 } from "@dunx/core";
2119
+ import { AppError as AppError9 } from "@dunx/core";
1300
2120
 
1301
2121
  class ThrottleOptions {
1302
2122
  limit;
@@ -1307,13 +2127,13 @@ class ThrottleOptions {
1307
2127
  store;
1308
2128
  constructor(init) {
1309
2129
  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' }.");
2130
+ 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
2131
  }
1312
2132
  if (!Number.isInteger(init.limit) || init.limit < 1) {
1313
- throw new AppError5(`ThrottleModule needs a limit of at least 1; got ${init.limit}.`);
2133
+ throw new AppError9(`ThrottleModule needs a limit of at least 1; got ${init.limit}.`);
1314
2134
  }
1315
2135
  if (!Number.isInteger(init.windowSeconds) || init.windowSeconds < 1) {
1316
- throw new AppError5("ThrottleModule needs a windowSeconds of at least 1; got " + `${init.windowSeconds}.`);
2136
+ throw new AppError9("ThrottleModule needs a windowSeconds of at least 1; got " + `${init.windowSeconds}.`);
1317
2137
  }
1318
2138
  this.limit = init.limit;
1319
2139
  this.windowSeconds = init.windowSeconds;
@@ -1326,12 +2146,12 @@ class ThrottleOptions {
1326
2146
  Object.defineProperty(ThrottleOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: ThrottleOptionsInit" }] });
1327
2147
 
1328
2148
  // src/throttle/store.ts
1329
- import { AppError as AppError6 } from "@dunx/core";
2149
+ import { AppError as AppError10 } from "@dunx/core";
1330
2150
 
1331
2151
  class ThrottleStore {
1332
2152
  constructor() {
1333
2153
  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.");
2154
+ 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
2155
  }
1336
2156
  }
1337
2157
  }
@@ -1535,21 +2355,8 @@ var OnMessage = (event) => (value) => {
1535
2355
  return value;
1536
2356
  };
1537
2357
  // src/ws/postgres-relay.ts
1538
- import { AppError as AppError7 } from "@dunx/core";
1539
2358
  var PROTOCOLS = ["postgres:", "postgresql:"];
1540
2359
  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
2360
 
1554
2361
  class PostgresRelay extends WsRelay {
1555
2362
  #url;
@@ -1558,14 +2365,11 @@ class PostgresRelay extends WsRelay {
1558
2365
  #subscription;
1559
2366
  constructor(options = {}) {
1560
2367
  super();
1561
- this.#url = assertUrl(options.url ?? defaultPostgresRelayUrl());
2368
+ this.#url = assertRelayUrl(options.url ?? defaultPostgresRelayUrl(), PROTOCOLS, "postgres://localhost:5432/app");
1562
2369
  this.#max = options.max ?? 1;
1563
2370
  }
1564
2371
  get url() {
1565
- const parsed = new URL(this.#url);
1566
- if (parsed.password)
1567
- parsed.password = "***";
1568
- return parsed.toString();
2372
+ return redactUrl(this.#url);
1569
2373
  }
1570
2374
  #client() {
1571
2375
  return this.#sql ??= new Bun.SQL({ url: this.#url, max: this.#max });
@@ -1599,6 +2403,82 @@ class PostgresRelay extends WsRelay {
1599
2403
  }
1600
2404
  }
1601
2405
  Object.defineProperty(PostgresRelay, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "options: PostgresRelayOptions = {}", optional: true }] });
2406
+ // src/ws/redis-relay.ts
2407
+ var PROTOCOLS2 = [
2408
+ "redis:",
2409
+ "rediss:",
2410
+ "valkey:",
2411
+ "valkeys:",
2412
+ "redis+tls:",
2413
+ "redis+unix:",
2414
+ "redis+tls+unix:"
2415
+ ];
2416
+ var defaultRelayUrl = () => process.env["VALKEY_URL"] ?? process.env["REDIS_URL"] ?? "redis://localhost:6379";
2417
+
2418
+ class RedisRelay extends WsRelay {
2419
+ #url;
2420
+ #options;
2421
+ #pub;
2422
+ #sub;
2423
+ #channel;
2424
+ constructor(options = {}) {
2425
+ super();
2426
+ this.#url = assertRelayUrl(options.url ?? defaultRelayUrl(), PROTOCOLS2, "redis://localhost:6379");
2427
+ this.#options = {
2428
+ maxRetries: options.maxRetries ?? 0,
2429
+ ...options.connectionTimeout !== undefined && {
2430
+ connectionTimeout: options.connectionTimeout
2431
+ },
2432
+ ...options.tls !== undefined && { tls: options.tls }
2433
+ };
2434
+ }
2435
+ get url() {
2436
+ return redactUrl(this.#url);
2437
+ }
2438
+ async publish(channel, message) {
2439
+ const client = this.#pub ??= new Bun.RedisClient(this.#url, this.#options);
2440
+ try {
2441
+ return await client.publish(channel, message);
2442
+ } catch (error) {
2443
+ if (this.#pub === client) {
2444
+ this.#pub = undefined;
2445
+ client.close();
2446
+ }
2447
+ throw error;
2448
+ }
2449
+ }
2450
+ async subscribe(channel, listener) {
2451
+ const client = this.#sub ??= new Bun.RedisClient(this.#url, this.#options);
2452
+ try {
2453
+ await client.connect();
2454
+ await client.subscribe(channel, listener);
2455
+ this.#channel = channel;
2456
+ } catch (error) {
2457
+ if (this.#sub === client) {
2458
+ this.#sub = undefined;
2459
+ client.close();
2460
+ }
2461
+ throw error;
2462
+ }
2463
+ }
2464
+ async close() {
2465
+ const sub = this.#sub;
2466
+ const channel = this.#channel;
2467
+ this.#pub?.close();
2468
+ this.#pub = undefined;
2469
+ this.#sub = undefined;
2470
+ this.#channel = undefined;
2471
+ if (!sub)
2472
+ return;
2473
+ if (channel !== undefined) {
2474
+ try {
2475
+ await sub.unsubscribe(channel);
2476
+ } catch {}
2477
+ }
2478
+ sub.close();
2479
+ }
2480
+ }
2481
+ Object.defineProperty(RedisRelay, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "options: RedisRelayOptions = {}", optional: true }] });
1602
2482
  // src/ws/relay-module.ts
1603
2483
  import {
1604
2484
  provide as provide5
@@ -1740,6 +2620,201 @@ class PingProbe {
1740
2620
 
1741
2621
  class QueryProbe {
1742
2622
  }
2623
+ // src/health/controller.ts
2624
+ import { inject } from "@dunx/core";
2625
+
2626
+ // src/health/report-schema.ts
2627
+ var state = {
2628
+ type: "string",
2629
+ enum: ["up", "down", "unknown"],
2630
+ description: "`unknown` is not `down`: a probe that timed out has told you nothing."
2631
+ };
2632
+ var HEALTH_REPORT_SCHEMA = Object.freeze({
2633
+ $id: "HealthReport",
2634
+ type: "object",
2635
+ description: "What the probe found. `up` answers 200 and anything else answers 503.",
2636
+ properties: {
2637
+ status: state,
2638
+ draining: {
2639
+ type: "boolean",
2640
+ description: "The process is shutting down, or something holds it out."
2641
+ },
2642
+ uptimeMs: {
2643
+ type: "integer",
2644
+ description: "Measured on a monotonic clock, so it never goes backwards."
2645
+ },
2646
+ checks: {
2647
+ type: "array",
2648
+ items: {
2649
+ type: "object",
2650
+ properties: {
2651
+ name: { type: "string" },
2652
+ state,
2653
+ critical: {
2654
+ type: "boolean",
2655
+ description: "A failure here sheds traffic. Memory and disk do not."
2656
+ },
2657
+ ms: { type: "integer", description: "How long the check took." },
2658
+ detail: {
2659
+ type: "string",
2660
+ description: "A latency, a version, or a failure message."
2661
+ }
2662
+ },
2663
+ required: ["name", "state", "critical", "ms"]
2664
+ }
2665
+ }
2666
+ },
2667
+ required: ["status", "draining", "uptimeMs", "checks"]
2668
+ });
2669
+
2670
+ // src/health/registry.ts
2671
+ var bounded = async (indicator, timeoutMs) => {
2672
+ let timer;
2673
+ const timeout = new Promise((resolve2) => {
2674
+ timer = setTimeout(() => resolve2({ state: "unknown", detail: `no answer in ${timeoutMs} ms` }), timeoutMs);
2675
+ timer.unref?.();
2676
+ });
2677
+ try {
2678
+ return await Promise.race([
2679
+ Promise.resolve().then(() => indicator.check()).catch((error) => ({
2680
+ state: "down",
2681
+ detail: error instanceof Error ? error.message : String(error)
2682
+ })),
2683
+ timeout
2684
+ ]);
2685
+ } finally {
2686
+ if (timer)
2687
+ clearTimeout(timer);
2688
+ }
2689
+ };
2690
+ var worst = (checks) => {
2691
+ const critical = checks.filter((check) => check.critical);
2692
+ if (critical.some((check) => check.state === "down"))
2693
+ return "down";
2694
+ if (critical.some((check) => check.state === "unknown"))
2695
+ return "unknown";
2696
+ return "up";
2697
+ };
2698
+
2699
+ class HealthOptions {
2700
+ liveness;
2701
+ readiness;
2702
+ timeoutMs;
2703
+ routes;
2704
+ documented;
2705
+ drainDelayMs;
2706
+ constructor(init = {}) {
2707
+ this.liveness = init.liveness ?? [];
2708
+ this.readiness = init.readiness ?? [];
2709
+ this.timeoutMs = init.timeoutMs ?? 2000;
2710
+ this.routes = init.routes ?? true;
2711
+ this.documented = init.documented ?? true;
2712
+ this.drainDelayMs = Math.max(0, init.drainDelayMs ?? 0);
2713
+ }
2714
+ }
2715
+ Object.defineProperty(HealthOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: HealthOptionsInit = {}", optional: true }] });
2716
+
2717
+ class HealthRegistry {
2718
+ options;
2719
+ readiness_;
2720
+ #startedAt = performance.now();
2721
+ constructor(options, readiness_) {
2722
+ this.options = options;
2723
+ this.readiness_ = readiness_;
2724
+ }
2725
+ async report(indicators) {
2726
+ const checks = await Promise.all(indicators.map(async (indicator) => {
2727
+ const started = performance.now();
2728
+ const result = await bounded(indicator, this.options.timeoutMs);
2729
+ return {
2730
+ name: indicator.name,
2731
+ state: result.state,
2732
+ critical: indicator.critical,
2733
+ ms: Math.round(performance.now() - started),
2734
+ ...result.detail === undefined ? {} : { detail: result.detail }
2735
+ };
2736
+ }));
2737
+ return {
2738
+ status: worst(checks),
2739
+ draining: this.readiness_.draining,
2740
+ uptimeMs: Math.round(performance.now() - this.#startedAt),
2741
+ checks
2742
+ };
2743
+ }
2744
+ liveness() {
2745
+ return this.report(this.options.liveness);
2746
+ }
2747
+ async readiness() {
2748
+ const report = await this.report(this.options.readiness);
2749
+ if (!this.readiness_.draining)
2750
+ return report;
2751
+ return {
2752
+ ...report,
2753
+ status: "down",
2754
+ checks: [
2755
+ {
2756
+ name: "readiness",
2757
+ state: "down",
2758
+ critical: true,
2759
+ ms: 0,
2760
+ detail: this.readiness_.reason ?? "not accepting traffic"
2761
+ },
2762
+ ...report.checks
2763
+ ]
2764
+ };
2765
+ }
2766
+ }
2767
+ Object.defineProperty(HealthRegistry, Symbol.for("dunx.deps"), { value: () => [HealthOptions, { unresolved: "private readonly readiness_: Readiness", typeOnly: "Readiness" }] });
2768
+
2769
+ // src/health/controller.ts
2770
+ var probeResponses = {
2771
+ response: { 200: HEALTH_REPORT_SCHEMA, 503: HEALTH_REPORT_SCHEMA }
2772
+ };
2773
+ var answer = (report) => Response.json(report, { status: report.status === "up" ? 200 : 503 });
2774
+ var _dec = [
2775
+ Controller("health")
2776
+ ];
2777
+ var _dec2 = [
2778
+ Public(),
2779
+ Get("/live", probeResponses)
2780
+ ];
2781
+ var _dec3 = [
2782
+ Public(),
2783
+ Get("/ready", probeResponses)
2784
+ ];
2785
+ var _health = new WeakMap;
2786
+ var _init = __decoratorStart(undefined);
2787
+
2788
+ class HealthController {
2789
+ constructor() {
2790
+ __privateAdd(this, _health, inject(HealthRegistry));
2791
+ __runInitializers(_init, 5, this);
2792
+ }
2793
+ async live() {
2794
+ return answer(await __privateGet(this, _health).liveness());
2795
+ }
2796
+ async ready() {
2797
+ return answer(await __privateGet(this, _health).readiness());
2798
+ }
2799
+ }
2800
+ __decorateElement(_init, 1, "live", _dec2, HealthController);
2801
+ __decorateElement(_init, 1, "ready", _dec3, HealthController);
2802
+ HealthController = __decorateElement(_init, 0, "HealthController", _dec, HealthController);
2803
+ __runInitializers(_init, 1, HealthController);
2804
+ __decoratorMetadata(_init, HealthController);
2805
+ let _HealthController = HealthController;
2806
+ var _dec = [
2807
+ ApiHidden()
2808
+ ];
2809
+ var _base = HealthController;
2810
+ var _init = __decoratorStart(_base);
2811
+
2812
+ class HiddenHealthController extends _base {
2813
+ }
2814
+ HiddenHealthController = __decorateElement(_init, 0, "HiddenHealthController", _dec, HiddenHealthController);
2815
+ __runInitializers(_init, 1, HiddenHealthController);
2816
+ __decoratorMetadata(_init, HiddenHealthController);
2817
+ let _HiddenHealthController = HiddenHealthController;
1743
2818
  // src/health/indicators.ts
1744
2819
  import { statfs } from "fs/promises";
1745
2820
  var ms = (started) => Math.round(performance.now() - started);