@dunx/http 3.8.1 → 3.8.3
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/README.md +4 -0
- package/dist/client.js +1 -1
- package/dist/health/contracts.d.ts +6 -0
- package/dist/health/indicators.d.ts +38 -7
- package/dist/index.d.ts +5 -2
- package/dist/index.js +155 -31
- package/dist/server/authorize.d.ts +18 -0
- package/dist/ws/middleware.d.ts +7 -4
- package/dist/ws/observer.d.ts +46 -0
- package/dist/ws/relay-module.d.ts +5 -4
- package/dist/ws/relay-publisher.d.ts +50 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -95,6 +95,10 @@ from, and it may change in any release.
|
|
|
95
95
|
otherwise silently keep one.
|
|
96
96
|
- Handlers may return a `Response`, any JSON-serialisable value, or `undefined`
|
|
97
97
|
for a 204.
|
|
98
|
+
- `Authorize` and `gate()` are the contract an ops surface gates itself with:
|
|
99
|
+
raw request in, 404 on refusal, and a returned `Response` sent as written for a
|
|
100
|
+
browser that needs a sign-in page. `@dunx/dashboard` and `@dunx/openapi` both
|
|
101
|
+
take one, so an app writes the policy once and hands it to both.
|
|
98
102
|
- Schemas, parsers and the status resolve at boot into the same closure the
|
|
99
103
|
middleware chain folds into. A request reads no metadata and does no lookup.
|
|
100
104
|
- Every request adopts W3C Trace Context, so `traceId`, `spanId`, `parentSpanId`
|
package/dist/client.js
CHANGED
|
@@ -41,7 +41,7 @@ class FetchTransportError extends AppError {
|
|
|
41
41
|
this.aborted = aborted;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
Object.defineProperty(FetchTransportError, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "readonly response: { readonly method: string; readonly url: string }" }, { unresolved: "readonly aborted: boolean" }, ErrorOptions] });
|
|
44
|
+
Object.defineProperty(FetchTransportError, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "readonly response: { readonly method: string; readonly url: string }" }, { unresolved: "readonly aborted: boolean" }, typeof ErrorOptions === "undefined" ? { unresolved: "options?: ErrorOptions" } : ErrorOptions] });
|
|
45
45
|
// src/client/options.ts
|
|
46
46
|
class HttpClientOptions {
|
|
47
47
|
baseUrl;
|
|
@@ -54,3 +54,9 @@ export declare abstract class PingProbe {
|
|
|
54
54
|
export declare abstract class QueryProbe {
|
|
55
55
|
abstract ping(): Promise<void>;
|
|
56
56
|
}
|
|
57
|
+
/** Enough of an object store to answer "can I reach it", which `Storage` from
|
|
58
|
+
* `@dunx/infra/files` is on either backend. `exists` and not a read or a write:
|
|
59
|
+
* one `HEAD` against S3, one `stat` locally, and nothing left behind. */
|
|
60
|
+
export declare abstract class StorageProbe {
|
|
61
|
+
abstract exists(key: string): Promise<boolean>;
|
|
62
|
+
}
|
|
@@ -1,16 +1,47 @@
|
|
|
1
|
-
import { HealthIndicator, type PingProbe, type ProbeResult, type QueryProbe } from './contracts.js';
|
|
1
|
+
import { HealthIndicator, type PingProbe, type ProbeResult, type QueryProbe, type StorageProbe } from './contracts.js';
|
|
2
|
+
/** Up on a completed round trip, down with the thrown message, detail the
|
|
3
|
+
* latency. Subclass it with a `name` for anything else answering a `ping()`. */
|
|
4
|
+
export declare abstract class RoundTripIndicator extends HealthIndicator {
|
|
5
|
+
private readonly probe;
|
|
6
|
+
constructor(probe: QueryProbe);
|
|
7
|
+
check(): Promise<ProbeResult>;
|
|
8
|
+
}
|
|
2
9
|
/** Redis is up if it answers `PING`. */
|
|
3
|
-
export declare class RedisIndicator extends
|
|
4
|
-
private readonly redis;
|
|
10
|
+
export declare class RedisIndicator extends RoundTripIndicator {
|
|
5
11
|
readonly name = "redis";
|
|
6
12
|
constructor(redis: PingProbe);
|
|
7
|
-
check(): Promise<ProbeResult>;
|
|
8
13
|
}
|
|
9
14
|
/** The database is up if a round trip completes. */
|
|
10
|
-
export declare class DatabaseIndicator extends
|
|
11
|
-
private readonly db;
|
|
15
|
+
export declare class DatabaseIndicator extends RoundTripIndicator {
|
|
12
16
|
readonly name = "database";
|
|
13
|
-
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The AMQP broker, up while the connection is established and unblocked, and
|
|
20
|
+
* satisfied by `AmqpConnection` from `@dunx/infra/amqp`. The first probe on an
|
|
21
|
+
* unopened connection waits `readyTimeoutMs`, whose 5 s default outruns
|
|
22
|
+
* `timeoutMs` at 2 s and so reports `unknown`; the rest never wait.
|
|
23
|
+
*/
|
|
24
|
+
export declare class AmqpIndicator extends RoundTripIndicator {
|
|
25
|
+
readonly name = "amqp";
|
|
26
|
+
}
|
|
27
|
+
export interface StorageProbeOptionsInit {
|
|
28
|
+
readonly key?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare class StorageProbeOptions {
|
|
31
|
+
readonly key: string;
|
|
32
|
+
constructor(init?: StorageProbeOptionsInit);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The configured object store, which is what `DiskIndicator` stops measuring the
|
|
36
|
+
* moment an app moves to `S3Storage`. Whether the key exists is no part of the
|
|
37
|
+
* signal and the answer is discarded: a store that replies is up, one that throws
|
|
38
|
+
* is down with the message expired credentials and a missing bucket arrive as.
|
|
39
|
+
*/
|
|
40
|
+
export declare class StorageIndicator extends HealthIndicator {
|
|
41
|
+
private readonly storage;
|
|
42
|
+
private readonly options;
|
|
43
|
+
readonly name = "storage";
|
|
44
|
+
constructor(storage: StorageProbe, options?: StorageProbeOptions);
|
|
14
45
|
check(): Promise<ProbeResult>;
|
|
15
46
|
}
|
|
16
47
|
export interface MemoryOptionsInit {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { Controller, Delete, Get, Patch, Post, Put, } from './route/decorators.j
|
|
|
2
2
|
export type { HttpMethod, RoutePath } from './route/marker.js';
|
|
3
3
|
export { ApiHidden, HIDDEN, meta, metaKey, metaOf, mergeMeta, Public, PUBLIC, Roles, ROLES, STREAMS, UNMATCHED, UseGuards, type MetaKey, type MetaRecord, } from './route/metadata.js';
|
|
4
4
|
export type { InferOutput, Input, JsonSchema, ResponseMap, Returns, RouteInput, RouteSchemas, StandardSchemaIssue, StandardSchemaResult, StandardSchemaV1, } from './route/schema.js';
|
|
5
|
+
export { gate, type Authorize, type AuthorizeDecision, } from './server/authorize.js';
|
|
5
6
|
export { ClientAddress } from './server/client-address.js';
|
|
6
7
|
export type { RouteContext } from './server/context.js';
|
|
7
8
|
export type { CorsOptions, CorsOrigin } from './server/cors.js';
|
|
@@ -32,15 +33,17 @@ export { Gateway, OnClose, OnDrain, OnMessage, OnOpen, OnPing, OnPong, OnUpgrade
|
|
|
32
33
|
export type { Envelope } from './ws/envelope.js';
|
|
33
34
|
export type { SocketContext, SocketDispatch, SocketFrame, SocketMiddleware, SocketNext, } from './ws/middleware.js';
|
|
34
35
|
export { SocketLoggingMiddleware, type SocketLoggingOptions, } from './ws/logging.js';
|
|
36
|
+
export { SocketObserver, type SocketOutcome } from './ws/observer.js';
|
|
35
37
|
export { PubSub } from './ws/pubsub.js';
|
|
36
38
|
export { PostgresRelay, type PostgresRelayOptions, } from './ws/postgres-relay.js';
|
|
37
39
|
export { RedisRelay, type RedisRelayOptions } from './ws/redis-relay.js';
|
|
38
40
|
export { PostgresRelayConnectionOptions, RelayConnectionOptions, WsRelayModule, } from './ws/relay-module.js';
|
|
41
|
+
export { RelayPublisher, type RelayPublisherInit, } from './ws/relay-publisher.js';
|
|
39
42
|
export { DEFAULT_RELAY_CHANNEL, WsRelay, type PubSubRelay, type RelayOptions, } from './ws/relay.js';
|
|
40
43
|
export type { Socket, SocketData, SocketErrorHandler, SocketOptions, } from './ws/socket.js';
|
|
41
|
-
export { HealthIndicator, PingProbe, QueryProbe, type ProbeResult, type ProbeState, } from './health/contracts.js';
|
|
44
|
+
export { HealthIndicator, PingProbe, QueryProbe, StorageProbe, type ProbeResult, type ProbeState, } from './health/contracts.js';
|
|
42
45
|
export { HealthController } from './health/controller.js';
|
|
43
|
-
export { DatabaseIndicator, DiskIndicator, DiskOptions, MemoryIndicator, MemoryOptions, RedisIndicator, type DiskOptionsInit, type MemoryOptionsInit, } from './health/indicators.js';
|
|
46
|
+
export { AmqpIndicator, DatabaseIndicator, DiskIndicator, DiskOptions, MemoryIndicator, MemoryOptions, RedisIndicator, RoundTripIndicator, StorageIndicator, StorageProbeOptions, type DiskOptionsInit, type MemoryOptionsInit, type StorageProbeOptionsInit, } from './health/indicators.js';
|
|
44
47
|
export { HealthModule } from './health/module.js';
|
|
45
48
|
export { HEALTH_REPORT_SCHEMA } from './health/report-schema.js';
|
|
46
49
|
export { Readiness, ReadinessOptions } from './health/readiness.js';
|
package/dist/index.js
CHANGED
|
@@ -62,6 +62,17 @@ var Post = verb("POST");
|
|
|
62
62
|
var Put = verb("PUT");
|
|
63
63
|
var Patch = verb("PATCH");
|
|
64
64
|
var Delete = verb("DELETE");
|
|
65
|
+
// src/server/authorize.ts
|
|
66
|
+
var gate = async (authorize, req) => {
|
|
67
|
+
if (authorize === undefined)
|
|
68
|
+
return;
|
|
69
|
+
const decision = await authorize(req);
|
|
70
|
+
if (decision === true)
|
|
71
|
+
return;
|
|
72
|
+
if (decision instanceof Response)
|
|
73
|
+
return decision;
|
|
74
|
+
return Response.json({ error: "NOT_FOUND", status: 404 }, { status: 404 });
|
|
75
|
+
};
|
|
65
76
|
// src/server/client-address.ts
|
|
66
77
|
import { AppError } from "@dunx/core";
|
|
67
78
|
var trustedHops = (setting) => {
|
|
@@ -299,19 +310,19 @@ var observe = (next, done) => {
|
|
|
299
310
|
try {
|
|
300
311
|
result = next();
|
|
301
312
|
} catch (error) {
|
|
302
|
-
done(error, undefined);
|
|
313
|
+
done(error, undefined, false);
|
|
303
314
|
throw error;
|
|
304
315
|
}
|
|
305
316
|
if (result instanceof Promise) {
|
|
306
317
|
return result.then((value) => {
|
|
307
|
-
done(undefined, value);
|
|
318
|
+
done(undefined, value, true);
|
|
308
319
|
return value;
|
|
309
320
|
}, (error) => {
|
|
310
|
-
done(error, undefined);
|
|
321
|
+
done(error, undefined, false);
|
|
311
322
|
throw error;
|
|
312
323
|
});
|
|
313
324
|
}
|
|
314
|
-
done(undefined, result);
|
|
325
|
+
done(undefined, result, true);
|
|
315
326
|
return result;
|
|
316
327
|
};
|
|
317
328
|
|
|
@@ -2671,6 +2682,19 @@ var OnMessage = (event) => (value) => {
|
|
|
2671
2682
|
markHandler(value, { kind: HandlerKind.MESSAGE, event });
|
|
2672
2683
|
return value;
|
|
2673
2684
|
};
|
|
2685
|
+
// src/ws/observer.ts
|
|
2686
|
+
class SocketObserver {
|
|
2687
|
+
reportsErrors = false;
|
|
2688
|
+
handle(frame, ctx, next) {
|
|
2689
|
+
return observe(next, (error, value, ok) => {
|
|
2690
|
+
try {
|
|
2691
|
+
this.settled(ok ? { ok, value } : { ok, error }, frame, ctx);
|
|
2692
|
+
} catch (failure) {
|
|
2693
|
+
console.error(`[dunx/http] ${this.constructor.name}.settled threw, which cannot ` + "change the outcome it was watching:", failure);
|
|
2694
|
+
}
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2674
2698
|
// src/ws/postgres-relay.ts
|
|
2675
2699
|
var PROTOCOLS = ["postgres:", "postgresql:"];
|
|
2676
2700
|
var defaultPostgresRelayUrl = () => process.env["POSTGRES_URL"] ?? process.env["DATABASE_URL"] ?? "postgres://localhost:5432";
|
|
@@ -2800,6 +2824,50 @@ Object.defineProperty(RedisRelay, Symbol.for("dunx.deps"), { value: () => [{ unr
|
|
|
2800
2824
|
import {
|
|
2801
2825
|
provide as provide5
|
|
2802
2826
|
} from "@dunx/core";
|
|
2827
|
+
|
|
2828
|
+
// src/ws/relay-publisher.ts
|
|
2829
|
+
class RelayPublisher {
|
|
2830
|
+
relay;
|
|
2831
|
+
#origin = `worker:${Bun.randomUUIDv7()}`;
|
|
2832
|
+
#channel;
|
|
2833
|
+
#onError;
|
|
2834
|
+
constructor(relay, init = {}) {
|
|
2835
|
+
this.relay = relay;
|
|
2836
|
+
this.#channel = init.channel ?? DEFAULT_RELAY_CHANNEL;
|
|
2837
|
+
this.#onError = init.onError ?? defaultRelayError;
|
|
2838
|
+
}
|
|
2839
|
+
get origin() {
|
|
2840
|
+
return this.#origin;
|
|
2841
|
+
}
|
|
2842
|
+
get channel() {
|
|
2843
|
+
return this.#channel;
|
|
2844
|
+
}
|
|
2845
|
+
publishEvent(topic, event, data) {
|
|
2846
|
+
this.publish(topic, encode(event, data));
|
|
2847
|
+
}
|
|
2848
|
+
publish(topic, data) {
|
|
2849
|
+
try {
|
|
2850
|
+
const result = this.relay.publish(this.#channel, encodeRelay(this.#origin, topic, data));
|
|
2851
|
+
if (result instanceof Promise) {
|
|
2852
|
+
result.catch((error) => {
|
|
2853
|
+
this.#report(error);
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
} catch (error) {
|
|
2857
|
+
this.#report(error);
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
#report(error) {
|
|
2861
|
+
try {
|
|
2862
|
+
this.#onError(error, "publish");
|
|
2863
|
+
} catch (failure) {
|
|
2864
|
+
console.error("[dunx/http] a RelayPublisher onError handler threw:", failure);
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
Object.defineProperty(RelayPublisher, Symbol.for("dunx.deps"), { value: () => [WsRelay, { unresolved: "init: RelayPublisherInit = {}", optional: true }] });
|
|
2869
|
+
|
|
2870
|
+
// src/ws/relay-module.ts
|
|
2803
2871
|
class RelayConnectionOptions {
|
|
2804
2872
|
url;
|
|
2805
2873
|
maxRetries;
|
|
@@ -2850,7 +2918,12 @@ class RelayLifecycle {
|
|
|
2850
2918
|
}
|
|
2851
2919
|
}
|
|
2852
2920
|
Object.defineProperty(RelayLifecycle, Symbol.for("dunx.deps"), { value: () => [WsRelay] });
|
|
2853
|
-
var
|
|
2921
|
+
var publisherBinding = (init) => provide5(RelayPublisher, {
|
|
2922
|
+
useFactory: (relay) => new RelayPublisher(relay, init),
|
|
2923
|
+
inject: [WsRelay]
|
|
2924
|
+
});
|
|
2925
|
+
var redisBindings = (options, publisher) => [
|
|
2926
|
+
publisherBinding(publisher),
|
|
2854
2927
|
provide5(RelayConnectionOptions, options),
|
|
2855
2928
|
provide5(RedisRelay, {
|
|
2856
2929
|
useFactory: (settings) => new RedisRelay(settings.toInit()),
|
|
@@ -2865,7 +2938,8 @@ var redisBindings = (options) => [
|
|
|
2865
2938
|
inject: [WsRelay]
|
|
2866
2939
|
})
|
|
2867
2940
|
];
|
|
2868
|
-
var postgresBindings = (options) => [
|
|
2941
|
+
var postgresBindings = (options, publisher) => [
|
|
2942
|
+
publisherBinding(publisher),
|
|
2869
2943
|
provide5(PostgresRelayConnectionOptions, options),
|
|
2870
2944
|
provide5(PostgresRelay, {
|
|
2871
2945
|
useFactory: (settings) => new PostgresRelay(settings.toInit()),
|
|
@@ -2882,48 +2956,58 @@ var postgresBindings = (options) => [
|
|
|
2882
2956
|
];
|
|
2883
2957
|
|
|
2884
2958
|
class WsRelayModule {
|
|
2885
|
-
static forRoot(init = {}) {
|
|
2959
|
+
static forRoot(init = {}, publisher = {}) {
|
|
2886
2960
|
return {
|
|
2887
2961
|
module: WsRelayModule,
|
|
2888
|
-
exports: [WsRelay, RedisRelay, RelayConnectionOptions],
|
|
2962
|
+
exports: [WsRelay, RedisRelay, RelayConnectionOptions, RelayPublisher],
|
|
2889
2963
|
providers: redisBindings({
|
|
2890
2964
|
useFactory: () => new RelayConnectionOptions(init),
|
|
2891
2965
|
inject: []
|
|
2892
|
-
})
|
|
2966
|
+
}, publisher)
|
|
2893
2967
|
};
|
|
2894
2968
|
}
|
|
2895
|
-
static forRootAsync(config) {
|
|
2969
|
+
static forRootAsync(config, publisher = {}) {
|
|
2896
2970
|
const useFactory = async (...deps) => new RelayConnectionOptions(await config.useFactory(...deps));
|
|
2897
2971
|
return {
|
|
2898
2972
|
module: WsRelayModule,
|
|
2899
2973
|
...config.imports === undefined ? {} : { imports: config.imports },
|
|
2900
|
-
exports: [WsRelay, RedisRelay, RelayConnectionOptions],
|
|
2974
|
+
exports: [WsRelay, RedisRelay, RelayConnectionOptions, RelayPublisher],
|
|
2901
2975
|
providers: redisBindings({
|
|
2902
2976
|
useFactory,
|
|
2903
2977
|
inject: config.inject ?? []
|
|
2904
|
-
})
|
|
2978
|
+
}, publisher)
|
|
2905
2979
|
};
|
|
2906
2980
|
}
|
|
2907
|
-
static forPostgres(init = {}) {
|
|
2981
|
+
static forPostgres(init = {}, publisher = {}) {
|
|
2908
2982
|
return {
|
|
2909
2983
|
module: WsRelayModule,
|
|
2910
|
-
exports: [
|
|
2984
|
+
exports: [
|
|
2985
|
+
WsRelay,
|
|
2986
|
+
PostgresRelay,
|
|
2987
|
+
PostgresRelayConnectionOptions,
|
|
2988
|
+
RelayPublisher
|
|
2989
|
+
],
|
|
2911
2990
|
providers: postgresBindings({
|
|
2912
2991
|
useFactory: () => new PostgresRelayConnectionOptions(init),
|
|
2913
2992
|
inject: []
|
|
2914
|
-
})
|
|
2993
|
+
}, publisher)
|
|
2915
2994
|
};
|
|
2916
2995
|
}
|
|
2917
|
-
static forPostgresAsync(config) {
|
|
2996
|
+
static forPostgresAsync(config, publisher = {}) {
|
|
2918
2997
|
const useFactory = async (...deps) => new PostgresRelayConnectionOptions(await config.useFactory(...deps));
|
|
2919
2998
|
return {
|
|
2920
2999
|
module: WsRelayModule,
|
|
2921
3000
|
...config.imports === undefined ? {} : { imports: config.imports },
|
|
2922
|
-
exports: [
|
|
3001
|
+
exports: [
|
|
3002
|
+
WsRelay,
|
|
3003
|
+
PostgresRelay,
|
|
3004
|
+
PostgresRelayConnectionOptions,
|
|
3005
|
+
RelayPublisher
|
|
3006
|
+
],
|
|
2923
3007
|
providers: postgresBindings({
|
|
2924
3008
|
useFactory,
|
|
2925
3009
|
inject: config.inject ?? []
|
|
2926
|
-
})
|
|
3010
|
+
}, publisher)
|
|
2927
3011
|
};
|
|
2928
3012
|
}
|
|
2929
3013
|
}
|
|
@@ -2937,6 +3021,9 @@ class PingProbe {
|
|
|
2937
3021
|
|
|
2938
3022
|
class QueryProbe {
|
|
2939
3023
|
}
|
|
3024
|
+
|
|
3025
|
+
class StorageProbe {
|
|
3026
|
+
}
|
|
2940
3027
|
// src/health/controller.ts
|
|
2941
3028
|
import { inject } from "@dunx/core";
|
|
2942
3029
|
|
|
@@ -3138,35 +3225,64 @@ let _HiddenHealthController = HiddenHealthController;
|
|
|
3138
3225
|
import { statfs } from "fs/promises";
|
|
3139
3226
|
var ms = (started) => Math.round(performance.now() - started);
|
|
3140
3227
|
|
|
3141
|
-
class
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
constructor(redis) {
|
|
3228
|
+
class RoundTripIndicator extends HealthIndicator {
|
|
3229
|
+
probe;
|
|
3230
|
+
constructor(probe) {
|
|
3145
3231
|
super();
|
|
3146
|
-
this.
|
|
3232
|
+
this.probe = probe;
|
|
3147
3233
|
}
|
|
3148
3234
|
async check() {
|
|
3149
3235
|
const started = performance.now();
|
|
3150
|
-
await this.
|
|
3236
|
+
await this.probe.ping();
|
|
3151
3237
|
return { state: "up", detail: `${ms(started)} ms` };
|
|
3152
3238
|
}
|
|
3153
3239
|
}
|
|
3154
|
-
Object.defineProperty(
|
|
3240
|
+
Object.defineProperty(RoundTripIndicator, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "private readonly probe: QueryProbe", typeOnly: "QueryProbe" }] });
|
|
3241
|
+
|
|
3242
|
+
class RedisIndicator extends RoundTripIndicator {
|
|
3243
|
+
name = "redis";
|
|
3244
|
+
constructor(redis) {
|
|
3245
|
+
super({
|
|
3246
|
+
ping: async () => {
|
|
3247
|
+
await redis.ping();
|
|
3248
|
+
}
|
|
3249
|
+
});
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
Object.defineProperty(RedisIndicator, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "redis: PingProbe", typeOnly: "PingProbe" }] });
|
|
3155
3253
|
|
|
3156
|
-
class DatabaseIndicator extends
|
|
3157
|
-
db;
|
|
3254
|
+
class DatabaseIndicator extends RoundTripIndicator {
|
|
3158
3255
|
name = "database";
|
|
3159
|
-
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
class AmqpIndicator extends RoundTripIndicator {
|
|
3259
|
+
name = "amqp";
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
class StorageProbeOptions {
|
|
3263
|
+
key;
|
|
3264
|
+
constructor(init = {}) {
|
|
3265
|
+
this.key = init.key ?? ".dunx-health";
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
Object.defineProperty(StorageProbeOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: StorageProbeOptionsInit = {}", optional: true }] });
|
|
3269
|
+
|
|
3270
|
+
class StorageIndicator extends HealthIndicator {
|
|
3271
|
+
storage;
|
|
3272
|
+
options;
|
|
3273
|
+
name = "storage";
|
|
3274
|
+
constructor(storage, options = new StorageProbeOptions) {
|
|
3160
3275
|
super();
|
|
3161
|
-
this.
|
|
3276
|
+
this.storage = storage;
|
|
3277
|
+
this.options = options;
|
|
3162
3278
|
}
|
|
3163
3279
|
async check() {
|
|
3164
3280
|
const started = performance.now();
|
|
3165
|
-
await this.
|
|
3281
|
+
await this.storage.exists(this.options.key);
|
|
3166
3282
|
return { state: "up", detail: `${ms(started)} ms` };
|
|
3167
3283
|
}
|
|
3168
3284
|
}
|
|
3169
|
-
Object.defineProperty(
|
|
3285
|
+
Object.defineProperty(StorageIndicator, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "private readonly storage: StorageProbe", typeOnly: "StorageProbe" }, StorageProbeOptions] });
|
|
3170
3286
|
|
|
3171
3287
|
class MemoryOptions {
|
|
3172
3288
|
maxRssBytes;
|
|
@@ -3320,6 +3436,7 @@ __runInitializers(_init, 1, HealthModule);
|
|
|
3320
3436
|
__decoratorMetadata(_init, HealthModule);
|
|
3321
3437
|
let _HealthModule = HealthModule;
|
|
3322
3438
|
export {
|
|
3439
|
+
AmqpIndicator,
|
|
3323
3440
|
ApiHidden2 as ApiHidden,
|
|
3324
3441
|
ClientAddress,
|
|
3325
3442
|
Compression,
|
|
@@ -3375,18 +3492,24 @@ export {
|
|
|
3375
3492
|
RedisRelay,
|
|
3376
3493
|
RedisThrottleStore,
|
|
3377
3494
|
RelayConnectionOptions,
|
|
3495
|
+
RelayPublisher,
|
|
3378
3496
|
RequestLoggingMiddleware,
|
|
3379
3497
|
RequestMetrics,
|
|
3380
3498
|
Roles2 as Roles,
|
|
3499
|
+
RoundTripIndicator,
|
|
3381
3500
|
SKIP_THROTTLE,
|
|
3382
3501
|
STREAMS2 as STREAMS,
|
|
3383
3502
|
SkipThrottle,
|
|
3384
3503
|
SocketLoggingMiddleware,
|
|
3504
|
+
SocketObserver,
|
|
3385
3505
|
Sse,
|
|
3386
3506
|
SseStream,
|
|
3387
3507
|
StaticFiles2 as StaticFiles,
|
|
3388
3508
|
StaticModule,
|
|
3389
3509
|
StaticOptions2 as StaticOptions,
|
|
3510
|
+
StorageIndicator,
|
|
3511
|
+
StorageProbe,
|
|
3512
|
+
StorageProbeOptions,
|
|
3390
3513
|
THROTTLE,
|
|
3391
3514
|
TRACEPARENT_HEADER2 as TRACEPARENT_HEADER,
|
|
3392
3515
|
TRACERESPONSE_HEADER2 as TRACERESPONSE_HEADER,
|
|
@@ -3405,6 +3528,7 @@ export {
|
|
|
3405
3528
|
WsRelayModule,
|
|
3406
3529
|
defaultErrorMapper,
|
|
3407
3530
|
errorMapper,
|
|
3531
|
+
gate,
|
|
3408
3532
|
mergeMeta2 as mergeMeta,
|
|
3409
3533
|
meta2 as meta,
|
|
3410
3534
|
metaKey2 as metaKey,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { BunRequest } from 'bun';
|
|
2
|
+
/**
|
|
3
|
+
* What an {@link Authorize} answers. `true` passes and `false` refuses with a
|
|
4
|
+
* 404; a `Response` refuses with itself, which is what a browser needs.
|
|
5
|
+
*/
|
|
6
|
+
export type AuthorizeDecision = boolean | Response;
|
|
7
|
+
/**
|
|
8
|
+
* Decides whether a request may see an ops surface at all - the dashboard page,
|
|
9
|
+
* the OpenAPI explorer. Both sit outside the app's session guard, so this gets
|
|
10
|
+
* the raw `BunRequest` and asks the auth library. Refusal is 404, not 403.
|
|
11
|
+
*/
|
|
12
|
+
export type Authorize = (req: BunRequest) => AuthorizeDecision | Promise<AuthorizeDecision>;
|
|
13
|
+
/**
|
|
14
|
+
* Runs the gate: `undefined` to carry on, or the response to answer with. Only
|
|
15
|
+
* `true` admits and only a `Response` replaces the refusal, so an `authorize`
|
|
16
|
+
* that falls off the end of a branch closes rather than opens.
|
|
17
|
+
*/
|
|
18
|
+
export declare const gate: (authorize: Authorize | undefined, req: BunRequest) => Promise<Response | undefined>;
|
package/dist/ws/middleware.d.ts
CHANGED
|
@@ -79,8 +79,11 @@ export declare const composeSocket: (middleware: readonly SocketMiddleware[], ct
|
|
|
79
79
|
* Calls `next()` and reports how it went, on whichever channel it went out on,
|
|
80
80
|
* leaving the result untouched.
|
|
81
81
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
82
|
+
* A synchronous throw and a rejection both reach `done` and are then rethrown, so
|
|
83
|
+
* a middleware that only observes cannot accidentally swallow a failure.
|
|
84
|
+
*
|
|
85
|
+
* `ok` says which happened, and it is not `error === undefined`: a handler may
|
|
86
|
+
* `throw undefined` or reject with it, and that reads as a success that returned
|
|
87
|
+
* nothing to anything comparing the value.
|
|
85
88
|
*/
|
|
86
|
-
export declare const observe: (next: SocketNext, done: (error: unknown, value: unknown) => void) => unknown;
|
|
89
|
+
export declare const observe: (next: SocketNext, done: (error: unknown, value: unknown, ok: boolean) => void) => unknown;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type SocketContext, type SocketFrame, type SocketMiddleware, type SocketNext } from './middleware.js';
|
|
2
|
+
/**
|
|
3
|
+
* How a handler went. A union rather than an `error` that is `undefined` on
|
|
4
|
+
* success: a handler may `throw undefined`, and `error !== undefined` reads that
|
|
5
|
+
* as a success.
|
|
6
|
+
*/
|
|
7
|
+
export type SocketOutcome = {
|
|
8
|
+
readonly ok: true;
|
|
9
|
+
readonly value: unknown;
|
|
10
|
+
} | {
|
|
11
|
+
readonly ok: false;
|
|
12
|
+
readonly error: unknown;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A {@link SocketMiddleware} that watches a frame and changes nothing about it.
|
|
16
|
+
*
|
|
17
|
+
* A gateway handler may return a value or a promise, so a middleware wanting the
|
|
18
|
+
* outcome has to handle a throw and a rejection and rethrow both; getting it
|
|
19
|
+
* wrong silently swallows a failure. Extend this and implement {@link settled}
|
|
20
|
+
* instead - the result reaches the caller untouched, `settled` throwing
|
|
21
|
+
* included.
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* export class Reporter extends SocketObserver {
|
|
25
|
+
* override readonly reportsErrors = true;
|
|
26
|
+
*
|
|
27
|
+
* protected override settled(outcome: SocketOutcome, _f: SocketFrame, ctx: SocketContext): void {
|
|
28
|
+
* if (outcome.ok) return;
|
|
29
|
+
* this.logger.error('socket handler failed', { event: ctx.event, err: outcome.error });
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* One that answers or refuses a frame is not this: implement `SocketMiddleware`.
|
|
35
|
+
*/
|
|
36
|
+
export declare abstract class SocketObserver implements SocketMiddleware {
|
|
37
|
+
/**
|
|
38
|
+
* Override to `true` when {@link settled} reports a failure somewhere. See
|
|
39
|
+
* {@link SocketMiddleware.reportsErrors} - a middleware that ignores a throw
|
|
40
|
+
* and claims otherwise turns error reporting off for the whole server.
|
|
41
|
+
*/
|
|
42
|
+
readonly reportsErrors: boolean;
|
|
43
|
+
handle(frame: SocketFrame, ctx: SocketContext, next: SocketNext): unknown;
|
|
44
|
+
/** How the handler went, once per frame, on whichever channel it used. */
|
|
45
|
+
protected abstract settled(outcome: SocketOutcome, frame: SocketFrame, ctx: SocketContext): void;
|
|
46
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type AsyncModuleConfig, type Deps, type DynamicModule } from '@dunx/core';
|
|
2
2
|
import { type PostgresRelayOptions } from './postgres-relay.js';
|
|
3
3
|
import { type RedisRelayOptions } from './redis-relay.js';
|
|
4
|
+
import { type RelayPublisherInit } from './relay-publisher.js';
|
|
4
5
|
/**
|
|
5
6
|
* The Redis relay's connection settings, as a class so a factory can bind them.
|
|
6
7
|
*
|
|
@@ -48,19 +49,19 @@ export declare class PostgresRelayConnectionOptions {
|
|
|
48
49
|
*/
|
|
49
50
|
export declare class WsRelayModule {
|
|
50
51
|
/** Redis or Valkey, over `Bun.RedisClient`. */
|
|
51
|
-
static forRoot(init?: RedisRelayOptions): DynamicModule;
|
|
52
|
+
static forRoot(init?: RedisRelayOptions, publisher?: RelayPublisherInit): DynamicModule;
|
|
52
53
|
/**
|
|
53
54
|
* The same bindings with the settings behind a factory, so the url can come off
|
|
54
55
|
* `ConfigService`. `imports` reaches that factory; importing the module
|
|
55
56
|
* alongside does not, since a dynamic module is its own scope.
|
|
56
57
|
*/
|
|
57
|
-
static forRootAsync<const D extends Deps>(config: AsyncModuleConfig<RedisRelayOptions, D
|
|
58
|
+
static forRootAsync<const D extends Deps>(config: AsyncModuleConfig<RedisRelayOptions, D>, publisher?: RelayPublisherInit): DynamicModule;
|
|
58
59
|
/**
|
|
59
60
|
* Postgres, over `Bun.SQL`'s `LISTEN`/`NOTIFY`, for an app that already has a
|
|
60
61
|
* database and would rather not run a broker. A frame over about 7.9 KB is
|
|
61
62
|
* refused; see {@link PostgresRelay}.
|
|
62
63
|
*/
|
|
63
|
-
static forPostgres(init?: PostgresRelayOptions): DynamicModule;
|
|
64
|
+
static forPostgres(init?: PostgresRelayOptions, publisher?: RelayPublisherInit): DynamicModule;
|
|
64
65
|
/** `forPostgres` with the settings behind a factory. */
|
|
65
|
-
static forPostgresAsync<const D extends Deps>(config: AsyncModuleConfig<PostgresRelayOptions, D
|
|
66
|
+
static forPostgresAsync<const D extends Deps>(config: AsyncModuleConfig<PostgresRelayOptions, D>, publisher?: RelayPublisherInit): DynamicModule;
|
|
66
67
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { WsRelay, type RelayOptions } from './relay.js';
|
|
2
|
+
/** What a publisher needs beyond the relay itself. */
|
|
3
|
+
export interface RelayPublisherInit {
|
|
4
|
+
/**
|
|
5
|
+
* The broker channel, which **must** be the one the servers listen on
|
|
6
|
+
* (`HttpOptionsProvider.relayChannel`). A mismatch is silent - the broker
|
|
7
|
+
* accepts the publish and delivers it to nobody - so read both from one
|
|
8
|
+
* constant rather than writing the name twice.
|
|
9
|
+
*/
|
|
10
|
+
readonly channel?: string;
|
|
11
|
+
/** Reports a publish the broker refused. Defaults to a `console.warn`. */
|
|
12
|
+
readonly onError?: RelayOptions['onError'];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Publishes a socket frame from a process that has no server.
|
|
16
|
+
*
|
|
17
|
+
* `PubSub` needs a live `Bun.serve` for the local half of its fan-out, which a
|
|
18
|
+
* queue worker or a forked job child has none of. This is the other half alone:
|
|
19
|
+
* every frame goes to the broker and comes back out of the servers listening on
|
|
20
|
+
* the channel, with no local delivery and no subscribe.
|
|
21
|
+
*
|
|
22
|
+
* `WsRelayModule` binds it, so it is a constructor parameter:
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* export class RoundAnnouncer {
|
|
26
|
+
* constructor(private readonly frames: RelayPublisher) {}
|
|
27
|
+
*
|
|
28
|
+
* announce(round: string): void {
|
|
29
|
+
* this.frames.publishEvent('lobby', 'round.started', { round });
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare class RelayPublisher {
|
|
35
|
+
#private;
|
|
36
|
+
private readonly relay;
|
|
37
|
+
constructor(relay: WsRelay, init?: RelayPublisherInit);
|
|
38
|
+
/** This process's id on the relay channel. Stable for its lifetime. */
|
|
39
|
+
get origin(): string;
|
|
40
|
+
/** The channel frames go out on. */
|
|
41
|
+
get channel(): string;
|
|
42
|
+
/** The same envelope `@OnMessage(event)` reads, published to a topic. */
|
|
43
|
+
publishEvent(topic: string, event: string, data?: unknown): void;
|
|
44
|
+
/**
|
|
45
|
+
* **Never throws.** A job handler that failed here would be retried and would
|
|
46
|
+
* repeat its side effects to deliver a frame nobody awaited. Failures go to
|
|
47
|
+
* `onError`.
|
|
48
|
+
*/
|
|
49
|
+
publish(topic: string, data: string | Bun.BufferSource): void;
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dunx/http",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.3",
|
|
4
4
|
"description": "Bun.serve adapter for the dunx framework: controllers, middleware and WebSocket gateways",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"peerDependencies": {
|
|
77
77
|
"@bufbuild/protobuf": "^2.15.0",
|
|
78
78
|
"@connectrpc/connect": "^2.2.0",
|
|
79
|
-
"@dunx/core": "^3.8.
|
|
79
|
+
"@dunx/core": "^3.8.3",
|
|
80
80
|
"@types/bun": ">=1.4.1"
|
|
81
81
|
},
|
|
82
82
|
"peerDependenciesMeta": {
|