@dunx/http 3.5.1 → 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.
Files changed (39) hide show
  1. package/README.md +14 -4
  2. package/dist/chunk-08k9vq31.js +94 -0
  3. package/dist/{chunk-p9hdmkm6.js → chunk-1jt27yka.js} +8 -83
  4. package/dist/chunk-3eecdh6d.js +160 -0
  5. package/dist/client/json.d.ts +12 -11
  6. package/dist/client/options.d.ts +3 -3
  7. package/dist/client/retry.d.ts +17 -31
  8. package/dist/client/service.d.ts +21 -12
  9. package/dist/client/sse.d.ts +39 -0
  10. package/dist/client.d.ts +10 -1
  11. package/dist/client.js +166 -108
  12. package/dist/connect/middleware.d.ts +25 -0
  13. package/dist/connect/module.d.ts +29 -0
  14. package/dist/connect/options.d.ts +64 -0
  15. package/dist/connect/registry.d.ts +47 -0
  16. package/dist/connect.d.ts +12 -0
  17. package/dist/connect.js +208 -0
  18. package/dist/index.d.ts +4 -1
  19. package/dist/index.js +253 -184
  20. package/dist/internal.d.ts +8 -10
  21. package/dist/internal.js +7 -3
  22. package/dist/route/claims.d.ts +11 -0
  23. package/dist/route/metadata.d.ts +11 -0
  24. package/dist/route/prefix.d.ts +6 -0
  25. package/dist/server/application.d.ts +1 -1
  26. package/dist/server/binding.d.ts +4 -2
  27. package/dist/server/claimed-routes.d.ts +15 -0
  28. package/dist/server/cors.d.ts +2 -2
  29. package/dist/server/middleware.d.ts +14 -2
  30. package/dist/server/options-provider.d.ts +3 -0
  31. package/dist/server/options.d.ts +9 -1
  32. package/dist/server/routes.d.ts +7 -2
  33. package/dist/sse/decorators.d.ts +28 -0
  34. package/dist/sse/event.d.ts +19 -0
  35. package/dist/sse/stream.d.ts +35 -0
  36. package/dist/static/files.d.ts +7 -0
  37. package/dist/static/options.d.ts +2 -2
  38. package/dist/throttle/guard.d.ts +3 -1
  39. package/package.json +19 -2
package/dist/client.js CHANGED
@@ -1,12 +1,12 @@
1
1
  // @bun
2
+ import {
3
+ HttpStatusCode2
4
+ } from "./chunk-bg0dr54z.js";
2
5
  import {
3
6
  TRACEPARENT_HEADER2,
4
7
  TRACESTATE_HEADER2,
5
8
  TraceContext2
6
9
  } from "./chunk-gmtwad7f.js";
7
- import {
8
- HttpStatusCode2
9
- } from "./chunk-bg0dr54z.js";
10
10
 
11
11
  // src/client/errors.ts
12
12
  import { AppError } from "@dunx/core";
@@ -71,6 +71,45 @@ class HttpClientOptions {
71
71
  }
72
72
  }
73
73
  Object.defineProperty(HttpClientOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: HttpClientOptionsInit = {}", optional: true }] });
74
+ // src/client/retry.ts
75
+ import {
76
+ RetryClassifier
77
+ } from "@dunx/core";
78
+ var retryAfterMs = (headers, now = Date.now()) => {
79
+ const header = headers.get("retry-after");
80
+ if (header === null)
81
+ return;
82
+ const seconds = Number(header);
83
+ if (Number.isFinite(seconds))
84
+ return Math.max(0, seconds * 1000);
85
+ const at = Date.parse(header);
86
+ return Number.isNaN(at) ? undefined : Math.max(0, at - now);
87
+ };
88
+ var isRetryableStatus = (status) => status >= HttpStatusCode2.INTERNAL_SERVER_ERROR || status === HttpStatusCode2.REQUEST_TIMEOUT || status === HttpStatusCode2.TOO_MANY_REQUESTS;
89
+
90
+ class HttpRetryClassifier extends RetryClassifier {
91
+ options;
92
+ constructor(options = {}) {
93
+ super();
94
+ this.options = options;
95
+ }
96
+ classify(error) {
97
+ if (error instanceof FetchTransportError)
98
+ return { retry: !error.aborted };
99
+ if (error instanceof FetchError) {
100
+ const {
101
+ shouldRetryOnStatus = isRetryableStatus,
102
+ respectRetryAfter = true
103
+ } = this.options;
104
+ if (!shouldRetryOnStatus(error.status))
105
+ return { retry: false };
106
+ const asked = respectRetryAfter ? retryAfterMs(error.response.headers) : undefined;
107
+ return asked === undefined ? { retry: true } : { retry: true, delayMs: asked };
108
+ }
109
+ return { retry: true };
110
+ }
111
+ }
112
+ Object.defineProperty(HttpRetryClassifier, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "private readonly options: HttpRetryOptions = {}", optional: true }] });
74
113
  // src/client/module.ts
75
114
  import {
76
115
  Logger as Logger2,
@@ -80,10 +119,16 @@ import {
80
119
  } from "@dunx/core";
81
120
 
82
121
  // src/client/service.ts
83
- import { Logger, RequestContext } from "@dunx/core";
122
+ import {
123
+ Logger,
124
+ RequestContext,
125
+ ResilienceOptions,
126
+ ResiliencePolicy
127
+ } from "@dunx/core";
84
128
  import { UrlHelper } from "@arkv/shared";
85
129
 
86
130
  // src/client/json.ts
131
+ import { isPlainObject } from "@dunx/core";
87
132
  var safeStringify = (value) => {
88
133
  const seen = new WeakSet;
89
134
  return JSON.stringify(value, (_key, entry) => {
@@ -102,69 +147,90 @@ var isJsonBody = (payload) => {
102
147
  return typeof payload !== "string";
103
148
  return !(payload instanceof FormData || payload instanceof URLSearchParams || payload instanceof Blob || payload instanceof ArrayBuffer || payload instanceof ReadableStream || ArrayBuffer.isView(payload));
104
149
  };
105
-
106
- // src/client/retry.ts
107
- var uniform = () => {
108
- const buffer = new Uint32Array(1);
109
- crypto.getRandomValues(buffer);
110
- return (buffer[0] ?? 0) / 2 ** 32;
111
- };
112
- var backoffDelay = (attempt, { baseMs, power = 2, jitterMs = 1000, maxMs = 30000 }) => Math.min(baseMs * power ** attempt + uniform() * jitterMs, maxMs);
113
- var retryAfterMs = (headers, now = Date.now()) => {
114
- const header = headers.get("retry-after");
115
- if (header === null)
150
+ var readBody = async (response) => {
151
+ const text = await response.text();
152
+ if (text === "")
116
153
  return;
117
- const seconds = Number(header);
118
- if (Number.isFinite(seconds))
119
- return Math.max(0, seconds * 1000);
120
- const at = Date.parse(header);
121
- return Number.isNaN(at) ? undefined : Math.max(0, at - now);
122
- };
123
- var isRetryableStatus = (status) => status >= HttpStatusCode2.INTERNAL_SERVER_ERROR || status === HttpStatusCode2.REQUEST_TIMEOUT || status === HttpStatusCode2.TOO_MANY_REQUESTS;
124
- var decide = (error, attempt, options) => {
125
- const {
126
- retryDelayMs = 1000,
127
- backoff,
128
- shouldRetryOnStatus = isRetryableStatus,
129
- respectRetryAfter = true
130
- } = options;
131
- const computed = backoffDelay(attempt, { baseMs: retryDelayMs, ...backoff });
132
- if (error instanceof FetchTransportError) {
133
- return { retry: !error.aborted, delayMs: computed };
134
- }
135
- if (error instanceof FetchError) {
136
- if (!shouldRetryOnStatus(error.status))
137
- return { retry: false, delayMs: 0 };
138
- const asked = respectRetryAfter ? retryAfterMs(error.response.headers) : undefined;
139
- const maxMs = backoff?.maxMs ?? 30000;
140
- return {
141
- retry: true,
142
- delayMs: asked === undefined ? computed : Math.min(asked, maxMs)
143
- };
154
+ try {
155
+ return JSON.parse(text);
156
+ } catch {
157
+ return text;
144
158
  }
145
- return { retry: true, delayMs: computed };
146
159
  };
147
- var executeWithRetry = async (operation, options = {}) => {
148
- const { maxRetries = 3, onAttempt, onError, onSuccess } = options;
149
- let lastError;
150
- for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
151
- onAttempt?.(attempt + 1, attempt > 0);
152
- try {
153
- const result = await operation();
154
- onSuccess?.(result, attempt + 1);
155
- return result;
156
- } catch (error) {
157
- lastError = error;
158
- const { retry, delayMs } = decide(error, attempt, options);
159
- const willRetry = retry && attempt < maxRetries;
160
- onError?.(error, attempt + 1, willRetry);
161
- if (!willRetry)
162
- throw error;
163
- await Bun.sleep(delayMs);
160
+
161
+ // src/client/sse.ts
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) {
171
+ const decoder = new TextDecoder;
172
+ let buffer = "";
173
+ let data = [];
174
+ let event;
175
+ let id;
176
+ let retry;
177
+ for await (const chunk of body) {
178
+ buffer += decoder.decode(chunk, { stream: true });
179
+ let end = LINE.exec(buffer);
180
+ while (end !== null) {
181
+ const line = buffer.slice(0, end.index);
182
+ buffer = buffer.slice(end.index + end[0].length);
183
+ end = LINE.exec(buffer);
184
+ if (line === "") {
185
+ const payload = data.join(`
186
+ `);
187
+ data = [];
188
+ if (payload === "") {
189
+ event = undefined;
190
+ continue;
191
+ }
192
+ if (payload === "[DONE]")
193
+ return;
194
+ yield {
195
+ data: payload,
196
+ ...event === undefined ? {} : { event },
197
+ ...id === undefined ? {} : { id },
198
+ ...retry === undefined ? {} : { retry }
199
+ };
200
+ event = undefined;
201
+ continue;
202
+ }
203
+ if (line.startsWith(":"))
204
+ continue;
205
+ const [field, value] = split(line);
206
+ if (field === "data")
207
+ data.push(value);
208
+ else if (field === "event")
209
+ event = value;
210
+ else if (field === "id" && !value.includes("\x00"))
211
+ id = value;
212
+ else if (field === "retry" && /^\d+$/.test(value))
213
+ retry = Number(value);
164
214
  }
165
215
  }
166
- throw lastError;
167
- };
216
+ }
217
+ class ConnectDeadline {
218
+ #controller = new AbortController;
219
+ #timer;
220
+ constructor(ms, target) {
221
+ this.#timer = ms > 0 ? setTimeout(() => {
222
+ this.#controller.abort(new DOMException(`Connecting to ${target} timed out after ${ms}ms`, "TimeoutError"));
223
+ }, ms) : undefined;
224
+ }
225
+ get signal() {
226
+ return this.#controller.signal;
227
+ }
228
+ clear() {
229
+ if (this.#timer !== undefined)
230
+ clearTimeout(this.#timer);
231
+ }
232
+ }
233
+ Object.defineProperty(ConnectDeadline, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "ms: number" }, { unresolved: "target: string" }] });
168
234
 
169
235
  // src/client/service.ts
170
236
  class HttpService extends UrlHelper {
@@ -184,12 +250,14 @@ class HttpService extends UrlHelper {
184
250
  let status;
185
251
  const { body, serialised } = this.bodyFor(config.payload);
186
252
  const replayable = !(config.payload instanceof ReadableStream);
187
- const attempt = async () => {
253
+ const attempt = async (signal) => {
188
254
  attempts += 1;
189
- const response = await this.send(config, url, body, serialised);
255
+ const response = await this.send(config, url, body, serialised, signal);
190
256
  status = response.status;
191
257
  if (!response.ok) {
192
- throw new FetchError(response.status, response.statusText, await readBody(response), {
258
+ throw new FetchError(response.status, response.statusText, await readBody(response).catch(() => {
259
+ return;
260
+ }), {
193
261
  method: config.method,
194
262
  url: url.href,
195
263
  headers: response.headers
@@ -198,15 +266,16 @@ class HttpService extends UrlHelper {
198
266
  return await readBody(response);
199
267
  };
200
268
  const describe = () => `${config.method} ${url.href}`;
269
+ const policy = this.policyFor(config, {
270
+ ...this.options.retry,
271
+ ...config.retry,
272
+ ...replayable ? {} : { maxRetries: 0 }
273
+ });
201
274
  try {
202
275
  const result = await this.requestContext.runWithContext({
203
276
  ...config.flow === undefined ? {} : { flow: config.flow },
204
277
  event: config.path ?? url.pathname
205
- }, () => executeWithRetry(attempt, {
206
- ...this.options.retry,
207
- ...config.retry,
208
- ...replayable ? {} : { maxRetries: 0 }
209
- }));
278
+ }, () => policy.run(attempt));
210
279
  this.logger.debug(`${describe()} succeeded`, {
211
280
  status,
212
281
  attempts,
@@ -261,34 +330,29 @@ class HttpService extends UrlHelper {
261
330
  });
262
331
  }
263
332
  async* streamSse(config) {
333
+ for await (const message of this.streamSseEvents(config))
334
+ yield message.data;
335
+ }
336
+ async* streamSseEvents(config) {
264
337
  const url = this.urlFor(config);
265
338
  const method = config.method ?? "POST";
266
339
  const startedAt = Date.now();
267
340
  const { body, serialised } = this.bodyFor(config.payload);
268
- const response = await this.send({ ...config, method }, url, body, serialised, "text/event-stream");
341
+ const deadline = new ConnectDeadline(config.timeoutMs ?? this.options.timeoutMs, url.href);
342
+ let response;
343
+ try {
344
+ const policy = this.policyFor({ ...config, timeoutMs: 0 }, { maxRetries: 0 });
345
+ response = await policy.run((signal) => this.send({ ...config, method }, url, body, serialised, AbortSignal.any([signal, deadline.signal]), "text/event-stream"));
346
+ } finally {
347
+ deadline.clear();
348
+ }
269
349
  if (!response.ok || response.body === null) {
270
- throw new FetchError(response.status, response.statusText, await readBody(response), { method, url: url.href, headers: response.headers });
350
+ throw new FetchError(response.status, response.statusText, await readBody(response).catch(() => {
351
+ return;
352
+ }), { method, url: url.href, headers: response.headers });
271
353
  }
272
- const decoder = new TextDecoder;
273
- let buffer = "";
274
354
  try {
275
- for await (const chunk of response.body) {
276
- buffer += decoder.decode(chunk, { stream: true });
277
- let newline = buffer.indexOf(`
278
- `);
279
- while (newline !== -1) {
280
- const line = buffer.slice(0, newline).trim();
281
- buffer = buffer.slice(newline + 1);
282
- newline = buffer.indexOf(`
283
- `);
284
- if (!line.startsWith("data:"))
285
- continue;
286
- const data = line.slice(5).trim();
287
- if (data === "[DONE]")
288
- return;
289
- yield data;
290
- }
291
- }
355
+ yield* sseMessages(response.body);
292
356
  } finally {
293
357
  this.logger.debug(`SSE ${method} ${url.href} closed`, {
294
358
  elapsedMs: Date.now() - startedAt
@@ -313,6 +377,14 @@ class HttpService extends UrlHelper {
313
377
  ...config.queryParams === undefined ? {} : { queryParams: config.queryParams }
314
378
  });
315
379
  }
380
+ policyFor(config, retry) {
381
+ return new ResiliencePolicy(new ResilienceOptions({
382
+ timeoutMs: config.timeoutMs ?? this.options.timeoutMs,
383
+ ...config.signal === undefined ? {} : { signal: config.signal },
384
+ retry,
385
+ classifier: new HttpRetryClassifier(retry)
386
+ }));
387
+ }
316
388
  bodyFor(payload) {
317
389
  if (payload === undefined || payload === null) {
318
390
  return { body: undefined, serialised: "", json: false };
@@ -323,7 +395,7 @@ class HttpService extends UrlHelper {
323
395
  const serialised = JSON.stringify(payload);
324
396
  return { body: serialised, serialised, json: true };
325
397
  }
326
- async send(config, url, body, serialised, accept = "application/json") {
398
+ async send(config, url, body, serialised, signal, accept = "application/json") {
327
399
  const trace = this.options.propagateTrace ? this.requestContext.getContext() : undefined;
328
400
  const headers = {
329
401
  accept,
@@ -345,17 +417,12 @@ class HttpService extends UrlHelper {
345
417
  }),
346
418
  ...config.headers
347
419
  };
348
- const timeoutMs = config.timeoutMs ?? this.options.timeoutMs;
349
- const signals = [
350
- ...timeoutMs > 0 ? [AbortSignal.timeout(timeoutMs)] : [],
351
- ...config.signal === undefined ? [] : [config.signal]
352
- ];
353
420
  try {
354
421
  return await fetch(url.href, {
355
422
  method: config.method,
356
423
  headers,
357
424
  ...body === undefined ? {} : { body },
358
- ...signals.length === 0 ? {} : { signal: AbortSignal.any(signals) },
425
+ signal,
359
426
  ...this.options.fetchOptions
360
427
  });
361
428
  } catch (error) {
@@ -366,16 +433,6 @@ class HttpService extends UrlHelper {
366
433
  }
367
434
  Object.defineProperty(HttpService, Symbol.for("dunx.deps"), { value: () => [HttpClientOptions, Logger, RequestContext] });
368
435
  var urlOf = (url) => url === undefined ? {} : { url };
369
- var readBody = async (response) => {
370
- const text = await response.text().catch(() => "");
371
- if (text === "")
372
- return;
373
- try {
374
- return JSON.parse(text);
375
- } catch {
376
- return text;
377
- }
378
- };
379
436
  var describeError = (error) => {
380
437
  if (error instanceof FetchError) {
381
438
  return {
@@ -458,6 +515,7 @@ export {
458
515
  FetchTransportError,
459
516
  HttpClientOptions,
460
517
  HttpModule,
518
+ HttpRetryClassifier,
461
519
  HttpService,
462
520
  httpClient
463
521
  };
@@ -0,0 +1,25 @@
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` is the one that does **not** cover an RPC: it returns early on
16
+ * every unmatched path so a burst of 404s cannot spend a caller's budget, and an
17
+ * RPC is unmatched. See docs/guide/27-rpc.md.
18
+ */
19
+ export declare class ConnectMiddleware implements Middleware, ClaimsPaths {
20
+ #private;
21
+ constructor(registry: ConnectRegistry);
22
+ /** Every mounted RPC path, so a controller cannot shadow one unnoticed. */
23
+ claimedPaths(): readonly string[];
24
+ handle(req: BunRequest, ctx: RouteContext, next: Next): Promise<Response>;
25
+ }
@@ -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,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';