@dunx/http 3.6.0 → 3.8.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.
- package/README.md +9 -0
- package/dist/chunk-08k9vq31.js +94 -0
- package/dist/{chunk-p9hdmkm6.js → chunk-1jt27yka.js} +8 -83
- package/dist/chunk-3eecdh6d.js +160 -0
- package/dist/chunk-cx4btdwe.js +56 -0
- package/dist/client/service.d.ts +13 -10
- package/dist/client/sse.d.ts +22 -8
- package/dist/client.d.ts +2 -0
- package/dist/client.js +72 -20
- package/dist/connect/middleware.d.ts +26 -0
- package/dist/connect/module.d.ts +29 -0
- package/dist/connect/options.d.ts +63 -0
- package/dist/connect/registry.d.ts +47 -0
- package/dist/connect.d.ts +12 -0
- package/dist/connect.js +213 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +312 -189
- package/dist/internal.d.ts +8 -10
- package/dist/internal.js +7 -3
- package/dist/route/claims.d.ts +11 -0
- package/dist/route/metadata.d.ts +11 -0
- package/dist/route/prefix.d.ts +6 -0
- package/dist/server/application.d.ts +1 -1
- package/dist/server/binding.d.ts +4 -2
- package/dist/server/claimed-routes.d.ts +15 -0
- package/dist/server/cors.d.ts +2 -2
- package/dist/server/metrics.d.ts +2 -0
- package/dist/server/middleware.d.ts +19 -2
- package/dist/server/options-provider.d.ts +3 -0
- package/dist/server/options.d.ts +8 -0
- package/dist/server/routes.d.ts +7 -2
- package/dist/server/trace-context.d.ts +9 -11
- package/dist/sse/decorators.d.ts +28 -0
- package/dist/sse/event.d.ts +20 -0
- package/dist/sse/stream.d.ts +38 -0
- package/dist/static/files.d.ts +7 -0
- package/dist/static/options.d.ts +2 -2
- package/dist/throttle/guard.d.ts +3 -1
- package/package.json +19 -2
- package/dist/chunk-gmtwad7f.js +0 -71
package/dist/client.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
TRACEPARENT_HEADER2,
|
|
7
7
|
TRACESTATE_HEADER2,
|
|
8
8
|
TraceContext2
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-cx4btdwe.js";
|
|
10
10
|
|
|
11
11
|
// src/client/errors.ts
|
|
12
12
|
import { AppError } from "@dunx/core";
|
|
@@ -159,28 +159,78 @@ var readBody = async (response) => {
|
|
|
159
159
|
};
|
|
160
160
|
|
|
161
161
|
// src/client/sse.ts
|
|
162
|
-
|
|
162
|
+
var LINE = /\r\n|\r|\n/;
|
|
163
|
+
var split = (line) => {
|
|
164
|
+
const colon = line.indexOf(":");
|
|
165
|
+
if (colon === -1)
|
|
166
|
+
return [line, ""];
|
|
167
|
+
const value = line.slice(colon + 1);
|
|
168
|
+
return [line.slice(0, colon), value.startsWith(" ") ? value.slice(1) : value];
|
|
169
|
+
};
|
|
170
|
+
async function* sseMessages(body) {
|
|
163
171
|
const decoder = new TextDecoder;
|
|
164
172
|
let buffer = "";
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
+
let data = [];
|
|
174
|
+
let event;
|
|
175
|
+
let id;
|
|
176
|
+
let retry;
|
|
177
|
+
const reader = body.getReader();
|
|
178
|
+
let done = false;
|
|
179
|
+
try {
|
|
180
|
+
for (;; ) {
|
|
181
|
+
let end = LINE.exec(buffer);
|
|
182
|
+
while (end !== null) {
|
|
183
|
+
if (!done && end[0] === "\r" && end.index + 1 === buffer.length)
|
|
184
|
+
break;
|
|
185
|
+
const line = buffer.slice(0, end.index);
|
|
186
|
+
buffer = buffer.slice(end.index + end[0].length);
|
|
187
|
+
end = LINE.exec(buffer);
|
|
188
|
+
if (line === "") {
|
|
189
|
+
const seen = data.length > 0;
|
|
190
|
+
const payload = data.join(`
|
|
173
191
|
`);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
192
|
+
data = [];
|
|
193
|
+
if (!seen) {
|
|
194
|
+
event = undefined;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (payload === "[DONE]")
|
|
198
|
+
return;
|
|
199
|
+
yield {
|
|
200
|
+
data: payload,
|
|
201
|
+
...event === undefined ? {} : { event },
|
|
202
|
+
...id === undefined ? {} : { id },
|
|
203
|
+
...retry === undefined ? {} : { retry }
|
|
204
|
+
};
|
|
205
|
+
event = undefined;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (line.startsWith(":"))
|
|
209
|
+
continue;
|
|
210
|
+
const [field, value] = split(line);
|
|
211
|
+
if (field === "data")
|
|
212
|
+
data.push(value);
|
|
213
|
+
else if (field === "event")
|
|
214
|
+
event = value;
|
|
215
|
+
else if (field === "id" && !value.includes("\x00"))
|
|
216
|
+
id = value;
|
|
217
|
+
else if (field === "retry" && /^\d+$/.test(value))
|
|
218
|
+
retry = Number(value);
|
|
219
|
+
}
|
|
220
|
+
if (done)
|
|
178
221
|
return;
|
|
179
|
-
|
|
222
|
+
const next = await reader.read();
|
|
223
|
+
if (next.done) {
|
|
224
|
+
buffer += decoder.decode();
|
|
225
|
+
done = true;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
buffer += decoder.decode(next.value, { stream: true });
|
|
180
229
|
}
|
|
230
|
+
} finally {
|
|
231
|
+
reader.releaseLock();
|
|
181
232
|
}
|
|
182
233
|
}
|
|
183
|
-
|
|
184
234
|
class ConnectDeadline {
|
|
185
235
|
#controller = new AbortController;
|
|
186
236
|
#timer;
|
|
@@ -297,6 +347,10 @@ class HttpService extends UrlHelper {
|
|
|
297
347
|
});
|
|
298
348
|
}
|
|
299
349
|
async* streamSse(config) {
|
|
350
|
+
for await (const message of this.streamSseEvents(config))
|
|
351
|
+
yield message.data;
|
|
352
|
+
}
|
|
353
|
+
async* streamSseEvents(config) {
|
|
300
354
|
const url = this.urlFor(config);
|
|
301
355
|
const method = config.method ?? "POST";
|
|
302
356
|
const startedAt = Date.now();
|
|
@@ -304,9 +358,7 @@ class HttpService extends UrlHelper {
|
|
|
304
358
|
const deadline = new ConnectDeadline(config.timeoutMs ?? this.options.timeoutMs, url.href);
|
|
305
359
|
let response;
|
|
306
360
|
try {
|
|
307
|
-
const policy = this.policyFor({ ...config, timeoutMs: 0 }, {
|
|
308
|
-
maxRetries: 0
|
|
309
|
-
});
|
|
361
|
+
const policy = this.policyFor({ ...config, timeoutMs: 0 }, { maxRetries: 0 });
|
|
310
362
|
response = await policy.run((signal) => this.send({ ...config, method }, url, body, serialised, AbortSignal.any([signal, deadline.signal]), "text/event-stream"));
|
|
311
363
|
} finally {
|
|
312
364
|
deadline.clear();
|
|
@@ -317,7 +369,7 @@ class HttpService extends UrlHelper {
|
|
|
317
369
|
}), { method, url: url.href, headers: response.headers });
|
|
318
370
|
}
|
|
319
371
|
try {
|
|
320
|
-
yield*
|
|
372
|
+
yield* sseMessages(response.body);
|
|
321
373
|
} finally {
|
|
322
374
|
this.logger.debug(`SSE ${method} ${url.href} closed`, {
|
|
323
375
|
elapsedMs: Date.now() - startedAt
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BunRequest } from 'bun';
|
|
2
|
+
import type { RouteContext } from '../server/context.js';
|
|
3
|
+
import type { ClaimsPaths, Middleware, Next } from '../server/middleware.js';
|
|
4
|
+
import { ConnectRegistry } from './registry.js';
|
|
5
|
+
/**
|
|
6
|
+
* Serves every registered RPC as ordinary middleware, so request logging, CORS,
|
|
7
|
+
* a guard and the dashboard apply to a call as they apply to a route. Register
|
|
8
|
+
* it with `app.use`, since position in the chain decides what covers it.
|
|
9
|
+
*
|
|
10
|
+
* RPC paths are in no route table, so they reach the `fetch` fallback, where
|
|
11
|
+
* `ctx.get(UNMATCHED)` is true and `ctx.path` is already parsed. Reading it
|
|
12
|
+
* first is what leaves a matched route paying nothing. Anything outside the
|
|
13
|
+
* registered paths falls through untouched.
|
|
14
|
+
*
|
|
15
|
+
* `ThrottleGuard` covers an RPC: its early return is for an unmatched path
|
|
16
|
+
* nobody claims, and a claimed path is served. See docs/guide/27-rpc.md.
|
|
17
|
+
*/
|
|
18
|
+
export declare class ConnectMiddleware implements Middleware, ClaimsPaths {
|
|
19
|
+
#private;
|
|
20
|
+
constructor(registry: ConnectRegistry);
|
|
21
|
+
/** Every mounted RPC path, so a controller cannot shadow one unnoticed. */
|
|
22
|
+
claimedPaths(): readonly string[];
|
|
23
|
+
/** Connect and gRPC-Web both POST. Connect's GET form is not served here. */
|
|
24
|
+
claimedMethods(): readonly string[];
|
|
25
|
+
handle(req: BunRequest, ctx: RouteContext, next: Next): Promise<Response>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type AsyncModuleConfig, type Deps, type DynamicModule } from '@dunx/core';
|
|
2
|
+
import { type ConnectOptionsInit, type ConnectServiceRegistration } from './options.js';
|
|
3
|
+
/** Everything `forRoot` takes except the services, which `forRootAsync` needs
|
|
4
|
+
* synchronously, and `imports`, which `AsyncModuleConfig` already carries. */
|
|
5
|
+
export type ConnectSettings = Omit<ConnectOptionsInit, 'services' | 'imports'>;
|
|
6
|
+
/**
|
|
7
|
+
* Serves protobuf services over Connect and gRPC-Web on the port `Bun.serve`
|
|
8
|
+
* already has. See `docs/guide/27-rpc.md`.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* ConnectModule.forRoot({
|
|
12
|
+
* services: [connectService(GreetService, GreetRpc)],
|
|
13
|
+
* // What GreetRpc injects: this module is its own scope.
|
|
14
|
+
* imports: [GreetingsModule],
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* It binds `ConnectMiddleware` and does not register it - position in the chain
|
|
19
|
+
* decides which guards cover an RPC, so the app calls `app.use`.
|
|
20
|
+
*/
|
|
21
|
+
export declare class ConnectModule {
|
|
22
|
+
static forRoot(init: ConnectOptionsInit): DynamicModule;
|
|
23
|
+
/**
|
|
24
|
+
* `forRoot` with everything but the services behind a factory, so the prefix
|
|
25
|
+
* or the read limits can come off `ConfigService`. The services are positional
|
|
26
|
+
* because their classes have to be providers before any factory runs.
|
|
27
|
+
*/
|
|
28
|
+
static forRootAsync<const D extends Deps>(services: readonly ConnectServiceRegistration[], config: AsyncModuleConfig<ConnectSettings, D>): DynamicModule;
|
|
29
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { DescService } from '@bufbuild/protobuf';
|
|
2
|
+
import type { ConnectRouterOptions, ServiceImpl } from '@connectrpc/connect';
|
|
3
|
+
import { type Ctor, type 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` limits the calls, and this
|
|
47
|
+
* limits how long one that is already streaming may idle.
|
|
48
|
+
*
|
|
49
|
+
* @default 0
|
|
50
|
+
*/
|
|
51
|
+
readonly streamTimeout?: number;
|
|
52
|
+
}
|
|
53
|
+
/** A class rather than an interface, so it is a runtime value the transform can
|
|
54
|
+
* record as a constructor parameter type. */
|
|
55
|
+
export declare class ConnectOptions {
|
|
56
|
+
readonly services: readonly ConnectServiceRegistration[];
|
|
57
|
+
readonly prefix: string;
|
|
58
|
+
readonly connect: boolean;
|
|
59
|
+
readonly grpcWeb: boolean;
|
|
60
|
+
readonly streamTimeout: number;
|
|
61
|
+
readonly router: ConnectRouterSettings;
|
|
62
|
+
constructor(init: ConnectOptionsInit);
|
|
63
|
+
}
|
|
@@ -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';
|
package/dist/connect.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
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 { AppError as AppError2 } from "@dunx/core";
|
|
20
|
+
import {
|
|
21
|
+
createConnectRouter
|
|
22
|
+
} from "@connectrpc/connect";
|
|
23
|
+
import { createFetchHandler } from "@connectrpc/connect/protocol";
|
|
24
|
+
|
|
25
|
+
// src/connect/options.ts
|
|
26
|
+
import { AppError } from "@dunx/core";
|
|
27
|
+
var connectService = (service, useClass) => ({ service, useClass });
|
|
28
|
+
|
|
29
|
+
class ConnectOptions {
|
|
30
|
+
services;
|
|
31
|
+
prefix;
|
|
32
|
+
connect;
|
|
33
|
+
grpcWeb;
|
|
34
|
+
streamTimeout;
|
|
35
|
+
router;
|
|
36
|
+
constructor(init) {
|
|
37
|
+
const {
|
|
38
|
+
services,
|
|
39
|
+
prefix,
|
|
40
|
+
connect,
|
|
41
|
+
grpcWeb,
|
|
42
|
+
streamTimeout,
|
|
43
|
+
imports: _imports,
|
|
44
|
+
...router
|
|
45
|
+
} = init;
|
|
46
|
+
this.services = services;
|
|
47
|
+
const mounted = normalizePrefix(prefix ?? "");
|
|
48
|
+
this.prefix = mounted === "/" ? "" : mounted;
|
|
49
|
+
this.connect = connect ?? true;
|
|
50
|
+
this.grpcWeb = grpcWeb ?? true;
|
|
51
|
+
this.streamTimeout = streamTimeout ?? 0;
|
|
52
|
+
if (!Number.isFinite(this.streamTimeout) || this.streamTimeout < 0) {
|
|
53
|
+
throw new AppError(`ConnectModule streamTimeout must be a non-negative number of seconds, got ${String(streamTimeout)}.`);
|
|
54
|
+
}
|
|
55
|
+
this.router = router;
|
|
56
|
+
if (!this.connect && !this.grpcWeb) {
|
|
57
|
+
throw new AppError("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.");
|
|
58
|
+
}
|
|
59
|
+
if (services.length === 0) {
|
|
60
|
+
throw new AppError("ConnectModule.forRoot was given no services. Pass at least one " + "connectService(Desc, Impl), or drop the module.");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
Object.defineProperty(ConnectOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: ConnectOptionsInit" }] });
|
|
65
|
+
|
|
66
|
+
// src/connect/registry.ts
|
|
67
|
+
class ConnectRegistry {
|
|
68
|
+
#routes = new Map;
|
|
69
|
+
get paths() {
|
|
70
|
+
return [...this.#routes.keys()];
|
|
71
|
+
}
|
|
72
|
+
streamTimeout;
|
|
73
|
+
#methods = [];
|
|
74
|
+
#stopping = new AbortController;
|
|
75
|
+
constructor(options, implementations) {
|
|
76
|
+
this.streamTimeout = options.streamTimeout;
|
|
77
|
+
const router = createConnectRouter({
|
|
78
|
+
...options.router,
|
|
79
|
+
connect: options.connect,
|
|
80
|
+
grpcWeb: options.grpcWeb,
|
|
81
|
+
grpc: false,
|
|
82
|
+
shutdownSignal: this.#stopping.signal
|
|
83
|
+
});
|
|
84
|
+
options.services.forEach((registration, index) => {
|
|
85
|
+
const implementation = implementations[index];
|
|
86
|
+
if (implementation === undefined) {
|
|
87
|
+
throw new AppError2(`No instance was resolved for ${registration.useClass.name}, which ` + `serves ${registration.service.typeName}.`);
|
|
88
|
+
}
|
|
89
|
+
router.service(registration.service, implementation);
|
|
90
|
+
});
|
|
91
|
+
const claims = new PathClaims("RPC", "Register each service once, or give one its own prefix.");
|
|
92
|
+
for (const handler of router.handlers) {
|
|
93
|
+
const path = `${options.prefix}${handler.requestPath}`;
|
|
94
|
+
const rpc = `${handler.service.typeName}.${handler.method.name}`;
|
|
95
|
+
claims.claim(path, rpc);
|
|
96
|
+
this.#routes.set(path, {
|
|
97
|
+
handle: createFetchHandler(handler),
|
|
98
|
+
streaming: handler.method.methodKind !== "unary"
|
|
99
|
+
});
|
|
100
|
+
this.#methods.push({
|
|
101
|
+
path,
|
|
102
|
+
service: handler.service.typeName,
|
|
103
|
+
method: handler.method.name,
|
|
104
|
+
kind: handler.method.methodKind,
|
|
105
|
+
protocols: handler.protocolNames
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
get methods() {
|
|
110
|
+
return this.#methods;
|
|
111
|
+
}
|
|
112
|
+
routeFor(path) {
|
|
113
|
+
return this.#routes.get(path);
|
|
114
|
+
}
|
|
115
|
+
onShutdown() {
|
|
116
|
+
this.#stopping.abort();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
Object.defineProperty(ConnectRegistry, Symbol.for("dunx.deps"), { value: () => [ConnectOptions, { unresolved: "implementations: readonly object[]" }] });
|
|
120
|
+
|
|
121
|
+
// src/connect/middleware.ts
|
|
122
|
+
var NATIVE_GRPC = /^\s*application\/grpc\s*(?:[;+]|$)/i;
|
|
123
|
+
var grpcUnsupported = () => Response.json({
|
|
124
|
+
code: "unimplemented",
|
|
125
|
+
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."
|
|
126
|
+
}, { status: HttpStatusCode2.UNSUPPORTED_MEDIA_TYPE });
|
|
127
|
+
|
|
128
|
+
class ConnectMiddleware {
|
|
129
|
+
#registry;
|
|
130
|
+
constructor(registry) {
|
|
131
|
+
this.#registry = registry;
|
|
132
|
+
}
|
|
133
|
+
claimedPaths() {
|
|
134
|
+
return this.#registry.paths;
|
|
135
|
+
}
|
|
136
|
+
claimedMethods() {
|
|
137
|
+
return ["POST"];
|
|
138
|
+
}
|
|
139
|
+
handle(req, ctx, next) {
|
|
140
|
+
if (ctx.get(UNMATCHED2) !== true)
|
|
141
|
+
return next();
|
|
142
|
+
const route = this.#registry.routeFor(ctx.path);
|
|
143
|
+
if (route === undefined)
|
|
144
|
+
return next();
|
|
145
|
+
const contentType = req.headers.get("content-type");
|
|
146
|
+
if (contentType !== null && NATIVE_GRPC.test(contentType)) {
|
|
147
|
+
return Promise.resolve(grpcUnsupported());
|
|
148
|
+
}
|
|
149
|
+
if (route.streaming) {
|
|
150
|
+
ctx.get(REQUEST_SERVER)?.timeout(req, this.#registry.streamTimeout);
|
|
151
|
+
}
|
|
152
|
+
return route.handle(req);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
Object.defineProperty(ConnectMiddleware, Symbol.for("dunx.deps"), { value: () => [ConnectRegistry] });
|
|
156
|
+
// src/connect/module.ts
|
|
157
|
+
import {
|
|
158
|
+
Module,
|
|
159
|
+
provide
|
|
160
|
+
} from "@dunx/core";
|
|
161
|
+
var build = (services, options, imports) => {
|
|
162
|
+
const implementations = services.map((registration) => registration.useClass);
|
|
163
|
+
return {
|
|
164
|
+
module: ConnectModule,
|
|
165
|
+
imports,
|
|
166
|
+
exports: [ConnectOptions, ConnectRegistry, ConnectMiddleware],
|
|
167
|
+
providers: [
|
|
168
|
+
...implementations,
|
|
169
|
+
options,
|
|
170
|
+
provide(ConnectRegistry, {
|
|
171
|
+
useFactory: (...deps) => {
|
|
172
|
+
const [resolved, ...instances] = deps;
|
|
173
|
+
return new ConnectRegistry(resolved, instances);
|
|
174
|
+
},
|
|
175
|
+
inject: [ConnectOptions, ...implementations]
|
|
176
|
+
}),
|
|
177
|
+
provide(ConnectMiddleware, {
|
|
178
|
+
useFactory: (registry) => new ConnectMiddleware(registry),
|
|
179
|
+
inject: [ConnectRegistry]
|
|
180
|
+
})
|
|
181
|
+
]
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
var _dec = [
|
|
185
|
+
Module({})
|
|
186
|
+
];
|
|
187
|
+
var _init = __decoratorStart(undefined);
|
|
188
|
+
|
|
189
|
+
class ConnectModule {
|
|
190
|
+
static forRoot(init) {
|
|
191
|
+
return build(init.services, provide(ConnectOptions, { useValue: new ConnectOptions(init) }), init.imports ?? []);
|
|
192
|
+
}
|
|
193
|
+
static forRootAsync(services, config) {
|
|
194
|
+
return build(services, provide(ConnectOptions, {
|
|
195
|
+
useFactory: async (...deps) => new ConnectOptions({
|
|
196
|
+
...await config.useFactory(...deps),
|
|
197
|
+
services
|
|
198
|
+
}),
|
|
199
|
+
inject: config.inject ?? []
|
|
200
|
+
}), config.imports ?? []);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
ConnectModule = __decorateElement(_init, 0, "ConnectModule", _dec, ConnectModule);
|
|
204
|
+
__runInitializers(_init, 1, ConnectModule);
|
|
205
|
+
__decoratorMetadata(_init, ConnectModule);
|
|
206
|
+
let _ConnectModule = ConnectModule;
|
|
207
|
+
export {
|
|
208
|
+
ConnectMiddleware,
|
|
209
|
+
ConnectModule,
|
|
210
|
+
ConnectOptions,
|
|
211
|
+
ConnectRegistry,
|
|
212
|
+
connectService
|
|
213
|
+
};
|
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';
|