@dunx/http 3.6.0 → 3.7.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,64 @@
1
+ import type { DescService } from '@bufbuild/protobuf';
2
+ import type { ConnectRouterOptions, ServiceImpl } from '@connectrpc/connect';
3
+ import type { Ctor, ModuleRef } from '@dunx/core';
4
+ /**
5
+ * Everything `createConnectRouter` takes except the protocol switches and
6
+ * `shutdownSignal`, which the container owns. `interceptors`, `contextValues`,
7
+ * `requestGate`, `jsonOptions` and the rest pass through untouched.
8
+ */
9
+ export type ConnectRouterSettings = Omit<ConnectRouterOptions, 'connect' | 'grpc' | 'grpcWeb' | 'shutdownSignal'>;
10
+ /** One protobuf service and the class implementing it. */
11
+ export interface ConnectServiceRegistration {
12
+ readonly service: DescService;
13
+ readonly useClass: Ctor<object>;
14
+ }
15
+ /**
16
+ * Pairs a generated `DescService` with the class serving it, checking at compile
17
+ * time that the class has a method per RPC. A generic function rather than an
18
+ * object literal, which would lose the link between the two arguments.
19
+ */
20
+ export declare const connectService: <T extends DescService>(service: T, useClass: Ctor<ServiceImpl<T>>) => ConnectServiceRegistration;
21
+ export interface ConnectOptionsInit extends ConnectRouterSettings {
22
+ /** The services to serve, each paired with its implementation class. */
23
+ readonly services: readonly ConnectServiceRegistration[];
24
+ /**
25
+ * Modules whose exports the implementation classes may inject. This module is
26
+ * its own scope and the classes are constructed in it, so a provider they need
27
+ * has to be exported by a module named here - importing it alongside does not
28
+ * reach them.
29
+ */
30
+ readonly imports?: readonly ModuleRef[];
31
+ /**
32
+ * Mounted in front of every RPC path, empty by default, which leaves them at
33
+ * `/{package}.{Service}/{Method}`. `setGlobalPrefix` does not move them: that
34
+ * prefixes discovered routes, and these are matched by a middleware.
35
+ */
36
+ readonly prefix?: string;
37
+ /** Connect, which a `curl` POST of JSON also speaks. @default true */
38
+ readonly connect?: boolean;
39
+ /** gRPC-Web, which browsers and `connect-go` speak. @default true */
40
+ readonly grpcWeb?: boolean;
41
+ /**
42
+ * Seconds a **streaming** RPC may idle before `Bun.serve` severs it. `0` lifts
43
+ * the deadline for the whole call, which is the default because a gap between
44
+ * messages is the protocol rather than a symptom.
45
+ *
46
+ * Worth setting on a public mount: `ThrottleGuard` returns early on every
47
+ * unmatched path and an RPC is unmatched, so nothing else here bounds how long
48
+ * or how many streams one caller holds open.
49
+ *
50
+ * @default 0
51
+ */
52
+ readonly streamTimeout?: number;
53
+ }
54
+ /** A class rather than an interface, so it is a runtime value the transform can
55
+ * record as a constructor parameter type. */
56
+ export declare class ConnectOptions {
57
+ readonly services: readonly ConnectServiceRegistration[];
58
+ readonly prefix: string;
59
+ readonly connect: boolean;
60
+ readonly grpcWeb: boolean;
61
+ readonly streamTimeout: number;
62
+ readonly router: ConnectRouterSettings;
63
+ constructor(init: ConnectOptionsInit);
64
+ }
@@ -0,0 +1,47 @@
1
+ import { ConnectOptions } from './options.js';
2
+ /** What `createFetchHandler` returns: the `Bun.serve` signature exactly. */
3
+ export type ConnectHandler = (req: Request) => Promise<Response>;
4
+ /** One mounted RPC. */
5
+ export interface ConnectMethodInfo {
6
+ /** The path it answers, including {@link ConnectOptions.prefix}. */
7
+ readonly path: string;
8
+ /** e.g. `greet.v1.GreetService`. */
9
+ readonly service: string;
10
+ /** e.g. `Say`. */
11
+ readonly method: string;
12
+ /** `unary`, `server_streaming`, `client_streaming` or `bidi_streaming`. */
13
+ readonly kind: string;
14
+ /** e.g. `['grpc-web', 'connect']`. */
15
+ readonly protocols: readonly string[];
16
+ }
17
+ /** What the middleware needs per path, in one lookup. */
18
+ export interface ConnectRoute {
19
+ readonly handle: ConnectHandler;
20
+ /** Anything but `unary`, so the gap between messages is the protocol. */
21
+ readonly streaming: boolean;
22
+ }
23
+ /**
24
+ * One fetch handler per RPC, built at boot into a path map, so a request costs a
25
+ * `Map.get`. Connect's own router picks the protocol off the content type.
26
+ *
27
+ * `grpc` is off and cannot be turned on: it carries `grpc-status` in an HTTP
28
+ * trailer and `Bun.serve` sends none, so advertising it would answer with a
29
+ * status no client reads. Probed on Bun 1.4.2.
30
+ */
31
+ export declare class ConnectRegistry {
32
+ #private;
33
+ /** Every mounted path, in registration order. */
34
+ get paths(): readonly string[];
35
+ /** Seconds a streaming call may idle; `0` lifts the deadline. */
36
+ readonly streamTimeout: number;
37
+ constructor(options: ConnectOptions, implementations: readonly object[]);
38
+ /** In registration order. */
39
+ get methods(): readonly ConnectMethodInfo[];
40
+ routeFor(path: string): ConnectRoute | undefined;
41
+ /**
42
+ * Aborts the signal every in-flight handler holds, so a long-running
43
+ * implementation gets its cue. It does not close an open response stream -
44
+ * the socket closing does. Measured on `@connectrpc/connect` 2.2.0.
45
+ */
46
+ onShutdown(): void;
47
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `@dunx/http/connect` - protobuf services over Connect and gRPC-Web, mounted as
3
+ * middleware. `@connectrpc/connect` and `@bufbuild/protobuf` are optional peers,
4
+ * and importing `@dunx/http` does not load any of this.
5
+ *
6
+ * Native gRPC is not here: `grpc-status` travels in an HTTP trailer and
7
+ * `Bun.serve` sends none. See `internal/notes/research/rpc.md`.
8
+ */
9
+ export { ConnectMiddleware } from './connect/middleware.js';
10
+ export { ConnectModule, type ConnectSettings } from './connect/module.js';
11
+ export { connectService, ConnectOptions, type ConnectOptionsInit, type ConnectRouterSettings, type ConnectServiceRegistration, } from './connect/options.js';
12
+ export { ConnectRegistry, type ConnectHandler, type ConnectMethodInfo, type ConnectRoute, } from './connect/registry.js';
@@ -0,0 +1,208 @@
1
+ // @bun
2
+ import {
3
+ UNMATCHED2,
4
+ REQUEST_SERVER,
5
+ normalizePrefix
6
+ } from "./chunk-1jt27yka.js";
7
+ import {
8
+ HttpStatusCode2
9
+ } from "./chunk-bg0dr54z.js";
10
+ import {
11
+ __decoratorStart,
12
+ __decoratorMetadata,
13
+ __runInitializers,
14
+ __decorateElement,
15
+ PathClaims
16
+ } from "./chunk-08k9vq31.js";
17
+
18
+ // src/connect/registry.ts
19
+ import {
20
+ createConnectRouter
21
+ } from "@connectrpc/connect";
22
+ import { createFetchHandler } from "@connectrpc/connect/protocol";
23
+
24
+ // src/connect/options.ts
25
+ var connectService = (service, useClass) => ({ service, useClass });
26
+
27
+ class ConnectOptions {
28
+ services;
29
+ prefix;
30
+ connect;
31
+ grpcWeb;
32
+ streamTimeout;
33
+ router;
34
+ constructor(init) {
35
+ const {
36
+ services,
37
+ prefix,
38
+ connect,
39
+ grpcWeb,
40
+ streamTimeout,
41
+ imports: _imports,
42
+ ...router
43
+ } = init;
44
+ this.services = services;
45
+ const mounted = normalizePrefix(prefix ?? "");
46
+ this.prefix = mounted === "/" ? "" : mounted;
47
+ this.connect = connect ?? true;
48
+ this.grpcWeb = grpcWeb ?? true;
49
+ this.streamTimeout = streamTimeout ?? 0;
50
+ if (!Number.isFinite(this.streamTimeout) || this.streamTimeout < 0) {
51
+ throw new Error(`ConnectModule streamTimeout must be a non-negative number of seconds, got ${String(streamTimeout)}.`);
52
+ }
53
+ this.router = router;
54
+ if (!this.connect && !this.grpcWeb) {
55
+ throw new Error("ConnectModule needs at least one protocol, and both connect and " + "grpcWeb are false. Native gRPC is not a third option here: it " + "carries grpc-status in an HTTP trailer and Bun.serve sends none.");
56
+ }
57
+ if (services.length === 0) {
58
+ throw new Error("ConnectModule.forRoot was given no services. Pass at least one " + "connectService(Desc, Impl), or drop the module.");
59
+ }
60
+ }
61
+ }
62
+ Object.defineProperty(ConnectOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: ConnectOptionsInit" }] });
63
+
64
+ // src/connect/registry.ts
65
+ class ConnectRegistry {
66
+ #routes = new Map;
67
+ get paths() {
68
+ return [...this.#routes.keys()];
69
+ }
70
+ streamTimeout;
71
+ #methods = [];
72
+ #stopping = new AbortController;
73
+ constructor(options, implementations) {
74
+ this.streamTimeout = options.streamTimeout;
75
+ const router = createConnectRouter({
76
+ ...options.router,
77
+ connect: options.connect,
78
+ grpcWeb: options.grpcWeb,
79
+ grpc: false,
80
+ shutdownSignal: this.#stopping.signal
81
+ });
82
+ options.services.forEach((registration, index) => {
83
+ const implementation = implementations[index];
84
+ if (implementation === undefined) {
85
+ throw new Error(`No instance was resolved for ${registration.useClass.name}, which ` + `serves ${registration.service.typeName}.`);
86
+ }
87
+ router.service(registration.service, implementation);
88
+ });
89
+ const claims = new PathClaims("RPC", "Register each service once, or give one its own prefix.");
90
+ for (const handler of router.handlers) {
91
+ const path = `${options.prefix}${handler.requestPath}`;
92
+ const rpc = `${handler.service.typeName}.${handler.method.name}`;
93
+ claims.claim(path, rpc);
94
+ this.#routes.set(path, {
95
+ handle: createFetchHandler(handler),
96
+ streaming: handler.method.methodKind !== "unary"
97
+ });
98
+ this.#methods.push({
99
+ path,
100
+ service: handler.service.typeName,
101
+ method: handler.method.name,
102
+ kind: handler.method.methodKind,
103
+ protocols: handler.protocolNames
104
+ });
105
+ }
106
+ }
107
+ get methods() {
108
+ return this.#methods;
109
+ }
110
+ routeFor(path) {
111
+ return this.#routes.get(path);
112
+ }
113
+ onShutdown() {
114
+ this.#stopping.abort();
115
+ }
116
+ }
117
+ Object.defineProperty(ConnectRegistry, Symbol.for("dunx.deps"), { value: () => [ConnectOptions, { unresolved: "implementations: readonly object[]" }] });
118
+
119
+ // src/connect/middleware.ts
120
+ var NATIVE_GRPC = /^\s*application\/grpc\s*(?:[;+]|$)/i;
121
+ var grpcUnsupported = () => Response.json({
122
+ code: "unimplemented",
123
+ message: "This endpoint serves Connect and gRPC-Web, not gRPC. gRPC carries " + "grpc-status in an HTTP trailer and Bun.serve sends no trailers, so a " + "gRPC client would read every call as a protocol error. Use a Connect " + "or gRPC-Web transport, or put a proxy in front that translates."
124
+ }, { status: HttpStatusCode2.UNSUPPORTED_MEDIA_TYPE });
125
+
126
+ class ConnectMiddleware {
127
+ #registry;
128
+ constructor(registry) {
129
+ this.#registry = registry;
130
+ }
131
+ claimedPaths() {
132
+ return this.#registry.paths;
133
+ }
134
+ handle(req, ctx, next) {
135
+ if (ctx.get(UNMATCHED2) !== true)
136
+ return next();
137
+ const route = this.#registry.routeFor(ctx.path);
138
+ if (route === undefined)
139
+ return next();
140
+ const contentType = req.headers.get("content-type");
141
+ if (contentType !== null && NATIVE_GRPC.test(contentType)) {
142
+ return Promise.resolve(grpcUnsupported());
143
+ }
144
+ if (route.streaming) {
145
+ ctx.get(REQUEST_SERVER)?.timeout(req, this.#registry.streamTimeout);
146
+ }
147
+ return route.handle(req);
148
+ }
149
+ }
150
+ Object.defineProperty(ConnectMiddleware, Symbol.for("dunx.deps"), { value: () => [ConnectRegistry] });
151
+ // src/connect/module.ts
152
+ import {
153
+ Module,
154
+ provide
155
+ } from "@dunx/core";
156
+ var build = (services, options, imports) => {
157
+ const implementations = services.map((registration) => registration.useClass);
158
+ return {
159
+ module: ConnectModule,
160
+ imports,
161
+ exports: [ConnectOptions, ConnectRegistry, ConnectMiddleware],
162
+ providers: [
163
+ ...implementations,
164
+ options,
165
+ provide(ConnectRegistry, {
166
+ useFactory: (...deps) => {
167
+ const [resolved, ...instances] = deps;
168
+ return new ConnectRegistry(resolved, instances);
169
+ },
170
+ inject: [ConnectOptions, ...implementations]
171
+ }),
172
+ provide(ConnectMiddleware, {
173
+ useFactory: (registry) => new ConnectMiddleware(registry),
174
+ inject: [ConnectRegistry]
175
+ })
176
+ ]
177
+ };
178
+ };
179
+ var _dec = [
180
+ Module({})
181
+ ];
182
+ var _init = __decoratorStart(undefined);
183
+
184
+ class ConnectModule {
185
+ static forRoot(init) {
186
+ return build(init.services, provide(ConnectOptions, { useValue: new ConnectOptions(init) }), init.imports ?? []);
187
+ }
188
+ static forRootAsync(services, config) {
189
+ return build(services, provide(ConnectOptions, {
190
+ useFactory: async (...deps) => new ConnectOptions({
191
+ ...await config.useFactory(...deps),
192
+ services
193
+ }),
194
+ inject: config.inject ?? []
195
+ }), config.imports ?? []);
196
+ }
197
+ }
198
+ ConnectModule = __decorateElement(_init, 0, "ConnectModule", _dec, ConnectModule);
199
+ __runInitializers(_init, 1, ConnectModule);
200
+ __decoratorMetadata(_init, ConnectModule);
201
+ let _ConnectModule = ConnectModule;
202
+ export {
203
+ ConnectMiddleware,
204
+ ConnectModule,
205
+ ConnectOptions,
206
+ ConnectRegistry,
207
+ connectService
208
+ };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { Controller, Delete, Get, Patch, Post, Put, } from './route/decorators.js';
2
2
  export type { HttpMethod, RoutePath } from './route/marker.js';
3
- export { ApiHidden, HIDDEN, meta, metaKey, metaOf, mergeMeta, Public, PUBLIC, Roles, ROLES, UNMATCHED, UseGuards, type MetaKey, type MetaRecord, } from './route/metadata.js';
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
5
  export { ClientAddress } from './server/client-address.js';
6
6
  export type { RouteContext } from './server/context.js';
@@ -19,6 +19,9 @@ export { StaticOptions, type StaticOptionsInit } from './static/options.js';
19
19
  export { Compression } from './compression/compression.js';
20
20
  export { CompressionModule } from './compression/module.js';
21
21
  export { CompressionEncoding, CompressionOptions, type CompressionOptionsInit, } from './compression/options.js';
22
+ export { Sse, type SseInput, type SseResult, type SseSchemas, } from './sse/decorators.js';
23
+ export type { SseEvent } from './sse/event.js';
24
+ export { SseStream, type SseStreamOptions } from './sse/stream.js';
22
25
  export { SKIP_THROTTLE, SkipThrottle, THROTTLE, Throttle, type ThrottleLimit, } from './throttle/decorators.js';
23
26
  export { ThrottleGuard } from './throttle/guard.js';
24
27
  export { ThrottleModule } from './throttle/module.js';