@dunx/http 3.0.5 → 3.1.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/client/module.d.ts +35 -11
- package/dist/client/options.d.ts +17 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +20 -13
- package/dist/index.d.ts +2 -0
- package/dist/index.js +183 -19
- package/dist/server/application.d.ts +24 -0
- package/dist/server/options-provider.d.ts +108 -0
- package/dist/ws/relay-module.d.ts +49 -0
- package/package.json +2 -2
package/dist/client/module.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
39
|
-
*
|
|
40
|
-
* `
|
|
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
|
-
*
|
|
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>,
|
|
60
|
-
static forRootAsync<const D extends Deps>(config: AsyncModuleConfig<HttpClientOptionsInit, D>,
|
|
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
|
}
|
package/dist/client/options.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 = (
|
|
296
|
-
const
|
|
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,
|
|
302
|
-
providers: [optionsProvider, serviceFrom(
|
|
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
|
-
|
|
310
|
-
|
|
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,
|
|
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 (
|
|
326
|
-
return namedModule(
|
|
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 ?? "
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
1619
|
+
provide6(ReadinessOptions, {
|
|
1460
1620
|
useFactory: (opts) => new ReadinessOptions({ drainDelayMs: opts.drainDelayMs }),
|
|
1461
1621
|
inject: [HealthOptions]
|
|
1462
1622
|
}),
|
|
1463
|
-
|
|
1623
|
+
provide6(Readiness, {
|
|
1464
1624
|
useFactory: (opts) => new Readiness(opts),
|
|
1465
1625
|
inject: [ReadinessOptions]
|
|
1466
1626
|
}),
|
|
1467
|
-
|
|
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([
|
|
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
|
-
|
|
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,
|
|
@@ -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
|
|
3
|
+
"version": "3.1.0",
|
|
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
|
|
65
|
+
"@dunx/core": "^3.1.0",
|
|
66
66
|
"@types/bun": ">=1.3.0"
|
|
67
67
|
},
|
|
68
68
|
"peerDependenciesMeta": {
|