@dunx/http 3.0.5 → 3.1.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.
@@ -1,4 +1,4 @@
1
- import { type Deps, type DynamicModule, type AsyncModuleConfig, type Token } from '@dunx/core';
1
+ import { type Deps, type DynamicModule, type AsyncModuleConfig, type Ctor, type Token } from '@dunx/core';
2
2
  import { type HttpClientOptionsInit } from './options.js';
3
3
  import { HttpService } from './service.js';
4
4
  /**
@@ -8,16 +8,28 @@ import { HttpService } from './service.js';
8
8
  * module and the consumer would hold different tokens for `'stripe'` and the lookup
9
9
  * would miss. Same name in, same token out.
10
10
  *
11
- * A `Token` is not a constructor type, so a named client cannot be a constructor
12
- * parameter. Reach it with `inject()` in a field initialiser:
11
+ * A `Token` is not a constructor type, so a client registered under one cannot be a
12
+ * constructor parameter. Reach it with `inject()` in a field initialiser:
13
13
  *
14
14
  * ```ts
15
15
  * class Payments {
16
16
  * readonly stripe = inject(httpClient('stripe'));
17
17
  * }
18
18
  * ```
19
+ *
20
+ * Passing `as` a subclass instead gives an ordinary constructor parameter, and is
21
+ * the shape to prefer for new code.
19
22
  */
20
23
  export declare const httpClient: (name: string) => Token<HttpService>;
24
+ /**
25
+ * How a client is addressed: a name, which binds a `Token`, or a subclass of
26
+ * `HttpService`, which binds the class itself.
27
+ *
28
+ * A subclass is both a token and a parameter type, so `constructor(private readonly
29
+ * email: EmailClient)` resolves - which a `Token` can never do. `as` is the spelling
30
+ * `ConfigModule.forRoot({ validate, as })` already established.
31
+ */
32
+ export type ClientTarget = string | Ctor<HttpService>;
21
33
  /**
22
34
  * The outbound half of `@dunx/http`.
23
35
  *
@@ -35,11 +47,22 @@ export declare const httpClient: (name: string) => Token<HttpService>;
35
47
  */
36
48
  export declare class HttpModule {
37
49
  /**
38
- * Binds `HttpService` and `HttpClientOptions`, or `httpClient(init.name)` alone
39
- * when `name` is set - a named registration deliberately does not also claim
40
- * `HttpService`, so several upstreams can coexist alongside one default.
50
+ * Binds `HttpService` and `HttpClientOptions`.
51
+ *
52
+ * Pass `as` a subclass, or set `init.name`, to register an additional client
53
+ * instead. Either way it does not also claim `HttpService`, so several upstreams
54
+ * coexist alongside one default.
55
+ *
56
+ * ```ts
57
+ * export class EmailClient extends HttpService {}
58
+ * HttpModule.forRoot({ baseUrl: 'https://email.internal' }, EmailClient);
59
+ *
60
+ * class Notifier {
61
+ * constructor(private readonly email: EmailClient) {}
62
+ * }
63
+ * ```
41
64
  */
42
- static forRoot(init?: HttpClientOptionsInit): DynamicModule;
65
+ static forRoot(init?: HttpClientOptionsInit, as?: Ctor<HttpService>): DynamicModule;
43
66
  /**
44
67
  * `forRoot` with the options behind a factory, so the base url or the timeout
45
68
  * can come off `ConfigService`.
@@ -53,9 +76,10 @@ export declare class HttpModule {
53
76
  * });
54
77
  * ```
55
78
  *
56
- * `name` is a parameter rather than a field of the awaited init, because the
57
- * token has to exist before the factory runs.
79
+ * The second parameter is positional rather than a field of the awaited init,
80
+ * because the token has to exist before the factory runs. A subclass there gives
81
+ * a constructor parameter; a string gives an `httpClient(name)` token.
58
82
  */
59
- static forRootAsync(load: () => HttpClientOptionsInit | Promise<HttpClientOptionsInit>, name?: string): DynamicModule;
60
- static forRootAsync<const D extends Deps>(config: AsyncModuleConfig<HttpClientOptionsInit, D>, name?: string): DynamicModule;
83
+ static forRootAsync(load: () => HttpClientOptionsInit | Promise<HttpClientOptionsInit>, as?: ClientTarget): DynamicModule;
84
+ static forRootAsync<const D extends Deps>(config: AsyncModuleConfig<HttpClientOptionsInit, D>, as?: ClientTarget): DynamicModule;
61
85
  }
@@ -43,13 +43,29 @@ export interface HttpClientOptionsInit {
43
43
  * ported one cannot: talk through a proxy, pin a certificate, or reach a unix
44
44
  * socket, with no dependency.
45
45
  */
46
- readonly proxy?: string;
46
+ /**
47
+ * The object form carries `Proxy-Authorization` to the proxy rather than to the
48
+ * target, which the string form cannot express.
49
+ */
50
+ readonly proxy?: BunFetchRequestInit['proxy'];
47
51
  readonly tls?: Bun.TLSOptions;
48
52
  readonly unix?: string;
49
53
  /** @default true - Bun decompresses by default. */
50
54
  readonly decompress?: boolean;
51
55
  /** Bun's own request/response tracing on stderr. Never on in production. */
52
56
  readonly verbose?: boolean;
57
+ /**
58
+ * Compress the request body. A string names the encoding; the object form sets
59
+ * the level too.
60
+ */
61
+ readonly compress?: BunFetchRequestInit['compress'];
62
+ /**
63
+ * `'http2'` lets concurrent requests to one origin share a connection, which is
64
+ * what a service calling a single upstream in a loop wants.
65
+ */
66
+ readonly protocol?: BunFetchRequestInit['protocol'];
67
+ /** How many redirects to follow before rejecting. */
68
+ readonly maxRedirects?: number;
53
69
  }
54
70
  export declare const DEFAULT_REQUEST_ID_HEADER = "x-request-id";
55
71
  /**
package/dist/client.d.ts CHANGED
@@ -8,5 +8,5 @@
8
8
  export { FetchError, FetchTransportError } from './client/errors.js';
9
9
  export { DEFAULT_REQUEST_ID_HEADER, HttpClientOptions, type HttpClientOptionsInit, } from './client/options.js';
10
10
  export type { BackoffOptions, RetryOptions } from './client/retry.js';
11
- export { httpClient, HttpModule } from './client/module.js';
11
+ export { httpClient, HttpModule, type ClientTarget } from './client/module.js';
12
12
  export { HttpService, type HeaderFactory, type RequestConfig, type RequestMethod, } from './client/service.js';
package/dist/client.js CHANGED
@@ -37,7 +37,10 @@ class HttpClientOptions {
37
37
  ["tls", init.tls],
38
38
  ["unix", init.unix],
39
39
  ["decompress", init.decompress],
40
- ["verbose", init.verbose]
40
+ ["verbose", init.verbose],
41
+ ["compress", init.compress],
42
+ ["protocol", init.protocol],
43
+ ["maxRedirects", init.maxRedirects]
41
44
  ].filter(([, value]) => value !== undefined));
42
45
  }
43
46
  }
@@ -288,26 +291,30 @@ var httpClient = (name) => {
288
291
  tokens.set(name, created);
289
292
  return created;
290
293
  };
291
- var serviceFrom = (target, optionsToken) => provide(target, {
292
- useFactory: (options, logger, context) => new HttpService(options, logger, context),
294
+ var serviceFrom = (target, optionsToken, ctor = HttpService) => provide(target, {
295
+ useFactory: (options, logger, context) => new ctor(options, logger, context),
293
296
  inject: [optionsToken, Logger2, RequestContext2]
294
297
  });
295
- var namedModule = (name, options, imports = []) => {
296
- const optionsToken = token(`HttpClientOptions(${name})`);
298
+ var namedModule = (target, options, imports = []) => {
299
+ const label = typeof target === "string" ? target : target.name;
300
+ const service = typeof target === "string" ? httpClient(target) : target;
301
+ const ctor = typeof target === "string" ? HttpService : target;
302
+ const optionsToken = token(`HttpClientOptions(${label})`);
297
303
  const optionsProvider = options instanceof HttpClientOptions ? provide(optionsToken, { useValue: options }) : provide(optionsToken, options);
298
304
  return {
299
305
  module: HttpModule,
300
306
  imports,
301
- exports: [optionsToken, httpClient(name)],
302
- providers: [optionsProvider, serviceFrom(httpClient(name), optionsToken)]
307
+ exports: [optionsToken, service],
308
+ providers: [optionsProvider, serviceFrom(service, optionsToken, ctor)]
303
309
  };
304
310
  };
305
311
 
306
312
  class HttpModule {
307
- static forRoot(init = {}) {
313
+ static forRoot(init = {}, as) {
308
314
  const options = new HttpClientOptions(init);
309
- if (options.name !== undefined)
310
- return namedModule(options.name, options);
315
+ const target = as ?? options.name;
316
+ if (target !== undefined)
317
+ return namedModule(target, options);
311
318
  return {
312
319
  module: HttpModule,
313
320
  exports: [HttpClientOptions, HttpService],
@@ -317,13 +324,13 @@ class HttpModule {
317
324
  ]
318
325
  };
319
326
  }
320
- static forRootAsync(source, name) {
327
+ static forRootAsync(source, as) {
321
328
  const load = typeof source === "function" ? source : source.useFactory;
322
329
  const inject = typeof source === "function" ? [] : source.inject ?? [];
323
330
  const imports = typeof source === "function" ? [] : source.imports ?? [];
324
331
  const useFactory = async (...deps) => new HttpClientOptions(await load(...deps));
325
- if (name !== undefined) {
326
- return namedModule(name, { useFactory, inject }, imports);
332
+ if (as !== undefined) {
333
+ return namedModule(as, { useFactory, inject }, imports);
327
334
  }
328
335
  return {
329
336
  module: HttpModule,
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export type { RouteContext } from './server/context.js';
7
7
  export type { CorsOptions, CorsOrigin } from './server/cors.js';
8
8
  export { defaultErrorMapper, ErrorFilter, errorMapper, HttpError, ValidationError, type ErrorHandler, type ErrorMapper, type HttpErrorOptions, type InputSource, type ValidationIssue, } from './server/errors.js';
9
9
  export { HttpFactory, type HttpApp, type HttpOptions, } from './server/factory.js';
10
+ export { DefaultHttpOptions, HttpOptionsProvider, } from './server/options-provider.js';
10
11
  export { REQUEST_ID_HEADER } from './server/request-id.js';
11
12
  export { TRACEPARENT_HEADER, TRACESTATE_HEADER, TraceContext, type Trace, } from './server/trace-context.js';
12
13
  export { RequestLoggingMiddleware, type RequestLoggingOptions, } from './server/request-logging.js';
@@ -30,6 +31,7 @@ export type { SocketContext, SocketDispatch, SocketFrame, SocketMiddleware, Sock
30
31
  export { SocketLoggingMiddleware, type SocketLoggingOptions, } from './ws/logging.js';
31
32
  export { PubSub } from './ws/pubsub.js';
32
33
  export { RedisRelay, type RedisRelayOptions } from './ws/redis-relay.js';
34
+ export { RelayConnectionOptions, WsRelayModule } from './ws/relay-module.js';
33
35
  export { DEFAULT_RELAY_CHANNEL, type PubSubRelay, type RelayOptions, } from './ws/relay.js';
34
36
  export type { Socket, SocketData, SocketErrorHandler, SocketOptions, } from './ws/socket.js';
35
37
  export { HealthIndicator, PingProbe, QueryProbe, type ProbeResult, type ProbeState, } from './health/contracts.js';
package/dist/index.js CHANGED
@@ -591,12 +591,29 @@ class HttpApplication {
591
591
  this.#relay = options.relay;
592
592
  this.#relayChannel = options.relayChannel;
593
593
  this.#relayResubscribe = options.relayResubscribe;
594
- this.#notFound = options.notFound ?? "guarded";
594
+ this.#notFound = options.notFound ?? "public";
595
595
  this.#bootLogging = options.bootLogging ?? true;
596
596
  this.gatewayPaths = websocket?.paths ?? [];
597
597
  this.closed = new Promise((resolve) => {
598
598
  this.#resolveClosed = resolve;
599
599
  });
600
+ if (options.prefix !== undefined && options.prefix !== "") {
601
+ this.setGlobalPrefix(options.prefix);
602
+ }
603
+ if (options.cors !== undefined) {
604
+ this.enableCors(options.cors);
605
+ }
606
+ if (options.trustProxy !== undefined) {
607
+ this.set("trust proxy", options.trustProxy);
608
+ }
609
+ const hooks = options.shutdownHooks;
610
+ if (hooks !== undefined && hooks !== false) {
611
+ if (hooks === true) {
612
+ this.enableShutdownHooks();
613
+ } else {
614
+ this.enableShutdownHooks(hooks.signals, hooks.options);
615
+ }
616
+ }
600
617
  }
601
618
  get(token) {
602
619
  return this.#app.get(token);
@@ -734,26 +751,91 @@ class HttpApplication {
734
751
  }
735
752
  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" }] });
736
753
 
754
+ // src/server/options-provider.ts
755
+ class HttpOptionsProvider {
756
+ middleware = [];
757
+ socketMiddleware = [];
758
+ notFound = "public";
759
+ bootLogging = true;
760
+ trustProxy = false;
761
+ shutdownHooks = false;
762
+ relayChannel = "dunx:ws";
763
+ get prefix() {
764
+ return "";
765
+ }
766
+ get port() {
767
+ return;
768
+ }
769
+ get cors() {
770
+ return;
771
+ }
772
+ get requestLogging() {
773
+ return true;
774
+ }
775
+ get socketLogging() {
776
+ return true;
777
+ }
778
+ get onError() {
779
+ return;
780
+ }
781
+ get websocket() {
782
+ return;
783
+ }
784
+ get relay() {
785
+ return;
786
+ }
787
+ get relayResubscribe() {
788
+ return;
789
+ }
790
+ }
791
+
792
+ class DefaultHttpOptions extends HttpOptionsProvider {
793
+ }
794
+ function resolveHttpOptions(settings, given) {
795
+ const merged = {
796
+ middleware: settings.middleware,
797
+ socketMiddleware: settings.socketMiddleware,
798
+ notFound: settings.notFound,
799
+ bootLogging: settings.bootLogging,
800
+ trustProxy: settings.trustProxy,
801
+ shutdownHooks: settings.shutdownHooks,
802
+ relayChannel: settings.relayChannel,
803
+ prefix: settings.prefix,
804
+ port: settings.port,
805
+ cors: settings.cors,
806
+ requestLogging: settings.requestLogging,
807
+ socketLogging: settings.socketLogging,
808
+ onError: settings.onError,
809
+ websocket: settings.websocket,
810
+ relay: settings.relay,
811
+ relayResubscribe: settings.relayResubscribe
812
+ };
813
+ for (const key of Object.keys(given)) {
814
+ merged[key] = given[key];
815
+ }
816
+ return merged;
817
+ }
818
+
737
819
  // src/server/factory.ts
738
820
  class HttpModule {
739
821
  }
822
+ var pick = (given, fallback) => {
823
+ const chosen = given ?? fallback;
824
+ return typeof chosen === "object" ? chosen : {};
825
+ };
740
826
 
741
827
  class HttpFactory {
742
828
  static async create(root, options = {}) {
743
829
  const logging = provide(RequestLoggingMiddleware, {
744
- useFactory: (logger, context) => new RequestLoggingMiddleware(logger, context, typeof options.requestLogging === "object" ? options.requestLogging : {}),
745
- inject: [Logger4, RequestContext3]
830
+ useFactory: (logger, context, settings) => new RequestLoggingMiddleware(logger, context, pick(options.requestLogging, settings.requestLogging)),
831
+ inject: [Logger4, RequestContext3, HttpOptionsProvider]
746
832
  });
747
833
  const socketLogging = provide(SocketLoggingMiddleware, {
748
- useFactory: (logger, context) => new SocketLoggingMiddleware(logger, context, typeof options.socketLogging === "object" ? options.socketLogging : {}),
749
- inject: [Logger4, RequestContext3]
834
+ useFactory: (logger, context, settings) => new SocketLoggingMiddleware(logger, context, pick(options.socketLogging, settings.socketLogging)),
835
+ inject: [Logger4, RequestContext3, HttpOptionsProvider]
750
836
  });
751
837
  const services = [PubSub, ClientAddress];
752
- const providers = [
753
- ...services,
754
- ...options.requestLogging === false ? [] : [logging],
755
- ...options.socketLogging === false ? [] : [socketLogging]
756
- ];
838
+ const providers = [...services, logging, socketLogging];
757
839
  const scope = {
758
840
  module: HttpModule,
759
841
  global: true,
@@ -761,7 +843,11 @@ class HttpFactory {
761
843
  providers,
762
844
  exports: providers.map((entry) => typeof entry === "function" ? entry : entry.token)
763
845
  };
764
- const app = await AppFactory.create(scope, options.overrides ? { overrides: options.overrides } : {});
846
+ const app = await AppFactory.create(scope, {
847
+ ...options.overrides ? { overrides: options.overrides } : {},
848
+ promote: [provide(HttpOptionsProvider, { useClass: DefaultHttpOptions })]
849
+ });
850
+ const resolved = resolveHttpOptions(app.get(HttpOptionsProvider, root), options);
765
851
  const modules = collectModules(scope);
766
852
  const discovered = [];
767
853
  for (const module of modules) {
@@ -782,11 +868,11 @@ class HttpFactory {
782
868
  }
783
869
  assertNoCollisions(discovered);
784
870
  const gateways = discoverGateways(modules, (token) => app.get(token));
785
- const websocket = gateways.length > 0 ? buildWebSocket(gateways, options.websocket, HttpFactory.#socketMiddleware(app, root, options)) : undefined;
871
+ const websocket = gateways.length > 0 ? buildWebSocket(gateways, resolved.websocket, HttpFactory.#socketMiddleware(app, root, resolved)) : undefined;
786
872
  for (const warning of websocket?.warnings ?? []) {
787
873
  app.get(Logger4).warn(warning);
788
874
  }
789
- return new HttpApplication(app, discovered, options, root, websocket);
875
+ return new HttpApplication(app, discovered, resolved, root, websocket);
790
876
  }
791
877
  static #socketMiddleware(app, root, options) {
792
878
  const declared = options.socketMiddleware ?? [];
@@ -1310,6 +1396,80 @@ var OnMessage = (event) => (value) => {
1310
1396
  markHandler(value, { kind: HandlerKind.MESSAGE, event });
1311
1397
  return value;
1312
1398
  };
1399
+ // src/ws/relay-module.ts
1400
+ import {
1401
+ provide as provide5
1402
+ } from "@dunx/core";
1403
+ class RelayConnectionOptions {
1404
+ url;
1405
+ maxRetries;
1406
+ connectionTimeout;
1407
+ tls;
1408
+ constructor(init = {}) {
1409
+ this.url = init.url;
1410
+ this.maxRetries = init.maxRetries;
1411
+ this.connectionTimeout = init.connectionTimeout;
1412
+ this.tls = init.tls;
1413
+ }
1414
+ toInit() {
1415
+ return {
1416
+ ...this.url !== undefined && { url: this.url },
1417
+ ...this.maxRetries !== undefined && { maxRetries: this.maxRetries },
1418
+ ...this.connectionTimeout !== undefined && {
1419
+ connectionTimeout: this.connectionTimeout
1420
+ },
1421
+ ...this.tls !== undefined && { tls: this.tls }
1422
+ };
1423
+ }
1424
+ }
1425
+ Object.defineProperty(RelayConnectionOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: RedisRelayOptions = {}", optional: true, typeOnly: "RedisRelayOptions" }] });
1426
+
1427
+ class RelayLifecycle {
1428
+ relay;
1429
+ constructor(relay) {
1430
+ this.relay = relay;
1431
+ }
1432
+ async onShutdown() {
1433
+ await this.relay.close();
1434
+ }
1435
+ }
1436
+ Object.defineProperty(RelayLifecycle, Symbol.for("dunx.deps"), { value: () => [RedisRelay] });
1437
+ var bindings = (options) => [
1438
+ provide5(RelayConnectionOptions, options),
1439
+ provide5(RedisRelay, {
1440
+ useFactory: (settings) => new RedisRelay(settings.toInit()),
1441
+ inject: [RelayConnectionOptions]
1442
+ }),
1443
+ provide5(RelayLifecycle, {
1444
+ useFactory: (relay) => new RelayLifecycle(relay),
1445
+ inject: [RedisRelay]
1446
+ })
1447
+ ];
1448
+
1449
+ class WsRelayModule {
1450
+ static forRoot(init = {}) {
1451
+ return {
1452
+ module: WsRelayModule,
1453
+ exports: [RedisRelay, RelayConnectionOptions],
1454
+ providers: bindings({
1455
+ useFactory: () => new RelayConnectionOptions(init),
1456
+ inject: []
1457
+ })
1458
+ };
1459
+ }
1460
+ static forRootAsync(config) {
1461
+ const useFactory = async (...deps) => new RelayConnectionOptions(await config.useFactory(...deps));
1462
+ return {
1463
+ module: WsRelayModule,
1464
+ ...config.imports === undefined ? {} : { imports: config.imports },
1465
+ exports: [RedisRelay, RelayConnectionOptions],
1466
+ providers: bindings({
1467
+ useFactory,
1468
+ inject: config.inject ?? []
1469
+ })
1470
+ };
1471
+ }
1472
+ }
1313
1473
  // src/health/contracts.ts
1314
1474
  class HealthIndicator {
1315
1475
  critical = true;
@@ -1413,7 +1573,7 @@ Object.defineProperty(DiskIndicator, Symbol.for("dunx.deps"), { value: () => [Di
1413
1573
  // src/health/module.ts
1414
1574
  import {
1415
1575
  Module as Module4,
1416
- provide as provide5
1576
+ provide as provide6
1417
1577
  } from "@dunx/core";
1418
1578
 
1419
1579
  // src/health/readiness.ts
@@ -1456,15 +1616,15 @@ Object.defineProperty(Readiness, Symbol.for("dunx.deps"), { value: () => [Readin
1456
1616
  // src/health/module.ts
1457
1617
  var wiring = (options) => [
1458
1618
  ...options,
1459
- provide5(ReadinessOptions, {
1619
+ provide6(ReadinessOptions, {
1460
1620
  useFactory: (opts) => new ReadinessOptions({ drainDelayMs: opts.drainDelayMs }),
1461
1621
  inject: [HealthOptions]
1462
1622
  }),
1463
- provide5(Readiness, {
1623
+ provide6(Readiness, {
1464
1624
  useFactory: (opts) => new Readiness(opts),
1465
1625
  inject: [ReadinessOptions]
1466
1626
  }),
1467
- provide5(HealthRegistry, {
1627
+ provide6(HealthRegistry, {
1468
1628
  useFactory: (opts, readiness) => new HealthRegistry(opts, readiness),
1469
1629
  inject: [HealthOptions, Readiness]
1470
1630
  })
@@ -1483,7 +1643,7 @@ class HealthModule {
1483
1643
  module: HealthModule,
1484
1644
  ...options.routes ? { controllers: [controllerFor(options.documented)] } : {},
1485
1645
  exports: surface,
1486
- providers: wiring([provide5(HealthOptions, { useValue: options })])
1646
+ providers: wiring([provide6(HealthOptions, { useValue: options })])
1487
1647
  };
1488
1648
  }
1489
1649
  static forRootAsync(config) {
@@ -1493,7 +1653,7 @@ class HealthModule {
1493
1653
  ...config.routes ?? true ? { controllers: [controllerFor(config.documented ?? true)] } : {},
1494
1654
  exports: surface,
1495
1655
  providers: wiring([
1496
- provide5(HealthOptions, {
1656
+ provide6(HealthOptions, {
1497
1657
  useFactory: async (...deps) => new HealthOptions(await config.useFactory(...deps)),
1498
1658
  inject: config.inject ?? []
1499
1659
  })
@@ -1515,6 +1675,7 @@ export {
1515
1675
  Controller,
1516
1676
  DEFAULT_RELAY_CHANNEL,
1517
1677
  DatabaseIndicator,
1678
+ DefaultHttpOptions,
1518
1679
  Delete,
1519
1680
  DiskIndicator,
1520
1681
  DiskOptions,
@@ -1530,6 +1691,7 @@ export {
1530
1691
  HealthRegistry,
1531
1692
  HttpError,
1532
1693
  HttpFactory,
1694
+ HttpOptionsProvider,
1533
1695
  HttpStatusCode,
1534
1696
  MemoryIndicator,
1535
1697
  MemoryOptions,
@@ -1556,6 +1718,7 @@ export {
1556
1718
  RedisIndicator,
1557
1719
  RedisRelay,
1558
1720
  RedisThrottleStore,
1721
+ RelayConnectionOptions,
1559
1722
  RequestLoggingMiddleware,
1560
1723
  Roles,
1561
1724
  SKIP_THROTTLE,
@@ -1576,6 +1739,7 @@ export {
1576
1739
  UNMATCHED,
1577
1740
  UseGuards,
1578
1741
  ValidationError,
1742
+ WsRelayModule,
1579
1743
  defaultErrorMapper,
1580
1744
  errorMapper,
1581
1745
  mergeMeta,
@@ -1,35 +1,7 @@
1
+ import type { StandardSchemaV1 } from '@dunx/core';
1
2
  import type { BunRequest } from 'bun';
2
3
  import type { DefaultStatus, HttpMethod } from './marker.js';
3
- /**
4
- * Standard Schema v1, restated rather than depended on: the spec is an interface,
5
- * so restating it keeps `@dunx/http` at zero dependencies. Zod 4, Valibot and
6
- * ArkType all satisfy this shape already.
7
- */
8
- export interface StandardSchemaV1<In = unknown, Out = In> {
9
- readonly '~standard': {
10
- readonly version: 1;
11
- readonly vendor: string;
12
- readonly validate: (value: unknown) => StandardSchemaResult<Out> | Promise<StandardSchemaResult<Out>>;
13
- readonly types?: {
14
- readonly input: In;
15
- readonly output: Out;
16
- } | undefined;
17
- };
18
- }
19
- /** Success carries `value`; failure carries `issues`. `issues` discriminates. */
20
- export type StandardSchemaResult<Out> = {
21
- readonly value: Out;
22
- readonly issues?: undefined;
23
- } | {
24
- readonly issues: readonly StandardSchemaIssue[];
25
- };
26
- export interface StandardSchemaIssue {
27
- readonly message: string;
28
- /** Zod yields bare keys, Valibot `{ key }` objects. The spec allows both. */
29
- readonly path?: readonly (PropertyKey | {
30
- readonly key: PropertyKey;
31
- })[] | undefined;
32
- }
4
+ export type { StandardSchemaIssue, StandardSchemaResult, StandardSchemaV1, } from '@dunx/core';
33
5
  /** The validated output of a schema - `InferOutput<typeof CreateNote>` is `Note`. */
34
6
  export type InferOutput<S> = S extends StandardSchemaV1<unknown, infer Out> ? Out : never;
35
7
  /**
@@ -154,4 +126,3 @@ export interface RouteInput {
154
126
  readonly query?: unknown;
155
127
  readonly params?: unknown;
156
128
  }
157
- export {};
@@ -13,6 +13,30 @@ import { type RequestLoggingOptions } from './request-logging.js';
13
13
  import { type AppSettings } from './settings.js';
14
14
  export interface HttpOptions extends AppOptions {
15
15
  readonly port?: number;
16
+ /**
17
+ * Prefixes every discovered route, the same thing {@link HttpApp.setGlobalPrefix}
18
+ * does. Both exist: the method is what NestJS offers and what a script reaches
19
+ * for, the field is what an `HttpOptionsProvider` can answer from validated
20
+ * config. A later `setGlobalPrefix` call still wins, because it happens after.
21
+ *
22
+ * Explicitly `| undefined`, unlike the rest: a suite running one fixture both
23
+ * prefixed and unprefixed passes a variable here, and under
24
+ * `exactOptionalPropertyTypes` that would otherwise need a conditional spread.
25
+ * "No prefix" and "absent" mean the same thing. `@dunx/testing` relies on it.
26
+ */
27
+ readonly prefix?: string | undefined;
28
+ /** Mounts an `OPTIONS` preflight per path, as {@link HttpApp.enableCors} does. */
29
+ readonly cors?: CorsOptions;
30
+ /** `app.set('trust proxy', ...)` as a field. */
31
+ readonly trustProxy?: boolean;
32
+ /**
33
+ * Calls `enableShutdownHooks` at construction. `true` takes the default signals;
34
+ * an object names them and tunes the force-exit.
35
+ */
36
+ readonly shutdownHooks?: boolean | {
37
+ readonly signals?: readonly ShutdownSignal[];
38
+ readonly options?: ShutdownHookOptions;
39
+ };
16
40
  /** Resolved from the container, so middleware can inject(). */
17
41
  readonly middleware?: readonly Ctor<Middleware>[];
18
42
  /**
@@ -0,0 +1,108 @@
1
+ import type { Ctor } from '@dunx/core';
2
+ import type { HttpOptions } from './application.js';
3
+ import type { SocketLoggingOptions } from '../ws/logging.js';
4
+ import type { SocketMiddleware } from '../ws/middleware.js';
5
+ import type { PubSubRelay, RelayOptions } from '../ws/relay.js';
6
+ import type { SocketOptions } from '../ws/socket.js';
7
+ import type { CorsOptions } from './cors.js';
8
+ import type { ErrorHandler } from './errors.js';
9
+ import type { Middleware } from './middleware.js';
10
+ import type { RequestLoggingOptions } from './request-logging.js';
11
+ /**
12
+ * How an app configures its HTTP server. A subclass is resolved from the
13
+ * container, so it can inject `ConfigService`.
14
+ *
15
+ * ```ts
16
+ * export class AppHttpOptions extends HttpOptionsProvider {
17
+ * constructor(private readonly config: AppConfigService) {
18
+ * super();
19
+ * }
20
+ *
21
+ * override trustProxy = true;
22
+ *
23
+ * override get prefix(): string {
24
+ * return this.config.get('app').prefix;
25
+ * }
26
+ * }
27
+ *
28
+ * @Module({ providers: [provide(HttpOptionsProvider, { useClass: AppHttpOptions })] })
29
+ * export class HttpConfigModule {}
30
+ * ```
31
+ *
32
+ * Every member has a default, and anything passed to `create()` wins field by
33
+ * field. Override a field with a field and a getter with a getter (`TS2611`,
34
+ * `TS2610`). See docs/architecture/http.md, "HTTP options as a provider".
35
+ */
36
+ export declare abstract class HttpOptionsProvider {
37
+ /**
38
+ * Global middleware, in order, outermost first. Module-scoped middleware is
39
+ * declared by that module instead and does not belong here.
40
+ */
41
+ readonly middleware: readonly Ctor<Middleware>[];
42
+ /** The socket half of {@link middleware}. */
43
+ readonly socketMiddleware: readonly Ctor<SocketMiddleware>[];
44
+ /**
45
+ * What an unmatched path looks like to global middleware. `'guarded'` gives the
46
+ * miss no route metadata, so a global guard refuses it and a prober cannot tell a
47
+ * 404 from a 401; `'public'` reports it as `@Public()` for a conventional 404.
48
+ */
49
+ readonly notFound: 'guarded' | 'public';
50
+ /** One entry at `listen()` naming every route and gateway served. */
51
+ readonly bootLogging: boolean;
52
+ /**
53
+ * Whether `x-forwarded-for` is believed. Off by default: believing it behind
54
+ * nothing lets any caller choose its own address.
55
+ */
56
+ readonly trustProxy: boolean;
57
+ /**
58
+ * Install `SIGTERM`/`SIGINT` handlers that shut the app down. Off by default,
59
+ * because installing a signal handler changes how the process terminates and
60
+ * that is the app's decision to make.
61
+ *
62
+ * The object form names the signals and tunes the force-exit watchdog, which is
63
+ * what `forceExitAfter()` in a hand-written `main.ts` was doing:
64
+ * `{ signals: ['SIGTERM'], options: { exitAfterMs: 8000 } }`.
65
+ */
66
+ readonly shutdownHooks: NonNullable<HttpOptions['shutdownHooks']>;
67
+ /** The broker channel a relay carries frames on. */
68
+ readonly relayChannel: string;
69
+ /** Prefixes every discovered route. Empty means none. */
70
+ get prefix(): string;
71
+ /** `undefined` lets `listen(port)` decide, which is what a test harness needs. */
72
+ get port(): number | undefined;
73
+ /** `undefined` mounts no preflight at all. */
74
+ get cors(): CorsOptions | undefined;
75
+ /** `false` removes the middleware from the chain; an object tunes it. */
76
+ get requestLogging(): boolean | RequestLoggingOptions;
77
+ get socketLogging(): boolean | SocketLoggingOptions;
78
+ /**
79
+ * Replaces the default mapper. Prefer an `ErrorFilter` class over a bare
80
+ * function: a class is resolved from the container and can inject.
81
+ */
82
+ get onError(): ErrorHandler | undefined;
83
+ get websocket(): SocketOptions | undefined;
84
+ /** Multi-node websocket fan-out. Absent publishes to this process only. */
85
+ get relay(): PubSubRelay | undefined;
86
+ get relayResubscribe(): RelayOptions['resubscribe'];
87
+ }
88
+ /**
89
+ * The base itself, bound when no module bound a subclass. Concrete because the
90
+ * container has to construct something, and every member already has a default.
91
+ */
92
+ export declare class DefaultHttpOptions extends HttpOptionsProvider {
93
+ }
94
+ /**
95
+ * One options object out of the two places they can come from.
96
+ *
97
+ * **The argument wins, field by field, and the provider fills the rest.** That
98
+ * ordering is not a preference: `HttpFactory.create(root, options)` already means
99
+ * something, and if the provider won, adding one to an existing app would silently
100
+ * change what its argument does. A field the argument does not mention is the
101
+ * provider's to answer, which is how an app moves configuration into the container
102
+ * one field at a time rather than all at once.
103
+ *
104
+ * Each getter is read exactly once, here, which is when the argument was read
105
+ * before. A getter that depends on something settling later should be read at the
106
+ * point it is used instead.
107
+ */
108
+ export declare function resolveHttpOptions(settings: HttpOptionsProvider, given: HttpOptions): HttpOptions;
@@ -0,0 +1,49 @@
1
+ import { type AsyncModuleConfig, type Deps, type DynamicModule } from '@dunx/core';
2
+ import { type RedisRelayOptions } from './redis-relay.js';
3
+ /**
4
+ * The relay's connection settings, as a class so a factory can bind them.
5
+ *
6
+ * `RedisRelayOptions` stays the interface a caller writes; this is what the
7
+ * container holds, which is the same split `HttpClientOptions` and
8
+ * `HttpClientOptionsInit` use.
9
+ */
10
+ export declare class RelayConnectionOptions {
11
+ readonly url: string | undefined;
12
+ readonly maxRetries: number | undefined;
13
+ readonly connectionTimeout: number | undefined;
14
+ readonly tls: boolean | Bun.TLSOptions | undefined;
15
+ constructor(init?: RedisRelayOptions);
16
+ /** Only the keys actually set, so each `RedisRelay` default still applies. */
17
+ toInit(): RedisRelayOptions;
18
+ }
19
+ /**
20
+ * Binds the websocket relay, so `relay` is a provider rather than an instance
21
+ * `main.ts` constructs and threads into `HttpFactory.create`.
22
+ *
23
+ * `RedisRelay` is a class, so an options provider takes it as a parameter:
24
+ *
25
+ * ```ts
26
+ * export class AppHttpOptions extends HttpOptionsProvider {
27
+ * constructor(private readonly bus: RedisRelay) {
28
+ * super();
29
+ * }
30
+ *
31
+ * override get relay(): PubSubRelay {
32
+ * return this.bus;
33
+ * }
34
+ * }
35
+ * ```
36
+ *
37
+ * A relay of your own needs no module: bind the class and return it from that
38
+ * same getter. This one exists because `RedisRelay` is the one dunx ships and its
39
+ * url comes from config like everything else.
40
+ */
41
+ export declare class WsRelayModule {
42
+ static forRoot(init?: RedisRelayOptions): DynamicModule;
43
+ /**
44
+ * The same bindings with the settings behind a factory, so the url can come off
45
+ * `ConfigService`. `imports` reaches that factory; importing the module
46
+ * alongside does not, since a dynamic module is its own scope.
47
+ */
48
+ static forRootAsync<const D extends Deps>(config: AsyncModuleConfig<RedisRelayOptions, D>): DynamicModule;
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dunx/http",
3
- "version": "3.0.5",
3
+ "version": "3.1.1",
4
4
  "description": "Bun.serve adapter for the dunx framework: controllers, middleware and WebSocket gateways",
5
5
  "keywords": [
6
6
  "bun",
@@ -62,7 +62,7 @@
62
62
  "@dunx/core": "workspace:*"
63
63
  },
64
64
  "peerDependencies": {
65
- "@dunx/core": "^3.0.5",
65
+ "@dunx/core": "^3.1.1",
66
66
  "@types/bun": ">=1.3.0"
67
67
  },
68
68
  "peerDependenciesMeta": {