@dunx/http 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -340,13 +340,15 @@ import { AppError as AppError3, ConsoleLogger } from "@dunx/core";
340
340
  class HttpError extends AppError3 {
341
341
  status;
342
342
  name = "HttpError";
343
+ headers;
343
344
  constructor(status, message, options) {
344
345
  super(message, options);
345
346
  this.status = status;
347
+ this.headers = options?.headers;
346
348
  }
347
349
  }
348
350
  Object.defineProperty(HttpError, Symbol.for("dunx.deps"), {
349
- value: () => [{ unresolved: "readonly status: number" }, { unresolved: "message: string" }, ErrorOptions]
351
+ value: () => [{ unresolved: "readonly status: number" }, { unresolved: "message: string" }, { unresolved: "options?: HttpErrorOptions" }]
350
352
  });
351
353
 
352
354
  class ValidationError extends HttpError {
@@ -369,10 +371,16 @@ var isErrorFilter = (handler) => typeof handler === "function" && typeof handler
369
371
  var toErrorMapper = (handler, resolve) => isErrorFilter(handler) ? (error, req) => resolve(handler).catch(error, req) : handler;
370
372
  var errorMapper = (logger) => (error) => {
371
373
  if (error instanceof ValidationError) {
372
- return Response.json({ error: error.message, status: error.status, issues: error.issues }, { status: error.status });
374
+ return Response.json({ error: error.message, status: error.status, issues: error.issues }, {
375
+ status: error.status,
376
+ ...error.headers && { headers: error.headers }
377
+ });
373
378
  }
374
379
  if (error instanceof HttpError) {
375
- return Response.json({ error: error.message, status: error.status }, { status: error.status });
380
+ return Response.json({ error: error.message, status: error.status }, {
381
+ status: error.status,
382
+ ...error.headers && { headers: error.headers }
383
+ });
376
384
  }
377
385
  logger.error("Unhandled error", error);
378
386
  return Response.json({
@@ -386,10 +394,10 @@ import {
386
394
  collectModules as collectModules2,
387
395
  AppError as AppError8,
388
396
  AppFactory,
389
- Logger as Logger3,
397
+ Logger as Logger4,
390
398
  provide,
391
399
  readControllers as readControllers2,
392
- RequestContext as RequestContext2
400
+ RequestContext as RequestContext3
393
401
  } from "@dunx/core";
394
402
 
395
403
  // src/ws/envelope.ts
@@ -409,6 +417,29 @@ var decode = (message) => {
409
417
  return typeof event === "string" ? { event, data } : undefined;
410
418
  };
411
419
 
420
+ // src/ws/middleware.ts
421
+ var composeSocket = (middleware, ctx) => middleware.reduceRight((next, current) => (frame, run) => current.handle(frame, ctx, () => next(frame, run)), (_frame, run) => run());
422
+ var observe = (next, done) => {
423
+ let result;
424
+ try {
425
+ result = next();
426
+ } catch (error) {
427
+ done(error, undefined);
428
+ throw error;
429
+ }
430
+ if (result instanceof Promise) {
431
+ return result.then((value) => {
432
+ done(undefined, value);
433
+ return value;
434
+ }, (error) => {
435
+ done(error, undefined);
436
+ throw error;
437
+ });
438
+ }
439
+ done(undefined, result);
440
+ return result;
441
+ };
442
+
412
443
  // src/ws/runtime.ts
413
444
  import { AppError as AppError4 } from "@dunx/core";
414
445
  var slotOf = (handler) => handler.kind === HandlerKind.MESSAGE && handler.event !== undefined ? `message ${JSON.stringify(handler.event)}` : handler.kind;
@@ -463,9 +494,13 @@ var someHandler = (gateways, pick) => {
463
494
 
464
495
  // src/ws/adapter.ts
465
496
  var RUNTIME = Symbol.for("dunx.ws.runtime");
497
+ var UNCLAIMED = Symbol.for("dunx.ws.unclaimed");
466
498
  var defaultOnError = (error, socket) => {
467
499
  console.error(`[dunx/http] ${socket.data.path} handler failed:`, error);
468
500
  };
501
+ var reportedByMiddleware = () => {
502
+ return;
503
+ };
469
504
  var runtimeOf = (socket) => socket.data[RUNTIME];
470
505
  var isBinary = (value) => value instanceof ArrayBuffer || ArrayBuffer.isView(value);
471
506
  var replyRaw = (socket, value) => {
@@ -489,10 +524,66 @@ var settle = (result, socket, onError, then) => {
489
524
  if (then)
490
525
  then(result);
491
526
  };
492
- var buildWebSocket = (discovered, options = {}) => {
527
+ var framing = (kind) => {
528
+ if (kind === HandlerKind.CLOSE) {
529
+ return (args) => ({
530
+ socket: args[0],
531
+ data: { code: args[1], reason: args[2] }
532
+ });
533
+ }
534
+ if (kind === HandlerKind.OPEN || kind === HandlerKind.DRAIN) {
535
+ return (args) => ({ socket: args[0], data: undefined });
536
+ }
537
+ return (args) => ({ socket: args[1], data: args[0] });
538
+ };
539
+ var NOTHING = () => {
540
+ return;
541
+ };
542
+ var through = (gateway, middleware, kind, event, invoke) => {
543
+ const ctx = {
544
+ gateway: gateway.name,
545
+ path: gateway.path,
546
+ kind,
547
+ event
548
+ };
549
+ const dispatch = composeSocket(middleware, ctx);
550
+ const frameOf = framing(kind);
551
+ const run = invoke ?? NOTHING;
552
+ return (...args) => dispatch(frameOf(args), () => run(...args));
553
+ };
554
+ var withMiddleware = (gateway, middleware) => {
555
+ const wrap = (kind, event, invoke) => through(gateway, middleware, kind, event, invoke);
556
+ const optional = (kind, invoke) => invoke === undefined ? undefined : wrap(kind, undefined, invoke);
557
+ return {
558
+ ...gateway,
559
+ open: wrap(HandlerKind.OPEN, undefined, gateway.open),
560
+ close: wrap(HandlerKind.CLOSE, undefined, gateway.close),
561
+ drain: optional(HandlerKind.DRAIN, gateway.drain),
562
+ ping: optional(HandlerKind.PING, gateway.ping),
563
+ pong: optional(HandlerKind.PONG, gateway.pong),
564
+ raw: optional(HandlerKind.MESSAGE, gateway.raw),
565
+ events: new Map([...gateway.events].map(([event, invoke]) => [
566
+ event,
567
+ wrap(HandlerKind.MESSAGE, event, invoke)
568
+ ]))
569
+ };
570
+ };
571
+ var unclaimedDispatch = (gateway, middleware) => (frame, event) => composeSocket(middleware, {
572
+ gateway: gateway.name,
573
+ path: gateway.path,
574
+ kind: HandlerKind.MESSAGE,
575
+ event
576
+ })(frame, () => {
577
+ return;
578
+ });
579
+ var buildWebSocket = (discovered, options = {}, middleware = []) => {
493
580
  const byPath = buildGateways(discovered);
494
- const gateways = [...byPath.values()];
495
- const onError = options.onError ?? defaultOnError;
581
+ const wrapped = middleware.length === 0 ? byPath : new Map([...byPath].map(([path, gateway]) => [
582
+ path,
583
+ withMiddleware(gateway, middleware)
584
+ ]));
585
+ const gateways = [...wrapped.values()];
586
+ const onError = options.onError ?? (middleware.length === 0 ? defaultOnError : reportedByMiddleware);
496
587
  const { onError: _onError, ...socketOptions } = options;
497
588
  const run = (invoke, args, ws, then) => {
498
589
  try {
@@ -505,6 +596,7 @@ var buildWebSocket = (discovered, options = {}) => {
505
596
  ...socketOptions,
506
597
  message(ws, message) {
507
598
  const gateway = runtimeOf(ws);
599
+ let event;
508
600
  if (gateway.events.size > 0) {
509
601
  const envelope = decode(message);
510
602
  const handler = envelope && gateway.events.get(envelope.event);
@@ -515,9 +607,19 @@ var buildWebSocket = (discovered, options = {}) => {
515
607
  });
516
608
  return;
517
609
  }
610
+ event = envelope?.event;
518
611
  }
519
612
  if (gateway.raw) {
520
613
  run(gateway.raw, [message, ws], ws, (value) => replyRaw(ws, value));
614
+ return;
615
+ }
616
+ const unclaimed2 = ws.data[UNCLAIMED];
617
+ if (!unclaimed2)
618
+ return;
619
+ try {
620
+ settle(unclaimed2({ socket: ws, data: message }, event), ws, onError, undefined);
621
+ } catch (error) {
622
+ onError(error, ws);
521
623
  }
522
624
  },
523
625
  ...someHandler(gateways, (g) => g.open) && {
@@ -556,8 +658,19 @@ var buildWebSocket = (discovered, options = {}) => {
556
658
  }
557
659
  }
558
660
  };
661
+ const unclaimed = new Map(middleware.length === 0 ? [] : gateways.map((gateway) => [
662
+ gateway,
663
+ unclaimedDispatch(gateway, middleware)
664
+ ]));
559
665
  const accept = (req, server, gateway, context) => {
560
- const data = { path: gateway.path, context, [RUNTIME]: gateway };
666
+ const fallback = unclaimed.get(gateway);
667
+ const data = {
668
+ path: gateway.path,
669
+ context,
670
+ id: crypto.randomUUID(),
671
+ [RUNTIME]: gateway,
672
+ ...fallback === undefined ? {} : { [UNCLAIMED]: fallback }
673
+ };
561
674
  return server.upgrade(req, { data }) ? undefined : new Response("Expected a WebSocket upgrade", { status: 426 });
562
675
  };
563
676
  const upgradeHandler = (gateway) => (req, server) => {
@@ -581,6 +694,104 @@ var buildWebSocket = (discovered, options = {}) => {
581
694
  };
582
695
  };
583
696
 
697
+ // src/ws/logging.ts
698
+ import { Logger, LogLevel, RequestContext } from "@dunx/core";
699
+ var LIFECYCLE_LABEL = {
700
+ [HandlerKind.OPEN]: "connect",
701
+ [HandlerKind.CLOSE]: "disconnect"
702
+ };
703
+ var elapsedMs = (started) => Math.round((Bun.nanoseconds() - started) / 1e6);
704
+
705
+ class SocketLoggingMiddleware {
706
+ logger;
707
+ context;
708
+ #level;
709
+ #errorLevel;
710
+ #events;
711
+ #lifecycle;
712
+ #payload;
713
+ #limit;
714
+ #correlate;
715
+ constructor(logger, context, options = {}) {
716
+ this.logger = logger;
717
+ this.context = context;
718
+ this.#level = options.level ?? LogLevel.DEBUG;
719
+ this.#errorLevel = options.errorLevel ?? LogLevel.ERROR;
720
+ this.#events = options.events ?? {};
721
+ this.#lifecycle = options.lifecycle ?? this.#level;
722
+ this.#payload = options.payload ?? false;
723
+ this.#limit = options.maxPayloadLength ?? 512;
724
+ this.#correlate = options.correlate ?? true;
725
+ }
726
+ #levelFor(ctx) {
727
+ if (ctx.kind !== HandlerKind.MESSAGE)
728
+ return this.#lifecycle;
729
+ if (ctx.event === undefined)
730
+ return this.#level;
731
+ return this.#events[ctx.event] ?? this.#level;
732
+ }
733
+ handle(frame, ctx, next) {
734
+ const level = this.#levelFor(ctx);
735
+ if (level === false)
736
+ return next();
737
+ const label = ctx.event ?? LIFECYCLE_LABEL[ctx.kind] ?? ctx.kind;
738
+ const connectionId = frame.socket.data.id;
739
+ const started = Bun.nanoseconds();
740
+ const write2 = (error, value) => {
741
+ const entry = {
742
+ gateway: ctx.gateway,
743
+ path: ctx.path,
744
+ event: label,
745
+ connectionId,
746
+ elapsedMs: elapsedMs(started),
747
+ ...this.#payload ? { payload: this.#brief(frame.data) } : {},
748
+ ...error === undefined ? { replied: value !== undefined } : { err: error }
749
+ };
750
+ const line = `${ctx.path} ${label}`;
751
+ this.#emit(error === undefined ? level : this.#errorLevel, line, entry);
752
+ };
753
+ if (!this.#correlate)
754
+ return observe(next, write2);
755
+ return this.context.runWithContext({ connectionId, event: label, flow: "ws", context: ctx.gateway }, () => observe(next, write2));
756
+ }
757
+ #emit(level, line, entry) {
758
+ switch (level) {
759
+ case LogLevel.VERBOSE:
760
+ this.logger.verbose(line, entry);
761
+ return;
762
+ case LogLevel.DEBUG:
763
+ this.logger.debug(line, entry);
764
+ return;
765
+ case LogLevel.INFO:
766
+ this.logger.info(line, entry);
767
+ return;
768
+ case LogLevel.WARN:
769
+ this.logger.warn(line, entry);
770
+ return;
771
+ case LogLevel.ERROR:
772
+ this.logger.error(line, entry);
773
+ return;
774
+ default:
775
+ this.logger.fatal(line, entry);
776
+ }
777
+ }
778
+ #brief(data) {
779
+ if (data === undefined || this.#limit === 0)
780
+ return;
781
+ if (typeof data === "string") {
782
+ return data.length > this.#limit ? `[${data.length} chars]` : data;
783
+ }
784
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
785
+ return `[${data.byteLength} bytes]`;
786
+ }
787
+ const text = JSON.stringify(data) ?? "";
788
+ return text.length > this.#limit ? `[${text.length} chars]` : data;
789
+ }
790
+ }
791
+ Object.defineProperty(SocketLoggingMiddleware, Symbol.for("dunx.deps"), {
792
+ value: () => [Logger, RequestContext, { unresolved: "options: SocketLoggingOptions = {}" }]
793
+ });
794
+
584
795
  // src/ws/pubsub.ts
585
796
  import { AppError as AppError5 } from "@dunx/core";
586
797
 
@@ -742,15 +953,17 @@ class PubSub {
742
953
  // src/server/application.ts
743
954
  import {
744
955
  AppError as AppError7,
745
- Logger as Logger2,
956
+ Logger as Logger3,
746
957
  runtimeInfo,
747
- ShutdownHooks
958
+ ShutdownHooks,
959
+ teardownError,
960
+ teardownFailures as toFailures
748
961
  } from "@dunx/core";
749
962
 
750
963
  // src/server/request-logging.ts
751
964
  import {
752
- Logger,
753
- RequestContext
965
+ Logger as Logger2,
966
+ RequestContext as RequestContext2
754
967
  } from "@dunx/core";
755
968
  var REQUEST_ID_HEADER = "x-request-id";
756
969
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -768,7 +981,7 @@ var parse = (text, limit) => {
768
981
  return text;
769
982
  }
770
983
  };
771
- var elapsedMs = (started) => Math.round((Bun.nanoseconds() - started) / 1e6);
984
+ var elapsedMs2 = (started) => Math.round((Bun.nanoseconds() - started) / 1e6);
772
985
 
773
986
  class RequestLoggingMiddleware {
774
987
  logger;
@@ -870,7 +1083,7 @@ class RequestLoggingMiddleware {
870
1083
  request,
871
1084
  err: error,
872
1085
  statusCode: status,
873
- elapsedMs: elapsedMs(started)
1086
+ elapsedMs: elapsedMs2(started)
874
1087
  };
875
1088
  const line = `${req.method} ${path} ${status}`;
876
1089
  if (status < HttpStatusCode.INTERNAL_SERVER_ERROR) {
@@ -886,7 +1099,7 @@ class RequestLoggingMiddleware {
886
1099
  ...scope,
887
1100
  request,
888
1101
  statusCode: response.status,
889
- elapsedMs: elapsedMs(started)
1102
+ elapsedMs: elapsedMs2(started)
890
1103
  });
891
1104
  response.headers.set(REQUEST_ID_HEADER, requestId);
892
1105
  return response;
@@ -897,7 +1110,7 @@ class RequestLoggingMiddleware {
897
1110
  request,
898
1111
  statusCode: response.status,
899
1112
  ...value === undefined ? {} : { responseBody: value },
900
- elapsedMs: elapsedMs(started)
1113
+ elapsedMs: elapsedMs2(started)
901
1114
  });
902
1115
  response.headers.set(REQUEST_ID_HEADER, requestId);
903
1116
  return response;
@@ -923,7 +1136,7 @@ class RequestLoggingMiddleware {
923
1136
  }
924
1137
  }
925
1138
  Object.defineProperty(RequestLoggingMiddleware, Symbol.for("dunx.deps"), {
926
- value: () => [Logger, RequestContext, { unresolved: "options: RequestLoggingOptions = {}" }]
1139
+ value: () => [Logger2, RequestContext2, { unresolved: "options: RequestLoggingOptions = {}" }]
927
1140
  });
928
1141
 
929
1142
  // src/server/routes.ts
@@ -1197,7 +1410,7 @@ class HttpApplication {
1197
1410
  ...options.requestLogging === false ? [] : [RequestLoggingMiddleware],
1198
1411
  ...options.middleware ?? []
1199
1412
  ];
1200
- this.#onError = options.onError === undefined ? errorMapper(app.get(Logger2)) : toErrorMapper(options.onError, (token) => app.get(token, root));
1413
+ this.#onError = options.onError === undefined ? errorMapper(app.get(Logger3)) : toErrorMapper(options.onError, (token) => app.get(token, root));
1201
1414
  this.#port = options.port ?? 3000;
1202
1415
  this.#websocket = websocket;
1203
1416
  this.#relay = options.relay;
@@ -1263,7 +1476,7 @@ class HttpApplication {
1263
1476
  const pubsub = this.#app.get(PubSub);
1264
1477
  pubsub.attach(this.#server);
1265
1478
  if (this.#relay) {
1266
- const logger = this.#app.get(Logger2);
1479
+ const logger = this.#app.get(Logger3);
1267
1480
  await pubsub.relayThrough(this.#relay, {
1268
1481
  ...this.#relayChannel !== undefined && {
1269
1482
  channel: this.#relayChannel
@@ -1287,7 +1500,7 @@ class HttpApplication {
1287
1500
  `${routes.length} route(s)`,
1288
1501
  ...gateways.length === 0 ? [] : [`${gateways.length} gateway(s)`]
1289
1502
  ].join(" and ");
1290
- this.#app.get(Logger2).info(`Serving ${subject}`, {
1503
+ this.#app.get(Logger3).info(`Serving ${subject}`, {
1291
1504
  ...runtimeInfo(),
1292
1505
  routes: routes.map((route) => `${route.method} ${route.path}`),
1293
1506
  ...gateways.length === 0 ? {} : {
@@ -1304,12 +1517,25 @@ class HttpApplication {
1304
1517
  }
1305
1518
  async shutdown() {
1306
1519
  this.#shuttingDown ??= (async () => {
1307
- await this.#app.drain();
1308
- await this.#server?.stop(this.#websocket !== undefined);
1520
+ const failures = [];
1521
+ const step = async (run) => {
1522
+ try {
1523
+ await run();
1524
+ } catch (error) {
1525
+ failures.push(...toFailures(error));
1526
+ }
1527
+ };
1528
+ await step(() => this.#app.drain());
1529
+ await step(async () => this.#server?.stop(this.#websocket !== undefined));
1309
1530
  this.#server = undefined;
1310
- await this.#app.get(PubSub).close();
1311
- await this.#app.shutdown();
1312
- this.#resolveClosed?.();
1531
+ await step(() => this.#app.get(PubSub).close());
1532
+ try {
1533
+ await step(() => this.#app.shutdown());
1534
+ } finally {
1535
+ this.#resolveClosed?.();
1536
+ }
1537
+ if (failures.length > 0)
1538
+ throw teardownError(failures);
1313
1539
  })();
1314
1540
  return this.#shuttingDown;
1315
1541
  }
@@ -1343,10 +1569,18 @@ class HttpFactory {
1343
1569
  static async create(root, options = {}) {
1344
1570
  const logging = provide(RequestLoggingMiddleware, {
1345
1571
  useFactory: (logger, context) => new RequestLoggingMiddleware(logger, context, typeof options.requestLogging === "object" ? options.requestLogging : {}),
1346
- inject: [Logger3, RequestContext2]
1572
+ inject: [Logger4, RequestContext3]
1573
+ });
1574
+ const socketLogging = provide(SocketLoggingMiddleware, {
1575
+ useFactory: (logger, context) => new SocketLoggingMiddleware(logger, context, typeof options.socketLogging === "object" ? options.socketLogging : {}),
1576
+ inject: [Logger4, RequestContext3]
1347
1577
  });
1348
1578
  const services = [PubSub, ClientAddress];
1349
- const providers = options.requestLogging === false ? services : [...services, logging];
1579
+ const providers = [
1580
+ ...services,
1581
+ ...options.requestLogging === false ? [] : [logging],
1582
+ ...options.socketLogging === false ? [] : [socketLogging]
1583
+ ];
1350
1584
  const scope = {
1351
1585
  module: HttpModule,
1352
1586
  global: true,
@@ -1375,9 +1609,14 @@ class HttpFactory {
1375
1609
  }
1376
1610
  assertNoCollisions(discovered);
1377
1611
  const gateways = discoverGateways(modules, (token) => app.get(token));
1378
- const websocket = gateways.length > 0 ? buildWebSocket(gateways, options.websocket) : undefined;
1612
+ const websocket = gateways.length > 0 ? buildWebSocket(gateways, options.websocket, HttpFactory.#socketMiddleware(app, root, options)) : undefined;
1379
1613
  return new HttpApplication(app, discovered, options, root, websocket);
1380
1614
  }
1615
+ static #socketMiddleware(app, root, options) {
1616
+ const declared = options.socketMiddleware ?? [];
1617
+ const entries = options.socketLogging === false ? declared : [SocketLoggingMiddleware, ...declared];
1618
+ return entries.map((entry) => app.get(entry, root));
1619
+ }
1381
1620
  }
1382
1621
  // src/static/files.ts
1383
1622
  import { join, normalize, resolve } from "path";
@@ -1502,6 +1741,242 @@ StaticModule = __decorateElement(_init, 0, "StaticModule", _dec, StaticModule);
1502
1741
  __runInitializers(_init, 1, StaticModule);
1503
1742
  __decoratorMetadata(_init, StaticModule);
1504
1743
  let _StaticModule = StaticModule;
1744
+ // src/throttle/decorators.ts
1745
+ var THROTTLE = metaKey("throttle");
1746
+ var SKIP_THROTTLE = metaKey("skip-throttle");
1747
+ var Throttle = (limit) => meta(THROTTLE, limit);
1748
+ var SkipThrottle = () => meta(SKIP_THROTTLE, true);
1749
+ // src/throttle/guard.ts
1750
+ import { Logger as Logger5 } from "@dunx/core";
1751
+
1752
+ // src/throttle/options.ts
1753
+ import { AppError as AppError9 } from "@dunx/core";
1754
+
1755
+ class ThrottleOptions {
1756
+ limit;
1757
+ windowSeconds;
1758
+ prefix;
1759
+ headers;
1760
+ subject;
1761
+ store;
1762
+ constructor(init) {
1763
+ if (init.prefix.trim() === "") {
1764
+ 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' }.");
1765
+ }
1766
+ if (!Number.isInteger(init.limit) || init.limit < 1) {
1767
+ throw new AppError9(`ThrottleModule needs a limit of at least 1; got ${init.limit}.`);
1768
+ }
1769
+ if (!Number.isInteger(init.windowSeconds) || init.windowSeconds < 1) {
1770
+ throw new AppError9("ThrottleModule needs a windowSeconds of at least 1; got " + `${init.windowSeconds}.`);
1771
+ }
1772
+ this.limit = init.limit;
1773
+ this.windowSeconds = init.windowSeconds;
1774
+ this.prefix = init.prefix;
1775
+ this.headers = init.headers ?? true;
1776
+ this.subject = init.subject;
1777
+ this.store = init.store;
1778
+ }
1779
+ }
1780
+ Object.defineProperty(ThrottleOptions, Symbol.for("dunx.deps"), {
1781
+ value: () => [{ unresolved: "init: ThrottleOptionsInit" }]
1782
+ });
1783
+
1784
+ // src/throttle/store.ts
1785
+ import { AppError as AppError10 } from "@dunx/core";
1786
+
1787
+ class ThrottleStore {
1788
+ constructor() {
1789
+ if (new.target === ThrottleStore) {
1790
+ 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.");
1791
+ }
1792
+ }
1793
+ }
1794
+
1795
+ class RedisThrottleStore extends ThrottleStore {
1796
+ redis;
1797
+ constructor(redis) {
1798
+ super();
1799
+ this.redis = redis;
1800
+ }
1801
+ async hit(key, windowSeconds) {
1802
+ const used = await this.redis.incr(key);
1803
+ if (used === 1)
1804
+ await this.redis.expire(key, windowSeconds);
1805
+ return used;
1806
+ }
1807
+ async ttl(key) {
1808
+ const left = await this.redis.ttl(key);
1809
+ return left > 0 ? left : undefined;
1810
+ }
1811
+ }
1812
+ Object.defineProperty(RedisThrottleStore, Symbol.for("dunx.deps"), {
1813
+ value: () => [{ unresolved: "private readonly redis: ThrottleRedis" }]
1814
+ });
1815
+
1816
+ class MemoryThrottleStore extends ThrottleStore {
1817
+ #windows = new Map;
1818
+ #maxKeys;
1819
+ constructor(maxKeys = 1e4) {
1820
+ super();
1821
+ this.#maxKeys = maxKeys;
1822
+ }
1823
+ hit(key, windowSeconds) {
1824
+ const now = Date.now();
1825
+ const existing = this.#windows.get(key);
1826
+ if (existing !== undefined && existing.expiresAt > now) {
1827
+ existing.count += 1;
1828
+ return Promise.resolve(existing.count);
1829
+ }
1830
+ if (this.#windows.size >= this.#maxKeys)
1831
+ this.#sweep(now);
1832
+ this.#windows.set(key, { count: 1, expiresAt: now + windowSeconds * 1000 });
1833
+ return Promise.resolve(1);
1834
+ }
1835
+ ttl(key) {
1836
+ const window = this.#windows.get(key);
1837
+ if (window === undefined)
1838
+ return Promise.resolve(undefined);
1839
+ const left = Math.ceil((window.expiresAt - Date.now()) / 1000);
1840
+ return Promise.resolve(left > 0 ? left : undefined);
1841
+ }
1842
+ #sweep(now) {
1843
+ for (const [key, window] of this.#windows) {
1844
+ if (window.expiresAt <= now)
1845
+ this.#windows.delete(key);
1846
+ }
1847
+ if (this.#windows.size >= this.#maxKeys)
1848
+ this.#windows.clear();
1849
+ }
1850
+ }
1851
+ Object.defineProperty(MemoryThrottleStore, Symbol.for("dunx.deps"), {
1852
+ value: () => [{ unresolved: "maxKeys = 10_000" }]
1853
+ });
1854
+
1855
+ // src/throttle/guard.ts
1856
+ class ThrottleGuard {
1857
+ options;
1858
+ store;
1859
+ address;
1860
+ logger;
1861
+ #warned = false;
1862
+ constructor(options, store, address, logger) {
1863
+ this.options = options;
1864
+ this.store = store;
1865
+ this.address = address;
1866
+ this.logger = logger;
1867
+ }
1868
+ async handle(req, ctx, next) {
1869
+ if (ctx.get(UNMATCHED) === true)
1870
+ return next();
1871
+ if (ctx.get(SKIP_THROTTLE) === true)
1872
+ return next();
1873
+ const limit = ctx.get(THROTTLE) ?? this.options;
1874
+ const key = this.#key(req, ctx);
1875
+ const used = await this.#hit(key, limit.windowSeconds);
1876
+ if (used === undefined)
1877
+ return next();
1878
+ if (used > limit.limit) {
1879
+ const after = await this.#ttl(key) ?? limit.windowSeconds;
1880
+ throw new HttpError(HttpStatusCode.TOO_MANY_REQUESTS, `Rate limit exceeded: ${limit.limit} requests per ` + `${limit.windowSeconds}s`, this.options.headers ? {
1881
+ headers: {
1882
+ "retry-after": String(after),
1883
+ "ratelimit-limit": String(limit.limit),
1884
+ "ratelimit-remaining": "0",
1885
+ "ratelimit-reset": String(after)
1886
+ }
1887
+ } : undefined);
1888
+ }
1889
+ const response = await next();
1890
+ if (this.options.headers) {
1891
+ response.headers.set("ratelimit-limit", String(limit.limit));
1892
+ response.headers.set("ratelimit-remaining", String(Math.max(0, limit.limit - used)));
1893
+ }
1894
+ return response;
1895
+ }
1896
+ #key(req, ctx) {
1897
+ const subject = (this.options.subject ?? ((request) => this.address.of(request)))(req, ctx) ?? "anonymous";
1898
+ return `${this.options.prefix}:throttle:${ctx.controller}:${ctx.handler}:${subject}`;
1899
+ }
1900
+ async#hit(key, windowSeconds) {
1901
+ try {
1902
+ return await this.store.hit(key, windowSeconds);
1903
+ } catch (error) {
1904
+ this.#degraded(error);
1905
+ return;
1906
+ }
1907
+ }
1908
+ async#ttl(key) {
1909
+ try {
1910
+ return await this.store.ttl(key);
1911
+ } catch (error) {
1912
+ this.#degraded(error);
1913
+ return;
1914
+ }
1915
+ }
1916
+ #degraded(error) {
1917
+ if (this.#warned)
1918
+ return;
1919
+ this.#warned = true;
1920
+ this.logger.warn("The rate limiter is unreachable, so requests are not being counted.", { reason: error.message });
1921
+ }
1922
+ }
1923
+ Object.defineProperty(ThrottleGuard, Symbol.for("dunx.deps"), {
1924
+ value: () => [ThrottleOptions, ThrottleStore, ClientAddress, Logger5]
1925
+ });
1926
+ // src/throttle/module.ts
1927
+ import {
1928
+ Logger as Logger6,
1929
+ Module as Module2,
1930
+ provide as provide3
1931
+ } from "@dunx/core";
1932
+ var EXPORTS = [ThrottleOptions, ThrottleStore, ThrottleGuard];
1933
+ var guard = () => provide3(ThrottleGuard, {
1934
+ useFactory: (options, store, address, logger) => new ThrottleGuard(options, store, address, logger),
1935
+ inject: [ThrottleOptions, ThrottleStore, ClientAddress, Logger6]
1936
+ });
1937
+ var store = () => provide3(ThrottleStore, {
1938
+ useFactory: (options) => options.store ?? new MemoryThrottleStore,
1939
+ inject: [ThrottleOptions]
1940
+ });
1941
+ var _dec = [
1942
+ Module2({})
1943
+ ];
1944
+ var _init = __decoratorStart(undefined);
1945
+
1946
+ class ThrottleModule {
1947
+ static forRoot(init) {
1948
+ return {
1949
+ module: ThrottleModule,
1950
+ global: true,
1951
+ exports: EXPORTS,
1952
+ providers: [
1953
+ provide3(ThrottleOptions, { useValue: new ThrottleOptions(init) }),
1954
+ store(),
1955
+ guard()
1956
+ ]
1957
+ };
1958
+ }
1959
+ static forRootAsync(config) {
1960
+ return {
1961
+ module: ThrottleModule,
1962
+ global: true,
1963
+ ...config.imports && { imports: config.imports },
1964
+ exports: EXPORTS,
1965
+ providers: [
1966
+ provide3(ThrottleOptions, {
1967
+ useFactory: async (...deps) => new ThrottleOptions(await config.useFactory(...deps)),
1968
+ inject: config.inject ?? []
1969
+ }),
1970
+ store(),
1971
+ guard()
1972
+ ]
1973
+ };
1974
+ }
1975
+ }
1976
+ ThrottleModule = __decorateElement(_init, 0, "ThrottleModule", _dec, ThrottleModule);
1977
+ __runInitializers(_init, 1, ThrottleModule);
1978
+ __decoratorMetadata(_init, ThrottleModule);
1979
+ let _ThrottleModule = ThrottleModule;
1505
1980
  // src/ws/decorators.ts
1506
1981
  var Gateway = (path = "/") => (target) => {
1507
1982
  markGateway(target, path);
@@ -1522,7 +1997,7 @@ var OnMessage = (event) => (value) => {
1522
1997
  return value;
1523
1998
  };
1524
1999
  // src/ws/redis-relay.ts
1525
- import { AppError as AppError9 } from "@dunx/core";
2000
+ import { AppError as AppError11 } from "@dunx/core";
1526
2001
  var PROTOCOLS = [
1527
2002
  "redis:",
1528
2003
  "rediss:",
@@ -1538,10 +2013,10 @@ var assertUrl = (url) => {
1538
2013
  try {
1539
2014
  parsed = new URL(url);
1540
2015
  } catch {
1541
- throw new AppError9(`${JSON.stringify(url)} is not a valid URL for the websocket relay. ` + "Expected something like redis://localhost:6379.");
2016
+ throw new AppError11(`${JSON.stringify(url)} is not a valid URL for the websocket relay. ` + "Expected something like redis://localhost:6379.");
1542
2017
  }
1543
2018
  if (!PROTOCOLS.includes(parsed.protocol)) {
1544
- throw new AppError9(`Unsupported protocol ${JSON.stringify(parsed.protocol)} in ` + `${JSON.stringify(url)}. Expected one of ${PROTOCOLS.join(", ")}.`);
2019
+ throw new AppError11(`Unsupported protocol ${JSON.stringify(parsed.protocol)} in ` + `${JSON.stringify(url)}. Expected one of ${PROTOCOLS.join(", ")}.`);
1545
2020
  }
1546
2021
  return url;
1547
2022
  };
@@ -1867,8 +2342,8 @@ Object.defineProperty(DiskIndicator, Symbol.for("dunx.deps"), {
1867
2342
  });
1868
2343
  // src/health/module.ts
1869
2344
  import {
1870
- Module as Module2,
1871
- provide as provide3
2345
+ Module as Module3,
2346
+ provide as provide4
1872
2347
  } from "@dunx/core";
1873
2348
 
1874
2349
  // src/health/readiness.ts
@@ -1915,22 +2390,22 @@ Object.defineProperty(Readiness, Symbol.for("dunx.deps"), {
1915
2390
  // src/health/module.ts
1916
2391
  var wiring = (options) => [
1917
2392
  ...options,
1918
- provide3(ReadinessOptions, {
2393
+ provide4(ReadinessOptions, {
1919
2394
  useFactory: (opts) => new ReadinessOptions({ drainDelayMs: opts.drainDelayMs }),
1920
2395
  inject: [HealthOptions]
1921
2396
  }),
1922
- provide3(Readiness, {
2397
+ provide4(Readiness, {
1923
2398
  useFactory: (opts) => new Readiness(opts),
1924
2399
  inject: [ReadinessOptions]
1925
2400
  }),
1926
- provide3(HealthRegistry, {
2401
+ provide4(HealthRegistry, {
1927
2402
  useFactory: (opts, readiness) => new HealthRegistry(opts, readiness),
1928
2403
  inject: [HealthOptions, Readiness]
1929
2404
  })
1930
2405
  ];
1931
2406
  var surface = [HealthOptions, HealthRegistry, Readiness];
1932
2407
  var _dec = [
1933
- Module2({})
2408
+ Module3({})
1934
2409
  ];
1935
2410
  var _init = __decoratorStart(undefined);
1936
2411
 
@@ -1941,7 +2416,7 @@ class HealthModule {
1941
2416
  module: HealthModule,
1942
2417
  ...options.routes ? { controllers: [HealthController] } : {},
1943
2418
  exports: surface,
1944
- providers: wiring([provide3(HealthOptions, { useValue: options })])
2419
+ providers: wiring([provide4(HealthOptions, { useValue: options })])
1945
2420
  };
1946
2421
  }
1947
2422
  static forRootAsync(config) {
@@ -1951,7 +2426,7 @@ class HealthModule {
1951
2426
  ...config.routes ?? true ? { controllers: [HealthController] } : {},
1952
2427
  exports: surface,
1953
2428
  providers: wiring([
1954
- provide3(HealthOptions, {
2429
+ provide4(HealthOptions, {
1955
2430
  useFactory: async (...deps) => new HealthOptions(await config.useFactory(...deps)),
1956
2431
  inject: config.inject ?? []
1957
2432
  })
@@ -1969,6 +2444,7 @@ export {
1969
2444
  toErrorMapper,
1970
2445
  routesOf,
1971
2446
  preflight,
2447
+ observe,
1972
2448
  normalizePrefix,
1973
2449
  normalizePath,
1974
2450
  metaOf,
@@ -1990,6 +2466,7 @@ export {
1990
2466
  defaultErrorMapper,
1991
2467
  decodeRelay,
1992
2468
  decode,
2469
+ composeSocket,
1993
2470
  compose,
1994
2471
  buildWebSocket,
1995
2472
  buildRuntime,
@@ -2001,11 +2478,21 @@ export {
2001
2478
  ValidationError,
2002
2479
  UseGuards,
2003
2480
  UNMATCHED,
2481
+ ThrottleStore,
2482
+ ThrottleOptions,
2483
+ ThrottleModule,
2484
+ ThrottleGuard,
2485
+ Throttle,
2486
+ THROTTLE,
2004
2487
  StaticOptions,
2005
2488
  StaticModule,
2006
2489
  StaticFiles,
2490
+ SocketLoggingMiddleware,
2491
+ SkipThrottle,
2492
+ SKIP_THROTTLE,
2007
2493
  Roles,
2008
2494
  RequestLoggingMiddleware,
2495
+ RedisThrottleStore,
2009
2496
  RedisRelay,
2010
2497
  RedisIndicator,
2011
2498
  ReadinessOptions,
@@ -2027,6 +2514,7 @@ export {
2027
2514
  OnMessage,
2028
2515
  OnDrain,
2029
2516
  OnClose,
2517
+ MemoryThrottleStore,
2030
2518
  MemoryOptions,
2031
2519
  MemoryIndicator,
2032
2520
  HttpStatusCode,
@@ -2052,5 +2540,5 @@ export {
2052
2540
  ApiHidden
2053
2541
  };
2054
2542
 
2055
- //# debugId=0B19A1EE11424A5864756E2164756E21
2543
+ //# debugId=80302AC7A1BB24E664756E2164756E21
2056
2544
  //# sourceMappingURL=index.js.map