@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.
- package/README.md +14 -4
- 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/client/json.d.ts +12 -11
- package/dist/client/options.d.ts +3 -3
- package/dist/client/retry.d.ts +17 -31
- package/dist/client/service.d.ts +21 -12
- package/dist/client/sse.d.ts +39 -0
- package/dist/client.d.ts +10 -1
- package/dist/client.js +166 -108
- package/dist/connect/middleware.d.ts +25 -0
- package/dist/connect/module.d.ts +29 -0
- package/dist/connect/options.d.ts +64 -0
- package/dist/connect/registry.d.ts +47 -0
- package/dist/connect.d.ts +12 -0
- package/dist/connect.js +208 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +253 -184
- 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/middleware.d.ts +14 -2
- package/dist/server/options-provider.d.ts +3 -0
- package/dist/server/options.d.ts +9 -1
- package/dist/server/routes.d.ts +7 -2
- package/dist/sse/decorators.d.ts +28 -0
- package/dist/sse/event.d.ts +19 -0
- package/dist/sse/stream.d.ts +35 -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/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 {
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
}, () =>
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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';
|