@dunx/http 3.8.0 → 3.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -32,10 +32,12 @@ export { Gateway, OnClose, OnDrain, OnMessage, OnOpen, OnPing, OnPong, OnUpgrade
32
32
  export type { Envelope } from './ws/envelope.js';
33
33
  export type { SocketContext, SocketDispatch, SocketFrame, SocketMiddleware, SocketNext, } from './ws/middleware.js';
34
34
  export { SocketLoggingMiddleware, type SocketLoggingOptions, } from './ws/logging.js';
35
+ export { SocketObserver, type SocketOutcome } from './ws/observer.js';
35
36
  export { PubSub } from './ws/pubsub.js';
36
37
  export { PostgresRelay, type PostgresRelayOptions, } from './ws/postgres-relay.js';
37
38
  export { RedisRelay, type RedisRelayOptions } from './ws/redis-relay.js';
38
39
  export { PostgresRelayConnectionOptions, RelayConnectionOptions, WsRelayModule, } from './ws/relay-module.js';
40
+ export { RelayPublisher, type RelayPublisherInit, } from './ws/relay-publisher.js';
39
41
  export { DEFAULT_RELAY_CHANNEL, WsRelay, type PubSubRelay, type RelayOptions, } from './ws/relay.js';
40
42
  export type { Socket, SocketData, SocketErrorHandler, SocketOptions, } from './ws/socket.js';
41
43
  export { HealthIndicator, PingProbe, QueryProbe, type ProbeResult, type ProbeState, } from './health/contracts.js';
package/dist/index.js CHANGED
@@ -299,19 +299,19 @@ var observe = (next, done) => {
299
299
  try {
300
300
  result = next();
301
301
  } catch (error) {
302
- done(error, undefined);
302
+ done(error, undefined, false);
303
303
  throw error;
304
304
  }
305
305
  if (result instanceof Promise) {
306
306
  return result.then((value) => {
307
- done(undefined, value);
307
+ done(undefined, value, true);
308
308
  return value;
309
309
  }, (error) => {
310
- done(error, undefined);
310
+ done(error, undefined, false);
311
311
  throw error;
312
312
  });
313
313
  }
314
- done(undefined, result);
314
+ done(undefined, result, true);
315
315
  return result;
316
316
  };
317
317
 
@@ -2671,6 +2671,19 @@ var OnMessage = (event) => (value) => {
2671
2671
  markHandler(value, { kind: HandlerKind.MESSAGE, event });
2672
2672
  return value;
2673
2673
  };
2674
+ // src/ws/observer.ts
2675
+ class SocketObserver {
2676
+ reportsErrors = false;
2677
+ handle(frame, ctx, next) {
2678
+ return observe(next, (error, value, ok) => {
2679
+ try {
2680
+ this.settled(ok ? { ok, value } : { ok, error }, frame, ctx);
2681
+ } catch (failure) {
2682
+ console.error(`[dunx/http] ${this.constructor.name}.settled threw, which cannot ` + "change the outcome it was watching:", failure);
2683
+ }
2684
+ });
2685
+ }
2686
+ }
2674
2687
  // src/ws/postgres-relay.ts
2675
2688
  var PROTOCOLS = ["postgres:", "postgresql:"];
2676
2689
  var defaultPostgresRelayUrl = () => process.env["POSTGRES_URL"] ?? process.env["DATABASE_URL"] ?? "postgres://localhost:5432";
@@ -2800,6 +2813,50 @@ Object.defineProperty(RedisRelay, Symbol.for("dunx.deps"), { value: () => [{ unr
2800
2813
  import {
2801
2814
  provide as provide5
2802
2815
  } from "@dunx/core";
2816
+
2817
+ // src/ws/relay-publisher.ts
2818
+ class RelayPublisher {
2819
+ relay;
2820
+ #origin = `worker:${Bun.randomUUIDv7()}`;
2821
+ #channel;
2822
+ #onError;
2823
+ constructor(relay, init = {}) {
2824
+ this.relay = relay;
2825
+ this.#channel = init.channel ?? DEFAULT_RELAY_CHANNEL;
2826
+ this.#onError = init.onError ?? defaultRelayError;
2827
+ }
2828
+ get origin() {
2829
+ return this.#origin;
2830
+ }
2831
+ get channel() {
2832
+ return this.#channel;
2833
+ }
2834
+ publishEvent(topic, event, data) {
2835
+ this.publish(topic, encode(event, data));
2836
+ }
2837
+ publish(topic, data) {
2838
+ try {
2839
+ const result = this.relay.publish(this.#channel, encodeRelay(this.#origin, topic, data));
2840
+ if (result instanceof Promise) {
2841
+ result.catch((error) => {
2842
+ this.#report(error);
2843
+ });
2844
+ }
2845
+ } catch (error) {
2846
+ this.#report(error);
2847
+ }
2848
+ }
2849
+ #report(error) {
2850
+ try {
2851
+ this.#onError(error, "publish");
2852
+ } catch (failure) {
2853
+ console.error("[dunx/http] a RelayPublisher onError handler threw:", failure);
2854
+ }
2855
+ }
2856
+ }
2857
+ Object.defineProperty(RelayPublisher, Symbol.for("dunx.deps"), { value: () => [WsRelay, { unresolved: "init: RelayPublisherInit = {}", optional: true }] });
2858
+
2859
+ // src/ws/relay-module.ts
2803
2860
  class RelayConnectionOptions {
2804
2861
  url;
2805
2862
  maxRetries;
@@ -2850,7 +2907,12 @@ class RelayLifecycle {
2850
2907
  }
2851
2908
  }
2852
2909
  Object.defineProperty(RelayLifecycle, Symbol.for("dunx.deps"), { value: () => [WsRelay] });
2853
- var redisBindings = (options) => [
2910
+ var publisherBinding = (init) => provide5(RelayPublisher, {
2911
+ useFactory: (relay) => new RelayPublisher(relay, init),
2912
+ inject: [WsRelay]
2913
+ });
2914
+ var redisBindings = (options, publisher) => [
2915
+ publisherBinding(publisher),
2854
2916
  provide5(RelayConnectionOptions, options),
2855
2917
  provide5(RedisRelay, {
2856
2918
  useFactory: (settings) => new RedisRelay(settings.toInit()),
@@ -2865,7 +2927,8 @@ var redisBindings = (options) => [
2865
2927
  inject: [WsRelay]
2866
2928
  })
2867
2929
  ];
2868
- var postgresBindings = (options) => [
2930
+ var postgresBindings = (options, publisher) => [
2931
+ publisherBinding(publisher),
2869
2932
  provide5(PostgresRelayConnectionOptions, options),
2870
2933
  provide5(PostgresRelay, {
2871
2934
  useFactory: (settings) => new PostgresRelay(settings.toInit()),
@@ -2882,48 +2945,58 @@ var postgresBindings = (options) => [
2882
2945
  ];
2883
2946
 
2884
2947
  class WsRelayModule {
2885
- static forRoot(init = {}) {
2948
+ static forRoot(init = {}, publisher = {}) {
2886
2949
  return {
2887
2950
  module: WsRelayModule,
2888
- exports: [WsRelay, RedisRelay, RelayConnectionOptions],
2951
+ exports: [WsRelay, RedisRelay, RelayConnectionOptions, RelayPublisher],
2889
2952
  providers: redisBindings({
2890
2953
  useFactory: () => new RelayConnectionOptions(init),
2891
2954
  inject: []
2892
- })
2955
+ }, publisher)
2893
2956
  };
2894
2957
  }
2895
- static forRootAsync(config) {
2958
+ static forRootAsync(config, publisher = {}) {
2896
2959
  const useFactory = async (...deps) => new RelayConnectionOptions(await config.useFactory(...deps));
2897
2960
  return {
2898
2961
  module: WsRelayModule,
2899
2962
  ...config.imports === undefined ? {} : { imports: config.imports },
2900
- exports: [WsRelay, RedisRelay, RelayConnectionOptions],
2963
+ exports: [WsRelay, RedisRelay, RelayConnectionOptions, RelayPublisher],
2901
2964
  providers: redisBindings({
2902
2965
  useFactory,
2903
2966
  inject: config.inject ?? []
2904
- })
2967
+ }, publisher)
2905
2968
  };
2906
2969
  }
2907
- static forPostgres(init = {}) {
2970
+ static forPostgres(init = {}, publisher = {}) {
2908
2971
  return {
2909
2972
  module: WsRelayModule,
2910
- exports: [WsRelay, PostgresRelay, PostgresRelayConnectionOptions],
2973
+ exports: [
2974
+ WsRelay,
2975
+ PostgresRelay,
2976
+ PostgresRelayConnectionOptions,
2977
+ RelayPublisher
2978
+ ],
2911
2979
  providers: postgresBindings({
2912
2980
  useFactory: () => new PostgresRelayConnectionOptions(init),
2913
2981
  inject: []
2914
- })
2982
+ }, publisher)
2915
2983
  };
2916
2984
  }
2917
- static forPostgresAsync(config) {
2985
+ static forPostgresAsync(config, publisher = {}) {
2918
2986
  const useFactory = async (...deps) => new PostgresRelayConnectionOptions(await config.useFactory(...deps));
2919
2987
  return {
2920
2988
  module: WsRelayModule,
2921
2989
  ...config.imports === undefined ? {} : { imports: config.imports },
2922
- exports: [WsRelay, PostgresRelay, PostgresRelayConnectionOptions],
2990
+ exports: [
2991
+ WsRelay,
2992
+ PostgresRelay,
2993
+ PostgresRelayConnectionOptions,
2994
+ RelayPublisher
2995
+ ],
2923
2996
  providers: postgresBindings({
2924
2997
  useFactory,
2925
2998
  inject: config.inject ?? []
2926
- })
2999
+ }, publisher)
2927
3000
  };
2928
3001
  }
2929
3002
  }
@@ -3375,6 +3448,7 @@ export {
3375
3448
  RedisRelay,
3376
3449
  RedisThrottleStore,
3377
3450
  RelayConnectionOptions,
3451
+ RelayPublisher,
3378
3452
  RequestLoggingMiddleware,
3379
3453
  RequestMetrics,
3380
3454
  Roles2 as Roles,
@@ -3382,6 +3456,7 @@ export {
3382
3456
  STREAMS2 as STREAMS,
3383
3457
  SkipThrottle,
3384
3458
  SocketLoggingMiddleware,
3459
+ SocketObserver,
3385
3460
  Sse,
3386
3461
  SseStream,
3387
3462
  StaticFiles2 as StaticFiles,
@@ -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
- * `error` is `undefined` on success. A synchronous throw and a rejection both
83
- * reach `done` and are then rethrown, so a middleware that only observes cannot
84
- * accidentally swallow a failure.
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>): DynamicModule;
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>): DynamicModule;
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.0",
3
+ "version": "3.8.2",
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.0",
79
+ "@dunx/core": "^3.8.2",
80
80
  "@types/bun": ">=1.4.1"
81
81
  },
82
82
  "peerDependenciesMeta": {