@dunx/http 0.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.
@@ -0,0 +1,83 @@
1
+ /**
2
+ * What `PubSub` needs from something that carries a message to the other nodes:
3
+ * publish, and subscribe. Nothing else, so anything that already talks to a
4
+ * broker satisfies it — `@dunx/infra/redis`'s `RedisConnection` does, structurally
5
+ * and with no adapter, and so does a bare `Bun.RedisClient` pair.
6
+ *
7
+ * The return types are `unknown` rather than `Promise<void>` deliberately: Bun's
8
+ * `publish` resolves the subscriber count, `@dunx/infra`'s resolves nothing, and a
9
+ * synchronous in-memory bus resolves at all. A returned promise is awaited by
10
+ * `subscribe` and watched for rejection by `publish`; anything else is taken as
11
+ * having succeeded.
12
+ */
13
+ export interface PubSubRelay {
14
+ /** Hand `message` to every node subscribed to `channel`, this one included. */
15
+ publish(channel: string, message: string): unknown;
16
+ /**
17
+ * Deliver every message published to `channel` to `listener`. Called once, with
18
+ * one channel — pattern subscription is not used, because Bun's `psubscribe`
19
+ * does not work (see docs/bun-apis.md).
20
+ */
21
+ subscribe(channel: string, listener: (message: string) => void): unknown;
22
+ /**
23
+ * Release whatever this relay opened. Implement it only for connections the
24
+ * relay itself owns: a relay that is the application's own shared
25
+ * `RedisConnection` must leave closing to the container, and simply omitting
26
+ * this method is how it says so.
27
+ */
28
+ close?(): unknown;
29
+ }
30
+ /** Which relay call failed, so one message can say what degraded. */
31
+ export type RelayPhase = 'publish' | 'subscribe' | 'close';
32
+ export interface RelayOptions {
33
+ /**
34
+ * The one broker channel every topic's frames travel on.
35
+ *
36
+ * One channel rather than one per topic, because a node cannot know which
37
+ * topics its sockets joined — `socket.subscribe()` goes straight into Bun — and
38
+ * `psubscribe` is unusable. The cost is that every node reads every relayed
39
+ * frame and drops the ones for topics it has no local subscriber on, which is a
40
+ * `server.publish` returning `0`. Two apps sharing a Redis need two channels.
41
+ *
42
+ * @default 'dunx:ws'
43
+ */
44
+ readonly channel?: string;
45
+ /**
46
+ * Where a relay failure goes. Called once when the relay starts failing and not
47
+ * again until it works, so an unreachable broker cannot flood the log.
48
+ *
49
+ * @default console.warn
50
+ */
51
+ readonly onError?: (error: unknown, phase: RelayPhase) => void;
52
+ /**
53
+ * What to do when the **boot** subscribe fails. Publishing recovers on its own —
54
+ * every publish retries the broker — but a failed subscribe used to be retried
55
+ * by nothing, so the node stayed permanently deaf to other nodes while still
56
+ * looking healthy.
57
+ *
58
+ * Bounded rather than infinite, and the timer is unref'd, so a broker that never
59
+ * comes back cannot hold the process open or spin forever.
60
+ */
61
+ readonly resubscribe?: {
62
+ /** Retries after the first failure. `0` disables them. @default 5 */
63
+ readonly attempts?: number;
64
+ /** First delay; doubles each attempt, capped at 30s. @default 500 */
65
+ readonly delayMs?: number;
66
+ };
67
+ }
68
+ export declare const DEFAULT_RELAY_CHANNEL = "dunx:ws";
69
+ export declare const defaultRelayError: (error: unknown, phase: RelayPhase) => void;
70
+ /**
71
+ * One relayed publish: which process published it, which topic it belongs to, and
72
+ * the frame itself. `origin` is the whole duplicate-delivery defence — the broker
73
+ * echoes a publish back to the publisher, and fanning that out locally a second
74
+ * time would give every client on the originating node the message twice.
75
+ */
76
+ export interface RelayFrame {
77
+ readonly origin: string;
78
+ readonly topic: string;
79
+ readonly data: string | Uint8Array<ArrayBufferLike>;
80
+ }
81
+ export declare const encodeRelay: (origin: string, topic: string, data: string | Bun.BufferSource) => string;
82
+ /** `undefined` for anything that is not one of our frames, which is then ignored. */
83
+ export declare const decodeRelay: (message: string) => RelayFrame | undefined;
@@ -0,0 +1,25 @@
1
+ import type { DiscoveredGateway, Invoke } from './discover.js';
2
+ /**
3
+ * One gateway reduced to direct references, built once at boot. Dispatch reads
4
+ * these fields and nothing else — no lookup, no metadata, no DI per message.
5
+ */
6
+ export interface GatewayRuntime {
7
+ readonly name: string;
8
+ readonly path: string;
9
+ readonly upgrade: Invoke | undefined;
10
+ readonly open: Invoke | undefined;
11
+ readonly close: Invoke | undefined;
12
+ readonly drain: Invoke | undefined;
13
+ readonly ping: Invoke | undefined;
14
+ readonly pong: Invoke | undefined;
15
+ /** The raw `@OnMessage()` catch-all: every frame no named event claimed. */
16
+ readonly raw: Invoke | undefined;
17
+ readonly events: ReadonlyMap<string, Invoke>;
18
+ }
19
+ export declare const buildRuntime: (gateway: DiscoveredGateway) => GatewayRuntime;
20
+ /**
21
+ * One route per gateway path, so two gateways on one path would mean one of them
22
+ * could never receive a connection. That is a boot error naming both.
23
+ */
24
+ export declare const buildGateways: (discovered: readonly DiscoveredGateway[]) => ReadonlyMap<string, GatewayRuntime>;
25
+ export declare const someHandler: (gateways: Iterable<GatewayRuntime>, pick: (gateway: GatewayRuntime) => Invoke | undefined) => boolean;
@@ -0,0 +1,28 @@
1
+ import type { ServerWebSocket, WebSocketHandler } from 'bun';
2
+ /**
3
+ * What every socket carries. `path` is the gateway it upgraded on; `context` is
4
+ * whatever that gateway's `@OnUpgrade` returned, so per-connection state is
5
+ * declared where the connection is accepted.
6
+ */
7
+ export interface SocketData<T = unknown> {
8
+ readonly path: string;
9
+ readonly context: T;
10
+ }
11
+ /**
12
+ * Bun's native socket, unwrapped. `send`, `subscribe`, `unsubscribe`,
13
+ * `isSubscribed`, `publish`, `cork` and `close` are its own methods — nothing here
14
+ * reimplements them.
15
+ */
16
+ export type Socket<T = unknown> = ServerWebSocket<SocketData<T>>;
17
+ /** Named for the socket, so it reads apart from the HTTP `ErrorMapper`. */
18
+ export type SocketErrorHandler = (error: unknown, socket: Socket) => void;
19
+ /**
20
+ * The websocket half of `HttpOptions`. Everything but `onError` is `Pick`ed from
21
+ * Bun's own handler type rather than restated, so the names and the accepted values
22
+ * cannot drift from the runtime. `idleTimeout` is in seconds and Bun rejects
23
+ * anything above 960 at `Bun.serve` time.
24
+ */
25
+ export type SocketOptions = Readonly<Pick<WebSocketHandler<SocketData>, 'backpressureLimit' | 'closeOnBackpressureLimit' | 'idleTimeout' | 'maxPayloadLength' | 'perMessageDeflate' | 'publishToSelf' | 'sendPings'>> & {
26
+ /** Where a throwing or rejecting handler goes. @default console.error */
27
+ readonly onError?: SocketErrorHandler;
28
+ };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@dunx/http",
3
+ "version": "0.1.0",
4
+ "description": "Bun.serve adapter for the dunx framework: controllers, middleware and WebSocket gateways",
5
+ "keywords": [
6
+ "bun",
7
+ "controller",
8
+ "dunx",
9
+ "http",
10
+ "pubsub",
11
+ "realtime",
12
+ "router",
13
+ "websocket"
14
+ ],
15
+ "homepage": "https://github.com/petarzarkov/dunx/tree/main/packages/http#readme",
16
+ "license": "MIT",
17
+ "author": {
18
+ "name": "Petar Zarkov",
19
+ "email": "pzarko1@gmail.com",
20
+ "url": "https://github.com/petarzarkov"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/petarzarkov/dunx.git",
25
+ "directory": "packages/http"
26
+ },
27
+ "files": [
28
+ "LICENSE",
29
+ "README.md",
30
+ "dist"
31
+ ],
32
+ "type": "module",
33
+ "main": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js"
39
+ }
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "scripts": {
45
+ "build": "bun ../../scripts/build-package.ts",
46
+ "test": "bun test --bail",
47
+ "test:cov": "bun test --coverage",
48
+ "typecheck": "tsc --noEmit"
49
+ },
50
+ "devDependencies": {
51
+ "@dunx/core": "workspace:*"
52
+ },
53
+ "peerDependencies": {
54
+ "@dunx/core": "0.1.0",
55
+ "@types/bun": ">=1.3.0"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "@types/bun": {
59
+ "optional": true
60
+ }
61
+ },
62
+ "engines": {
63
+ "bun": ">=1.3.0"
64
+ }
65
+ }