@dunx/http 3.1.0 → 3.1.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.
@@ -534,6 +534,7 @@ var discoverGateways = (modules, resolve) => {
534
534
  };
535
535
 
536
536
  // src/ws/relay.ts
537
+ import { AppError as AppError4 } from "@dunx/core";
537
538
  var DEFAULT_RELAY_CHANNEL = "dunx:ws";
538
539
  var defaultRelayError = (error, phase) => {
539
540
  console.warn(`[dunx/http] the websocket relay could not ${phase}. Fan-out is local to ` + "this process until it recovers:", error);
@@ -561,6 +562,14 @@ var decodeRelay = (message) => {
561
562
  return { origin: o, topic: t, data: b ? Buffer.from(d, "base64") : d };
562
563
  };
563
564
 
565
+ class WsRelay {
566
+ constructor() {
567
+ if (new.target === WsRelay) {
568
+ throw new AppError4("WsRelay is a contract, not an implementation. Bind one with " + "WsRelayModule.forRoot() for Redis or WsRelayModule.forPostgres() " + "for Postgres, or extend it with a relay of your own.");
569
+ }
570
+ }
571
+ }
572
+
564
573
  // src/server/request-id.ts
565
574
  var REQUEST_ID_HEADER = "x-request-id";
566
575
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -651,7 +660,7 @@ var preflight = (options, methods) => {
651
660
  var compose = (middleware, ctx, handler) => middleware.reduceRight((next, current) => (req) => current.handle(req, ctx, () => next(req)), handler);
652
661
 
653
662
  // src/server/routes.ts
654
- import { AppError as AppError4 } from "@dunx/core";
663
+ import { AppError as AppError5 } from "@dunx/core";
655
664
 
656
665
  // src/server/raw-body.ts
657
666
  var WANTED = Symbol.for("dunx.http.rawBody.wanted");
@@ -793,7 +802,7 @@ var assertNoCollisions = (discovered) => {
793
802
  const owner = `${route.controller}.${route.handlerName}`;
794
803
  const existing = owners.get(key);
795
804
  if (existing !== undefined) {
796
- throw new AppError4(`Route collision: ${key} is declared by ${existing} and by ${owner}. ` + "Bun would keep only one of them.");
805
+ throw new AppError5(`Route collision: ${key} is declared by ${existing} and by ${owner}. ` + "Bun would keep only one of them.");
797
806
  }
798
807
  owners.set(key, owner);
799
808
  }
@@ -802,7 +811,7 @@ var assertNoGatewayCollisions = (discovered, gatewayPaths) => {
802
811
  const gateways = new Set(gatewayPaths);
803
812
  for (const route of discovered) {
804
813
  if (gateways.has(route.path)) {
805
- throw new AppError4(`Gateway path collision: ${route.path} is served by a gateway and by ` + `${route.controller}.${route.handlerName}(). The upgrade is a route too, ` + "so one of them would be dropped.");
814
+ throw new AppError5(`Gateway path collision: ${route.path} is served by a gateway and by ` + `${route.controller}.${route.handlerName}(). The upgrade is a route too, ` + "so one of them would be dropped.");
806
815
  }
807
816
  }
808
817
  };
@@ -1018,7 +1027,7 @@ class CompressionOptions {
1018
1027
  Object.defineProperty(CompressionOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: CompressionOptionsInit = {}", optional: true }] });
1019
1028
 
1020
1029
  // src/ws/redis-relay.ts
1021
- import { AppError as AppError5 } from "@dunx/core";
1030
+ import { AppError as AppError6 } from "@dunx/core";
1022
1031
  var PROTOCOLS = [
1023
1032
  "redis:",
1024
1033
  "rediss:",
@@ -1034,21 +1043,22 @@ var assertUrl = (url) => {
1034
1043
  try {
1035
1044
  parsed = new URL(url);
1036
1045
  } catch {
1037
- throw new AppError5(`${JSON.stringify(url)} is not a valid URL for the websocket relay. ` + "Expected something like redis://localhost:6379.");
1046
+ throw new AppError6(`${JSON.stringify(url)} is not a valid URL for the websocket relay. ` + "Expected something like redis://localhost:6379.");
1038
1047
  }
1039
1048
  if (!PROTOCOLS.includes(parsed.protocol)) {
1040
- throw new AppError5(`Unsupported protocol ${JSON.stringify(parsed.protocol)} in ` + `${JSON.stringify(url)}. Expected one of ${PROTOCOLS.join(", ")}.`);
1049
+ throw new AppError6(`Unsupported protocol ${JSON.stringify(parsed.protocol)} in ` + `${JSON.stringify(url)}. Expected one of ${PROTOCOLS.join(", ")}.`);
1041
1050
  }
1042
1051
  return url;
1043
1052
  };
1044
1053
 
1045
- class RedisRelay {
1054
+ class RedisRelay extends WsRelay {
1046
1055
  #url;
1047
1056
  #options;
1048
1057
  #pub;
1049
1058
  #sub;
1050
1059
  #channel;
1051
1060
  constructor(options = {}) {
1061
+ super();
1052
1062
  this.#url = assertUrl(options.url ?? defaultRelayUrl());
1053
1063
  this.#options = {
1054
1064
  maxRetries: options.maxRetries ?? 0,
@@ -1303,4 +1313,4 @@ __runInitializers(_init, 1, HiddenHealthController);
1303
1313
  __decoratorMetadata(_init, HiddenHealthController);
1304
1314
  let _HiddenHealthController = HiddenHealthController;
1305
1315
 
1306
- export { defaultStatusFor, Controller, Get, Post, Put, Patch, Delete, metaKey, meta, ROLES, PUBLIC, HIDDEN, UNMATCHED, Roles, Public, ApiHidden, UseGuards, guardsOf, metaOf, mergeMeta, HttpError, ValidationError, ErrorFilter, isErrorFilter, toErrorMapper, errorMapper, defaultErrorMapper, joinPath, discoverRoutes, encode, decode, HandlerKind, markHandler, markGateway, isGateway, composeSocket, observe, buildRuntime, buildGateways, buildWebSocket, normalizePath, discoverGateway, discoverGateways, DEFAULT_RELAY_CHANNEL, defaultRelayError, encodeRelay, decodeRelay, RawBody, REQUEST_ID_HEADER, RequestIds, buildContext, withCors, preflight, compose, assertNoCollisions, assertNoGatewayCollisions, withUpgradeRoutes, buildFallback, buildRoutes, StaticOptions, normalizePrefix, negotiate, CompressionEncoding, isCompressibleType, CompressionOptions, defaultRelayUrl, RedisRelay, HEALTH_REPORT_SCHEMA, HealthOptions, HealthRegistry, HealthController, HiddenHealthController };
1316
+ export { defaultStatusFor, Controller, Get, Post, Put, Patch, Delete, metaKey, meta, ROLES, PUBLIC, HIDDEN, UNMATCHED, Roles, Public, ApiHidden, UseGuards, guardsOf, metaOf, mergeMeta, HttpError, ValidationError, ErrorFilter, isErrorFilter, toErrorMapper, errorMapper, defaultErrorMapper, joinPath, discoverRoutes, encode, decode, HandlerKind, markHandler, markGateway, isGateway, composeSocket, observe, buildRuntime, buildGateways, buildWebSocket, normalizePath, discoverGateway, discoverGateways, DEFAULT_RELAY_CHANNEL, defaultRelayError, encodeRelay, decodeRelay, WsRelay, RawBody, REQUEST_ID_HEADER, RequestIds, buildContext, withCors, preflight, compose, assertNoCollisions, assertNoGatewayCollisions, withUpgradeRoutes, buildFallback, buildRoutes, StaticOptions, normalizePrefix, negotiate, CompressionEncoding, isCompressibleType, CompressionOptions, defaultRelayUrl, RedisRelay, HEALTH_REPORT_SCHEMA, HealthOptions, HealthRegistry, HealthController, HiddenHealthController };
package/dist/index.d.ts CHANGED
@@ -30,9 +30,10 @@ export type { Envelope } from './ws/envelope.js';
30
30
  export type { SocketContext, SocketDispatch, SocketFrame, SocketMiddleware, SocketNext, } from './ws/middleware.js';
31
31
  export { SocketLoggingMiddleware, type SocketLoggingOptions, } from './ws/logging.js';
32
32
  export { PubSub } from './ws/pubsub.js';
33
+ export { PostgresRelay, type PostgresRelayOptions, } from './ws/postgres-relay.js';
33
34
  export { RedisRelay, type RedisRelayOptions } from './ws/redis-relay.js';
34
- export { RelayConnectionOptions, WsRelayModule } from './ws/relay-module.js';
35
- export { DEFAULT_RELAY_CHANNEL, type PubSubRelay, type RelayOptions, } from './ws/relay.js';
35
+ export { PostgresRelayConnectionOptions, RelayConnectionOptions, WsRelayModule, } from './ws/relay-module.js';
36
+ export { DEFAULT_RELAY_CHANNEL, WsRelay, type PubSubRelay, type RelayOptions, } from './ws/relay.js';
36
37
  export type { Socket, SocketData, SocketErrorHandler, SocketOptions, } from './ws/socket.js';
37
38
  export { HealthIndicator, PingProbe, QueryProbe, type ProbeResult, type ProbeState, } from './health/contracts.js';
38
39
  export { HealthController } from './health/controller.js';
package/dist/index.js CHANGED
@@ -36,6 +36,7 @@ import {
36
36
  UNMATCHED,
37
37
  UseGuards,
38
38
  ValidationError,
39
+ WsRelay,
39
40
  assertNoCollisions,
40
41
  assertNoGatewayCollisions,
41
42
  buildFallback,
@@ -60,7 +61,7 @@ import {
60
61
  observe,
61
62
  toErrorMapper,
62
63
  withUpgradeRoutes
63
- } from "./chunk-3xs8zrpv.js";
64
+ } from "./chunk-53cs6qek.js";
64
65
  import {
65
66
  HttpStatusCode,
66
67
  __decorateElement,
@@ -1396,6 +1397,71 @@ var OnMessage = (event) => (value) => {
1396
1397
  markHandler(value, { kind: HandlerKind.MESSAGE, event });
1397
1398
  return value;
1398
1399
  };
1400
+ // src/ws/postgres-relay.ts
1401
+ import { AppError as AppError7 } from "@dunx/core";
1402
+ var PROTOCOLS = ["postgres:", "postgresql:"];
1403
+ var defaultPostgresRelayUrl = () => process.env["POSTGRES_URL"] ?? process.env["DATABASE_URL"] ?? "postgres://localhost:5432";
1404
+ var assertUrl = (url) => {
1405
+ let parsed;
1406
+ try {
1407
+ parsed = new URL(url);
1408
+ } catch {
1409
+ throw new AppError7(`${JSON.stringify(url)} is not a valid URL for the websocket relay. ` + "Expected something like postgres://localhost:5432/app.");
1410
+ }
1411
+ if (!PROTOCOLS.includes(parsed.protocol)) {
1412
+ throw new AppError7(`Unsupported protocol ${JSON.stringify(parsed.protocol)} in ` + `${JSON.stringify(url)}. Expected one of ${PROTOCOLS.join(", ")}.`);
1413
+ }
1414
+ return url;
1415
+ };
1416
+
1417
+ class PostgresRelay extends WsRelay {
1418
+ #url;
1419
+ #max;
1420
+ #sql;
1421
+ #subscription;
1422
+ constructor(options = {}) {
1423
+ super();
1424
+ this.#url = assertUrl(options.url ?? defaultPostgresRelayUrl());
1425
+ this.#max = options.max ?? 1;
1426
+ }
1427
+ get url() {
1428
+ const parsed = new URL(this.#url);
1429
+ if (parsed.password)
1430
+ parsed.password = "***";
1431
+ return parsed.toString();
1432
+ }
1433
+ #client() {
1434
+ return this.#sql ??= new Bun.SQL({ url: this.#url, max: this.#max });
1435
+ }
1436
+ async publish(channel, message) {
1437
+ await this.#client().notify(channel, message);
1438
+ }
1439
+ async subscribe(channel, listener) {
1440
+ const sql = this.#client();
1441
+ try {
1442
+ this.#subscription = await sql.listen(channel, listener);
1443
+ } catch (error) {
1444
+ if (this.#sql === sql) {
1445
+ this.#sql = undefined;
1446
+ await sql.close();
1447
+ }
1448
+ throw error;
1449
+ }
1450
+ }
1451
+ async close() {
1452
+ const sql = this.#sql;
1453
+ const subscription = this.#subscription;
1454
+ this.#sql = undefined;
1455
+ this.#subscription = undefined;
1456
+ if (subscription) {
1457
+ try {
1458
+ await subscription.unlisten();
1459
+ } catch {}
1460
+ }
1461
+ await sql?.close();
1462
+ }
1463
+ }
1464
+ Object.defineProperty(PostgresRelay, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "options: PostgresRelayOptions = {}", optional: true }] });
1399
1465
  // src/ws/relay-module.ts
1400
1466
  import {
1401
1467
  provide as provide5
@@ -1424,6 +1490,22 @@ class RelayConnectionOptions {
1424
1490
  }
1425
1491
  Object.defineProperty(RelayConnectionOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: RedisRelayOptions = {}", optional: true, typeOnly: "RedisRelayOptions" }] });
1426
1492
 
1493
+ class PostgresRelayConnectionOptions {
1494
+ url;
1495
+ max;
1496
+ constructor(init = {}) {
1497
+ this.url = init.url;
1498
+ this.max = init.max;
1499
+ }
1500
+ toInit() {
1501
+ return {
1502
+ ...this.url !== undefined && { url: this.url },
1503
+ ...this.max !== undefined && { max: this.max }
1504
+ };
1505
+ }
1506
+ }
1507
+ Object.defineProperty(PostgresRelayConnectionOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: PostgresRelayOptions = {}", optional: true, typeOnly: "PostgresRelayOptions" }] });
1508
+
1427
1509
  class RelayLifecycle {
1428
1510
  relay;
1429
1511
  constructor(relay) {
@@ -1433,16 +1515,35 @@ class RelayLifecycle {
1433
1515
  await this.relay.close();
1434
1516
  }
1435
1517
  }
1436
- Object.defineProperty(RelayLifecycle, Symbol.for("dunx.deps"), { value: () => [RedisRelay] });
1437
- var bindings = (options) => [
1518
+ Object.defineProperty(RelayLifecycle, Symbol.for("dunx.deps"), { value: () => [WsRelay] });
1519
+ var redisBindings = (options) => [
1438
1520
  provide5(RelayConnectionOptions, options),
1439
1521
  provide5(RedisRelay, {
1440
1522
  useFactory: (settings) => new RedisRelay(settings.toInit()),
1441
1523
  inject: [RelayConnectionOptions]
1442
1524
  }),
1525
+ provide5(WsRelay, {
1526
+ useFactory: (relay) => relay,
1527
+ inject: [RedisRelay]
1528
+ }),
1443
1529
  provide5(RelayLifecycle, {
1444
1530
  useFactory: (relay) => new RelayLifecycle(relay),
1445
- inject: [RedisRelay]
1531
+ inject: [WsRelay]
1532
+ })
1533
+ ];
1534
+ var postgresBindings = (options) => [
1535
+ provide5(PostgresRelayConnectionOptions, options),
1536
+ provide5(PostgresRelay, {
1537
+ useFactory: (settings) => new PostgresRelay(settings.toInit()),
1538
+ inject: [PostgresRelayConnectionOptions]
1539
+ }),
1540
+ provide5(WsRelay, {
1541
+ useFactory: (relay) => relay,
1542
+ inject: [PostgresRelay]
1543
+ }),
1544
+ provide5(RelayLifecycle, {
1545
+ useFactory: (relay) => new RelayLifecycle(relay),
1546
+ inject: [WsRelay]
1446
1547
  })
1447
1548
  ];
1448
1549
 
@@ -1450,8 +1551,8 @@ class WsRelayModule {
1450
1551
  static forRoot(init = {}) {
1451
1552
  return {
1452
1553
  module: WsRelayModule,
1453
- exports: [RedisRelay, RelayConnectionOptions],
1454
- providers: bindings({
1554
+ exports: [WsRelay, RedisRelay, RelayConnectionOptions],
1555
+ providers: redisBindings({
1455
1556
  useFactory: () => new RelayConnectionOptions(init),
1456
1557
  inject: []
1457
1558
  })
@@ -1462,8 +1563,30 @@ class WsRelayModule {
1462
1563
  return {
1463
1564
  module: WsRelayModule,
1464
1565
  ...config.imports === undefined ? {} : { imports: config.imports },
1465
- exports: [RedisRelay, RelayConnectionOptions],
1466
- providers: bindings({
1566
+ exports: [WsRelay, RedisRelay, RelayConnectionOptions],
1567
+ providers: redisBindings({
1568
+ useFactory,
1569
+ inject: config.inject ?? []
1570
+ })
1571
+ };
1572
+ }
1573
+ static forPostgres(init = {}) {
1574
+ return {
1575
+ module: WsRelayModule,
1576
+ exports: [WsRelay, PostgresRelay, PostgresRelayConnectionOptions],
1577
+ providers: postgresBindings({
1578
+ useFactory: () => new PostgresRelayConnectionOptions(init),
1579
+ inject: []
1580
+ })
1581
+ };
1582
+ }
1583
+ static forPostgresAsync(config) {
1584
+ const useFactory = async (...deps) => new PostgresRelayConnectionOptions(await config.useFactory(...deps));
1585
+ return {
1586
+ module: WsRelayModule,
1587
+ ...config.imports === undefined ? {} : { imports: config.imports },
1588
+ exports: [WsRelay, PostgresRelay, PostgresRelayConnectionOptions],
1589
+ providers: postgresBindings({
1467
1590
  useFactory,
1468
1591
  inject: config.inject ?? []
1469
1592
  })
@@ -1707,6 +1830,8 @@ export {
1707
1830
  Patch,
1708
1831
  PingProbe,
1709
1832
  Post,
1833
+ PostgresRelay,
1834
+ PostgresRelayConnectionOptions,
1710
1835
  PubSub,
1711
1836
  Public,
1712
1837
  Put,
@@ -1739,6 +1864,7 @@ export {
1739
1864
  UNMATCHED,
1740
1865
  UseGuards,
1741
1866
  ValidationError,
1867
+ WsRelay,
1742
1868
  WsRelayModule,
1743
1869
  defaultErrorMapper,
1744
1870
  errorMapper,
package/dist/internal.js CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  toErrorMapper,
37
37
  withCors,
38
38
  withUpgradeRoutes
39
- } from "./chunk-3xs8zrpv.js";
39
+ } from "./chunk-53cs6qek.js";
40
40
  import {
41
41
  backoffDelay,
42
42
  executeWithRetry,
@@ -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 {};
@@ -0,0 +1,38 @@
1
+ import { WsRelay } from './relay.js';
2
+ /** The same fallback chain `Bun.SQL` uses when given no URL. */
3
+ export declare const defaultPostgresRelayUrl: () => string;
4
+ export interface PostgresRelayOptions {
5
+ /** @default `$POSTGRES_URL`, `$DATABASE_URL`, then `postgres://localhost:5432` */
6
+ readonly url?: string;
7
+ /**
8
+ * Size of the query pool `notify` publishes through. **`subscribe` opens a
9
+ * dedicated connection on top of it**, so a relay that is listening holds up to
10
+ * `max + 1`. Measured: `max: 1` shows two rows in `pg_stat_activity`.
11
+ *
12
+ * @default 1
13
+ */
14
+ readonly max?: number;
15
+ }
16
+ /**
17
+ * A {@link WsRelay} on `Bun.SQL`'s `LISTEN`/`NOTIFY`, a Bun global, so it costs
18
+ * `@dunx/http` no dependency and an app already on Postgres needs no broker.
19
+ *
20
+ * One client, two connections while it listens: Bun dedicates one to the `LISTEN`
21
+ * and leaves the pool to answer `notify`. Budget `max + 1` per replica.
22
+ *
23
+ * **A frame over about 7.9 KB is refused**, because Postgres caps a `NOTIFY`
24
+ * payload at 7999 bytes and the relay envelope adds to the frame. `PubSub` reports
25
+ * one `logger.warn` and fan-out stays local for that message. Redis has no
26
+ * comparable ceiling; measured in docs/architecture/http.md.
27
+ *
28
+ * Reconnection is Bun's, so there is no retry budget here.
29
+ */
30
+ export declare class PostgresRelay extends WsRelay {
31
+ #private;
32
+ constructor(options?: PostgresRelayOptions);
33
+ /** The URL with any password removed, for logs and error messages. */
34
+ get url(): string;
35
+ publish(channel: string, message: string): Promise<void>;
36
+ subscribe(channel: string, listener: (message: string) => void): Promise<void>;
37
+ close(): Promise<void>;
38
+ }
@@ -1,4 +1,4 @@
1
- import type { PubSubRelay } from './relay.js';
1
+ import { WsRelay } from './relay.js';
2
2
  /** The same fallback chain `Bun.RedisClient` uses when given no URL. */
3
3
  export declare const defaultRelayUrl: () => string;
4
4
  export interface RedisRelayOptions {
@@ -18,14 +18,14 @@ export interface RedisRelayOptions {
18
18
  readonly tls?: boolean | Bun.TLSOptions;
19
19
  }
20
20
  /**
21
- * A {@link PubSubRelay} on `Bun.RedisClient`, a Bun global, so it costs
21
+ * A {@link WsRelay} on `Bun.RedisClient`, a Bun global, so it costs
22
22
  * `@dunx/http` no dependency.
23
23
  *
24
24
  * Two connections: a client in subscriber mode rejects every data command, so the
25
25
  * subscription cannot share the publishing socket. Both open lazily, and a failed
26
26
  * one is discarded rather than reused.
27
27
  */
28
- export declare class RedisRelay implements PubSubRelay {
28
+ export declare class RedisRelay extends WsRelay {
29
29
  #private;
30
30
  constructor(options?: RedisRelayOptions);
31
31
  /** The URL with any password removed, for logs and error messages. */
@@ -1,7 +1,8 @@
1
1
  import { type AsyncModuleConfig, type Deps, type DynamicModule } from '@dunx/core';
2
+ import { type PostgresRelayOptions } from './postgres-relay.js';
2
3
  import { type RedisRelayOptions } from './redis-relay.js';
3
4
  /**
4
- * The relay's connection settings, as a class so a factory can bind them.
5
+ * The Redis relay's connection settings, as a class so a factory can bind them.
5
6
  *
6
7
  * `RedisRelayOptions` stays the interface a caller writes; this is what the
7
8
  * container holds, which is the same split `HttpClientOptions` and
@@ -16,15 +17,22 @@ export declare class RelayConnectionOptions {
16
17
  /** Only the keys actually set, so each `RedisRelay` default still applies. */
17
18
  toInit(): RedisRelayOptions;
18
19
  }
20
+ /** The same, for the Postgres relay. */
21
+ export declare class PostgresRelayConnectionOptions {
22
+ readonly url: string | undefined;
23
+ readonly max: number | undefined;
24
+ constructor(init?: PostgresRelayOptions);
25
+ toInit(): PostgresRelayOptions;
26
+ }
19
27
  /**
20
28
  * Binds the websocket relay, so `relay` is a provider rather than an instance
21
29
  * `main.ts` constructs and threads into `HttpFactory.create`.
22
30
  *
23
- * `RedisRelay` is a class, so an options provider takes it as a parameter:
31
+ * Name {@link WsRelay} at the injection site and the backend is a wiring choice:
24
32
  *
25
33
  * ```ts
26
34
  * export class AppHttpOptions extends HttpOptionsProvider {
27
- * constructor(private readonly bus: RedisRelay) {
35
+ * constructor(private readonly bus: WsRelay) {
28
36
  * super();
29
37
  * }
30
38
  *
@@ -34,11 +42,12 @@ export declare class RelayConnectionOptions {
34
42
  * }
35
43
  * ```
36
44
  *
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.
45
+ * The backend is chosen by which method you call, not by a field in the options.
46
+ * A relay of your own needs no module: extend `WsRelay`, bind it, and return it
47
+ * from that same getter.
40
48
  */
41
49
  export declare class WsRelayModule {
50
+ /** Redis or Valkey, over `Bun.RedisClient`. */
42
51
  static forRoot(init?: RedisRelayOptions): DynamicModule;
43
52
  /**
44
53
  * The same bindings with the settings behind a factory, so the url can come off
@@ -46,4 +55,12 @@ export declare class WsRelayModule {
46
55
  * alongside does not, since a dynamic module is its own scope.
47
56
  */
48
57
  static forRootAsync<const D extends Deps>(config: AsyncModuleConfig<RedisRelayOptions, D>): DynamicModule;
58
+ /**
59
+ * Postgres, over `Bun.SQL`'s `LISTEN`/`NOTIFY`, for an app that already has a
60
+ * database and would rather not run a broker. A frame over about 7.9 KB is
61
+ * refused; see {@link PostgresRelay}.
62
+ */
63
+ static forPostgres(init?: PostgresRelayOptions): DynamicModule;
64
+ /** `forPostgres` with the settings behind a factory. */
65
+ static forPostgresAsync<const D extends Deps>(config: AsyncModuleConfig<PostgresRelayOptions, D>): DynamicModule;
49
66
  }
@@ -81,3 +81,18 @@ export interface RelayFrame {
81
81
  export declare const encodeRelay: (origin: string, topic: string, data: string | Bun.BufferSource) => string;
82
82
  /** `undefined` for anything that is not one of our frames, which is then ignored. */
83
83
  export declare const decodeRelay: (message: string) => RelayFrame | undefined;
84
+ /**
85
+ * The injectable form of {@link PubSubRelay}. An interface has no runtime value
86
+ * for the container to record, so a module binds this and a consumer that wants
87
+ * the relay names this rather than `RedisRelay` or `PostgresRelay` - which is
88
+ * what lets the backend change without the code that publishes changing with it.
89
+ *
90
+ * `close` is abstract here though it is optional on the interface: a relay the
91
+ * container built is the container's to close.
92
+ */
93
+ export declare abstract class WsRelay implements PubSubRelay {
94
+ constructor();
95
+ abstract publish(channel: string, message: string): unknown;
96
+ abstract subscribe(channel: string, listener: (message: string) => void): unknown;
97
+ abstract close(): unknown;
98
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dunx/http",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
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.1.0",
65
+ "@dunx/core": "^3.1.2",
66
66
  "@types/bun": ">=1.3.0"
67
67
  },
68
68
  "peerDependenciesMeta": {