@dunx/http 3.7.0 → 3.8.1
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 +2 -2
- package/dist/chunk-cx4btdwe.js +56 -0
- package/dist/client/sse.d.ts +4 -3
- package/dist/client.js +49 -32
- package/dist/connect/middleware.d.ts +4 -3
- package/dist/connect/options.d.ts +3 -4
- package/dist/connect.js +9 -4
- package/dist/index.js +63 -9
- package/dist/server/metrics.d.ts +2 -0
- package/dist/server/middleware.d.ts +5 -0
- package/dist/server/trace-context.d.ts +9 -11
- package/dist/sse/event.d.ts +2 -1
- package/dist/sse/stream.d.ts +3 -0
- package/package.json +2 -2
- package/dist/chunk-gmtwad7f.js +0 -71
package/README.md
CHANGED
|
@@ -108,8 +108,8 @@ from, and it may change in any release.
|
|
|
108
108
|
`grpc-status` in an HTTP trailer and `Bun.serve` sends no trailers, so a request
|
|
109
109
|
with `content-type: application/grpc` gets a 415 that says so.
|
|
110
110
|
`@connectrpc/connect` and `@bufbuild/protobuf` are optional peers, and the
|
|
111
|
-
`.proto` toolchain stays yours. `ThrottleGuard`
|
|
112
|
-
|
|
111
|
+
`.proto` toolchain stays yours. `ThrottleGuard` covers an RPC: it skips an
|
|
112
|
+
unmatched path nobody claims, and an RPC path is claimed.
|
|
113
113
|
|
|
114
114
|
## License
|
|
115
115
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/server/trace-context.ts
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_TRACE_FLAGS,
|
|
5
|
+
formatTraceparent,
|
|
6
|
+
isSampled,
|
|
7
|
+
mintSpanId,
|
|
8
|
+
mintTraceId,
|
|
9
|
+
parseTraceparent,
|
|
10
|
+
TRACEPARENT_HEADER as TRACEPARENT_HEADER2,
|
|
11
|
+
TRACESTATE_HEADER as TRACESTATE_HEADER2
|
|
12
|
+
} from "@dunx/core";
|
|
13
|
+
var TRACERESPONSE_HEADER2 = "traceresponse";
|
|
14
|
+
var TRACE = Symbol.for("dunx.http.trace");
|
|
15
|
+
var EXPOSE = Symbol.for("dunx.http.trace.expose");
|
|
16
|
+
|
|
17
|
+
class TraceContext2 {
|
|
18
|
+
static adopt(req, expose = true) {
|
|
19
|
+
const inbound = parseTraceparent(req.headers.get(TRACEPARENT_HEADER2));
|
|
20
|
+
const state = req.headers.get(TRACESTATE_HEADER2);
|
|
21
|
+
const trace = inbound === undefined ? {
|
|
22
|
+
traceId: mintTraceId(),
|
|
23
|
+
spanId: mintSpanId(),
|
|
24
|
+
flags: DEFAULT_TRACE_FLAGS
|
|
25
|
+
} : {
|
|
26
|
+
traceId: inbound.traceId,
|
|
27
|
+
spanId: mintSpanId(),
|
|
28
|
+
parentSpanId: inbound.spanId,
|
|
29
|
+
flags: inbound.flags,
|
|
30
|
+
...state === null ? {} : { state }
|
|
31
|
+
};
|
|
32
|
+
req[TRACE] = trace;
|
|
33
|
+
if (expose)
|
|
34
|
+
req[EXPOSE] = true;
|
|
35
|
+
return trace;
|
|
36
|
+
}
|
|
37
|
+
static of(req) {
|
|
38
|
+
return req[TRACE];
|
|
39
|
+
}
|
|
40
|
+
static header(trace) {
|
|
41
|
+
return formatTraceparent(trace);
|
|
42
|
+
}
|
|
43
|
+
static stamp(response, req) {
|
|
44
|
+
const traced = req;
|
|
45
|
+
const trace = traced[TRACE];
|
|
46
|
+
if (trace !== undefined && traced[EXPOSE] === true) {
|
|
47
|
+
response.headers.set(TRACERESPONSE_HEADER2, TraceContext2.header(trace));
|
|
48
|
+
}
|
|
49
|
+
return response;
|
|
50
|
+
}
|
|
51
|
+
static sampled(trace) {
|
|
52
|
+
return isSampled(trace);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { TRACEPARENT_HEADER2, TRACESTATE_HEADER2, TRACERESPONSE_HEADER2, TraceContext2 };
|
package/dist/client/sse.d.ts
CHANGED
|
@@ -13,9 +13,10 @@ export interface SseMessage {
|
|
|
13
13
|
* Every event of a server-sent-events body, in order, ending with the stream or
|
|
14
14
|
* with `[DONE]`. One still being read when the body ends is dropped, per spec.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* `getReader()` rather than async iteration, so the last read is told apart from
|
|
17
|
+
* a chunk boundary: a trailing `\r` is half a `\r\n` in one and a line ending in
|
|
18
|
+
* the other. `releaseLock` in a `finally` covers a `break` and `[DONE]`.
|
|
19
|
+
* Hand-rolled: Bun exposes no `EventSource` and no SSE parser, measured.
|
|
19
20
|
*/
|
|
20
21
|
export declare function sseMessages(body: ReadableStream<Uint8Array>): AsyncGenerator<SseMessage>;
|
|
21
22
|
/**
|
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";
|
|
@@ -174,44 +174,61 @@ async function* sseMessages(body) {
|
|
|
174
174
|
let event;
|
|
175
175
|
let id;
|
|
176
176
|
let retry;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
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(`
|
|
186
191
|
`);
|
|
187
|
-
|
|
188
|
-
|
|
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
|
+
};
|
|
189
205
|
event = undefined;
|
|
190
206
|
continue;
|
|
191
207
|
}
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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);
|
|
202
219
|
}
|
|
203
|
-
if (
|
|
220
|
+
if (done)
|
|
221
|
+
return;
|
|
222
|
+
const next = await reader.read();
|
|
223
|
+
if (next.done) {
|
|
224
|
+
buffer += decoder.decode();
|
|
225
|
+
done = true;
|
|
204
226
|
continue;
|
|
205
|
-
|
|
206
|
-
|
|
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);
|
|
227
|
+
}
|
|
228
|
+
buffer += decoder.decode(next.value, { stream: true });
|
|
214
229
|
}
|
|
230
|
+
} finally {
|
|
231
|
+
reader.releaseLock();
|
|
215
232
|
}
|
|
216
233
|
}
|
|
217
234
|
class ConnectDeadline {
|
|
@@ -12,14 +12,15 @@ import { ConnectRegistry } from './registry.js';
|
|
|
12
12
|
* first is what leaves a matched route paying nothing. Anything outside the
|
|
13
13
|
* registered paths falls through untouched.
|
|
14
14
|
*
|
|
15
|
-
* `ThrottleGuard`
|
|
16
|
-
*
|
|
17
|
-
* RPC is unmatched. See docs/guide/27-rpc.md.
|
|
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.
|
|
18
17
|
*/
|
|
19
18
|
export declare class ConnectMiddleware implements Middleware, ClaimsPaths {
|
|
20
19
|
#private;
|
|
21
20
|
constructor(registry: ConnectRegistry);
|
|
22
21
|
/** Every mounted RPC path, so a controller cannot shadow one unnoticed. */
|
|
23
22
|
claimedPaths(): readonly string[];
|
|
23
|
+
/** Connect and gRPC-Web both POST. Connect's GET form is not served here. */
|
|
24
|
+
claimedMethods(): readonly string[];
|
|
24
25
|
handle(req: BunRequest, ctx: RouteContext, next: Next): Promise<Response>;
|
|
25
26
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DescService } from '@bufbuild/protobuf';
|
|
2
2
|
import type { ConnectRouterOptions, ServiceImpl } from '@connectrpc/connect';
|
|
3
|
-
import type
|
|
3
|
+
import { type Ctor, type ModuleRef } from '@dunx/core';
|
|
4
4
|
/**
|
|
5
5
|
* Everything `createConnectRouter` takes except the protocol switches and
|
|
6
6
|
* `shutdownSignal`, which the container owns. `interceptors`, `contextValues`,
|
|
@@ -43,9 +43,8 @@ export interface ConnectOptionsInit extends ConnectRouterSettings {
|
|
|
43
43
|
* the deadline for the whole call, which is the default because a gap between
|
|
44
44
|
* messages is the protocol rather than a symptom.
|
|
45
45
|
*
|
|
46
|
-
* Worth setting on a public mount: `ThrottleGuard`
|
|
47
|
-
*
|
|
48
|
-
* or how many streams one caller holds open.
|
|
46
|
+
* Worth setting on a public mount: `ThrottleGuard` limits the calls, and this
|
|
47
|
+
* limits how long one that is already streaming may idle.
|
|
49
48
|
*
|
|
50
49
|
* @default 0
|
|
51
50
|
*/
|
package/dist/connect.js
CHANGED
|
@@ -16,12 +16,14 @@ import {
|
|
|
16
16
|
} from "./chunk-08k9vq31.js";
|
|
17
17
|
|
|
18
18
|
// src/connect/registry.ts
|
|
19
|
+
import { AppError as AppError2 } from "@dunx/core";
|
|
19
20
|
import {
|
|
20
21
|
createConnectRouter
|
|
21
22
|
} from "@connectrpc/connect";
|
|
22
23
|
import { createFetchHandler } from "@connectrpc/connect/protocol";
|
|
23
24
|
|
|
24
25
|
// src/connect/options.ts
|
|
26
|
+
import { AppError } from "@dunx/core";
|
|
25
27
|
var connectService = (service, useClass) => ({ service, useClass });
|
|
26
28
|
|
|
27
29
|
class ConnectOptions {
|
|
@@ -48,14 +50,14 @@ class ConnectOptions {
|
|
|
48
50
|
this.grpcWeb = grpcWeb ?? true;
|
|
49
51
|
this.streamTimeout = streamTimeout ?? 0;
|
|
50
52
|
if (!Number.isFinite(this.streamTimeout) || this.streamTimeout < 0) {
|
|
51
|
-
throw new
|
|
53
|
+
throw new AppError(`ConnectModule streamTimeout must be a non-negative number of seconds, got ${String(streamTimeout)}.`);
|
|
52
54
|
}
|
|
53
55
|
this.router = router;
|
|
54
56
|
if (!this.connect && !this.grpcWeb) {
|
|
55
|
-
throw new
|
|
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.");
|
|
56
58
|
}
|
|
57
59
|
if (services.length === 0) {
|
|
58
|
-
throw new
|
|
60
|
+
throw new AppError("ConnectModule.forRoot was given no services. Pass at least one " + "connectService(Desc, Impl), or drop the module.");
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
}
|
|
@@ -82,7 +84,7 @@ class ConnectRegistry {
|
|
|
82
84
|
options.services.forEach((registration, index) => {
|
|
83
85
|
const implementation = implementations[index];
|
|
84
86
|
if (implementation === undefined) {
|
|
85
|
-
throw new
|
|
87
|
+
throw new AppError2(`No instance was resolved for ${registration.useClass.name}, which ` + `serves ${registration.service.typeName}.`);
|
|
86
88
|
}
|
|
87
89
|
router.service(registration.service, implementation);
|
|
88
90
|
});
|
|
@@ -131,6 +133,9 @@ class ConnectMiddleware {
|
|
|
131
133
|
claimedPaths() {
|
|
132
134
|
return this.#registry.paths;
|
|
133
135
|
}
|
|
136
|
+
claimedMethods() {
|
|
137
|
+
return ["POST"];
|
|
138
|
+
}
|
|
134
139
|
handle(req, ctx, next) {
|
|
135
140
|
if (ctx.get(UNMATCHED2) !== true)
|
|
136
141
|
return next();
|
package/dist/index.js
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
TRACESTATE_HEADER2,
|
|
38
38
|
TRACERESPONSE_HEADER2,
|
|
39
39
|
TraceContext2
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-cx4btdwe.js";
|
|
41
41
|
import {
|
|
42
42
|
__privateGet,
|
|
43
43
|
__privateAdd,
|
|
@@ -189,16 +189,21 @@ var seriesFor = (route, method) => ({
|
|
|
189
189
|
class RequestMetrics {
|
|
190
190
|
#series = new Map;
|
|
191
191
|
#unmatched = new Map;
|
|
192
|
+
#claimed = new Map;
|
|
193
|
+
#claimedPaths = new Set;
|
|
192
194
|
#since = new Date;
|
|
193
195
|
#server;
|
|
194
196
|
observe(ctx, status, durationNs, traceId) {
|
|
195
197
|
let series = this.#series.get(ctx);
|
|
196
198
|
if (series === undefined) {
|
|
197
199
|
if (ctx.get(UNMATCHED2) === true) {
|
|
198
|
-
|
|
200
|
+
const claimed = this.#claimedPaths.has(ctx.path);
|
|
201
|
+
const key = claimed ? `${ctx.method} ${ctx.path}` : ctx.method;
|
|
202
|
+
const bucket = claimed ? this.#claimed : this.#unmatched;
|
|
203
|
+
series = bucket.get(key);
|
|
199
204
|
if (series === undefined) {
|
|
200
|
-
series = seriesFor(UNMATCHED_ROUTE, ctx.method);
|
|
201
|
-
|
|
205
|
+
series = seriesFor(claimed ? ctx.path : UNMATCHED_ROUTE, ctx.method);
|
|
206
|
+
bucket.set(key, series);
|
|
202
207
|
}
|
|
203
208
|
} else {
|
|
204
209
|
series = seriesFor(ctx.path, ctx.method);
|
|
@@ -218,6 +223,7 @@ class RequestMetrics {
|
|
|
218
223
|
const routes = [];
|
|
219
224
|
for (const series of [
|
|
220
225
|
...this.#series.values(),
|
|
226
|
+
...this.#claimed.values(),
|
|
221
227
|
...this.#unmatched.values()
|
|
222
228
|
]) {
|
|
223
229
|
routes.push({
|
|
@@ -238,9 +244,13 @@ class RequestMetrics {
|
|
|
238
244
|
}
|
|
239
245
|
reset() {
|
|
240
246
|
this.#series.clear();
|
|
247
|
+
this.#claimed.clear();
|
|
241
248
|
this.#unmatched.clear();
|
|
242
249
|
this.#since = new Date;
|
|
243
250
|
}
|
|
251
|
+
claim(paths) {
|
|
252
|
+
this.#claimedPaths = new Set(paths);
|
|
253
|
+
}
|
|
244
254
|
attach(server) {
|
|
245
255
|
this.#server = server;
|
|
246
256
|
}
|
|
@@ -1366,7 +1376,22 @@ var buildFallback = (middleware = [], onError = defaultErrorMapper, cors, notFou
|
|
|
1366
1376
|
const miss = () => {
|
|
1367
1377
|
throw new HttpError(HttpStatusCode2.NOT_FOUND, "NOT_FOUND");
|
|
1368
1378
|
};
|
|
1379
|
+
const claimedPreflight = new Map;
|
|
1380
|
+
if (cors) {
|
|
1381
|
+
for (const entry of middleware) {
|
|
1382
|
+
if (!hasClaimedPaths(entry))
|
|
1383
|
+
continue;
|
|
1384
|
+
const answer = preflight(cors, entry.claimedMethods());
|
|
1385
|
+
for (const path of entry.claimedPaths())
|
|
1386
|
+
claimedPreflight.set(path, answer);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1369
1389
|
const run = async (req, server) => {
|
|
1390
|
+
if (req.method === "OPTIONS") {
|
|
1391
|
+
const answer = claimedPreflight.get(new URL(req.url).pathname);
|
|
1392
|
+
if (answer !== undefined)
|
|
1393
|
+
return answer(req);
|
|
1394
|
+
}
|
|
1370
1395
|
try {
|
|
1371
1396
|
return await compose(middleware, unmatchedContext(req, notFound === "public", server), miss)(req);
|
|
1372
1397
|
} catch (error) {
|
|
@@ -1645,7 +1670,9 @@ class HttpApplication extends ShutdownAware {
|
|
|
1645
1670
|
if (ws && !this.#split)
|
|
1646
1671
|
assertNoGatewayCollisions(prefixed, ws.paths);
|
|
1647
1672
|
assertNoShadowedClaims(prefixed, middleware);
|
|
1648
|
-
|
|
1673
|
+
const claimed = middleware.flatMap((entry) => hasClaimedPaths(entry) ? [...entry.claimedPaths()] : []);
|
|
1674
|
+
this.#app.get(ClaimedRoutes).attach(claimed);
|
|
1675
|
+
this.#app.get(RequestMetrics).claim(claimed);
|
|
1649
1676
|
const fetch = buildFallback(middleware, this.#onError, this.#cors, this.#notFound);
|
|
1650
1677
|
const bound = this.#binding.bind({ port, routes, fetch, websocket: ws });
|
|
1651
1678
|
attachAddressSource(this.#app.get(ClientAddress), {
|
|
@@ -2240,6 +2267,8 @@ var frameEvent = (event) => {
|
|
|
2240
2267
|
const data = typeof event.data === "string" ? event.data : JSON.stringify(event.data);
|
|
2241
2268
|
for (const line of lines(data ?? ""))
|
|
2242
2269
|
fields.push(`data: ${line}`);
|
|
2270
|
+
} else if (event.event !== undefined || event.id !== undefined) {
|
|
2271
|
+
fields.push("data: ");
|
|
2243
2272
|
}
|
|
2244
2273
|
return `${fields.join(`
|
|
2245
2274
|
`)}
|
|
@@ -2266,11 +2295,17 @@ class SseStream {
|
|
|
2266
2295
|
#timer;
|
|
2267
2296
|
#lastEventId;
|
|
2268
2297
|
#closed = false;
|
|
2298
|
+
#gone = new AbortController;
|
|
2299
|
+
#drained;
|
|
2269
2300
|
constructor(options = {}) {
|
|
2270
2301
|
this.#body = new ReadableStream({
|
|
2271
2302
|
start: (controller) => {
|
|
2272
2303
|
this.#controller = controller;
|
|
2273
2304
|
},
|
|
2305
|
+
pull: () => {
|
|
2306
|
+
this.#drained?.();
|
|
2307
|
+
this.#drained = undefined;
|
|
2308
|
+
},
|
|
2274
2309
|
cancel: () => {
|
|
2275
2310
|
this.#stop();
|
|
2276
2311
|
}
|
|
@@ -2292,6 +2327,7 @@ class SseStream {
|
|
|
2292
2327
|
if (stream.#closed)
|
|
2293
2328
|
break;
|
|
2294
2329
|
stream.send(event);
|
|
2330
|
+
await stream.#backpressure();
|
|
2295
2331
|
}
|
|
2296
2332
|
stream.close();
|
|
2297
2333
|
} catch (error) {
|
|
@@ -2300,6 +2336,9 @@ class SseStream {
|
|
|
2300
2336
|
})();
|
|
2301
2337
|
return stream;
|
|
2302
2338
|
}
|
|
2339
|
+
get signal() {
|
|
2340
|
+
return this.#gone.signal;
|
|
2341
|
+
}
|
|
2303
2342
|
get lastEventId() {
|
|
2304
2343
|
return this.#lastEventId;
|
|
2305
2344
|
}
|
|
@@ -2321,9 +2360,11 @@ class SseStream {
|
|
|
2321
2360
|
this.#controller?.close();
|
|
2322
2361
|
}
|
|
2323
2362
|
toResponse(headers = {}) {
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2363
|
+
const merged = new Headers(headers);
|
|
2364
|
+
for (const [name, value] of Object.entries(SSE_HEADERS)) {
|
|
2365
|
+
merged.set(name, value);
|
|
2366
|
+
}
|
|
2367
|
+
return new Response(this.#body, { headers: merged });
|
|
2327
2368
|
}
|
|
2328
2369
|
#write(text) {
|
|
2329
2370
|
if (this.#closed)
|
|
@@ -2336,8 +2377,20 @@ class SseStream {
|
|
|
2336
2377
|
this.#stop();
|
|
2337
2378
|
this.#controller?.error(error);
|
|
2338
2379
|
}
|
|
2380
|
+
#backpressure() {
|
|
2381
|
+
const wanted = this.#controller?.desiredSize;
|
|
2382
|
+
if (this.#closed || wanted === undefined || wanted === null || wanted > 0) {
|
|
2383
|
+
return Promise.resolve();
|
|
2384
|
+
}
|
|
2385
|
+
return new Promise((resolve) => {
|
|
2386
|
+
this.#drained = resolve;
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2339
2389
|
#stop() {
|
|
2340
2390
|
this.#closed = true;
|
|
2391
|
+
this.#gone.abort();
|
|
2392
|
+
this.#drained?.();
|
|
2393
|
+
this.#drained = undefined;
|
|
2341
2394
|
if (this.#timer !== undefined)
|
|
2342
2395
|
clearInterval(this.#timer);
|
|
2343
2396
|
this.#timer = undefined;
|
|
@@ -2512,7 +2565,8 @@ class ThrottleGuard {
|
|
|
2512
2565
|
}
|
|
2513
2566
|
#key(req, ctx) {
|
|
2514
2567
|
const subject = (this.options.subject ?? ((request) => this.address.of(request)))(req, ctx) ?? "anonymous";
|
|
2515
|
-
|
|
2568
|
+
const scope = ctx.get(UNMATCHED2) === true ? ctx.path : `${ctx.controller}:${ctx.handler}`;
|
|
2569
|
+
return `${this.options.prefix}:throttle:${scope}:${subject}`;
|
|
2516
2570
|
}
|
|
2517
2571
|
async#hit(key, windowSeconds) {
|
|
2518
2572
|
try {
|
package/dist/server/metrics.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ export declare class RequestMetrics {
|
|
|
54
54
|
* reflecting a deploy three days ago; who calls this is the app's decision.
|
|
55
55
|
*/
|
|
56
56
|
reset(): void;
|
|
57
|
+
/** Internal: the paths a middleware answers off the fallback. */
|
|
58
|
+
claim(paths: Iterable<string>): void;
|
|
57
59
|
/**
|
|
58
60
|
* Internal: `listen()` hands the bound server's counters to the resolved
|
|
59
61
|
* singleton. Structural, so an app serving from two ports can hand over the
|
|
@@ -28,5 +28,10 @@ export declare const compose: (middleware: readonly Middleware[], ctx: RouteCont
|
|
|
28
28
|
*/
|
|
29
29
|
export interface ClaimsPaths {
|
|
30
30
|
claimedPaths(): readonly string[];
|
|
31
|
+
/**
|
|
32
|
+
* The methods those paths answer. `preflight` is mounted over the route table,
|
|
33
|
+
* which a claimed path is not in, so an `OPTIONS` would reach the 404.
|
|
34
|
+
*/
|
|
35
|
+
claimedMethods(): readonly string[];
|
|
31
36
|
}
|
|
32
37
|
export declare const hasClaimedPaths: (value: object) => value is ClaimsPaths;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
import { TRACEPARENT_HEADER, TRACESTATE_HEADER, type TraceIds } from '@dunx/core';
|
|
2
|
+
export { TRACEPARENT_HEADER, TRACESTATE_HEADER };
|
|
3
3
|
/**
|
|
4
4
|
* The span that answered, sent back so a caller can record which of the callee's
|
|
5
5
|
* spans its own span points at. Same four fields as `traceparent`, and the
|
|
@@ -12,15 +12,14 @@ export declare const TRACESTATE_HEADER = "tracestate";
|
|
|
12
12
|
* and adoption is thin, so treat a caller reading it as a bonus.
|
|
13
13
|
*/
|
|
14
14
|
export declare const TRACERESPONSE_HEADER = "traceresponse";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
/**
|
|
16
|
+
* This server's view of a trace: the {@link TraceIds} on the wire plus the
|
|
17
|
+
* caller's span and any vendor `tracestate`, neither of which a `traceparent`
|
|
18
|
+
* carries on its own.
|
|
19
|
+
*/
|
|
20
|
+
export interface Trace extends TraceIds {
|
|
20
21
|
/** The caller's span, when one arrived in `traceparent`. */
|
|
21
22
|
readonly parentSpanId?: string;
|
|
22
|
-
/** Two hex digits. Bit 0 is `sampled`. */
|
|
23
|
-
readonly flags: string;
|
|
24
23
|
/** `tracestate` verbatim, when one arrived. Vendor data this server does not read. */
|
|
25
24
|
readonly state?: string;
|
|
26
25
|
}
|
|
@@ -41,7 +40,6 @@ export interface Trace {
|
|
|
41
40
|
* request carries no correlation id at all.
|
|
42
41
|
*/
|
|
43
42
|
export declare class TraceContext {
|
|
44
|
-
#private;
|
|
45
43
|
/**
|
|
46
44
|
* The inbound `traceparent`, or a fresh trace. A malformed header is discarded
|
|
47
45
|
* rather than repaired, as the standard requires. Version `ff` is invalid; a
|
|
@@ -63,7 +61,7 @@ export declare class TraceContext {
|
|
|
63
61
|
* The `traceparent` to send upstream. This server's span becomes the callee's
|
|
64
62
|
* parent, so the two link without inventing a span nothing logged.
|
|
65
63
|
*/
|
|
66
|
-
static header(trace:
|
|
64
|
+
static header(trace: TraceIds): string;
|
|
67
65
|
/**
|
|
68
66
|
* The response, carrying `traceresponse` if this request adopted a trace.
|
|
69
67
|
*
|
package/dist/sse/event.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One server-sent event. Every field is optional: `retry` alone changes the
|
|
3
|
-
* reconnection delay, `event` alone
|
|
3
|
+
* reconnection delay, and `event` or `id` alone carries an empty `data:` so it
|
|
4
|
+
* dispatches, which a frame with no data field at all does not.
|
|
4
5
|
*/
|
|
5
6
|
export interface SseEvent {
|
|
6
7
|
/** A string as it is, anything else through `JSON.stringify`. Each line of it
|
package/dist/sse/stream.d.ts
CHANGED
|
@@ -20,6 +20,9 @@ export declare class SseStream {
|
|
|
20
20
|
* terminal chunk and an `EventSource` reconnects.
|
|
21
21
|
*/
|
|
22
22
|
static from(events: AsyncIterable<SseEvent>, options?: SseStreamOptions): SseStream;
|
|
23
|
+
/** Aborts when the client goes away, for a handler that waits between events
|
|
24
|
+
* to race: `for await` only sees a disconnect between yields. */
|
|
25
|
+
get signal(): AbortSignal;
|
|
23
26
|
/** The `id` of the last event sent, or `undefined` if none carried one. */
|
|
24
27
|
get lastEventId(): string | undefined;
|
|
25
28
|
get closed(): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dunx/http",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.1",
|
|
4
4
|
"description": "Bun.serve adapter for the dunx framework: controllers, middleware and WebSocket gateways",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"peerDependencies": {
|
|
77
77
|
"@bufbuild/protobuf": "^2.15.0",
|
|
78
78
|
"@connectrpc/connect": "^2.2.0",
|
|
79
|
-
"@dunx/core": "^3.
|
|
79
|
+
"@dunx/core": "^3.8.1",
|
|
80
80
|
"@types/bun": ">=1.4.1"
|
|
81
81
|
},
|
|
82
82
|
"peerDependenciesMeta": {
|
package/dist/chunk-gmtwad7f.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
// src/server/trace-context.ts
|
|
3
|
-
var TRACEPARENT_HEADER2 = "traceparent";
|
|
4
|
-
var TRACESTATE_HEADER2 = "tracestate";
|
|
5
|
-
var TRACERESPONSE_HEADER2 = "traceresponse";
|
|
6
|
-
var HEX_32 = /^[0-9a-f]{32}$/;
|
|
7
|
-
var HEX_16 = /^[0-9a-f]{16}$/;
|
|
8
|
-
var HEX_2 = /^[0-9a-f]{2}$/;
|
|
9
|
-
var ZERO_TRACE = "0".repeat(32);
|
|
10
|
-
var ZERO_SPAN = "0".repeat(16);
|
|
11
|
-
var SAMPLED = 1;
|
|
12
|
-
var DEFAULT_FLAGS = "01";
|
|
13
|
-
var TRACE = Symbol.for("dunx.http.trace");
|
|
14
|
-
var EXPOSE = Symbol.for("dunx.http.trace.expose");
|
|
15
|
-
var mint = (bytes) => crypto.getRandomValues(new Uint8Array(bytes)).toHex();
|
|
16
|
-
|
|
17
|
-
class TraceContext2 {
|
|
18
|
-
static adopt(req, expose = true) {
|
|
19
|
-
const inbound = TraceContext2.#parse(req.headers.get(TRACEPARENT_HEADER2));
|
|
20
|
-
const state = req.headers.get(TRACESTATE_HEADER2);
|
|
21
|
-
const trace = inbound === undefined ? { traceId: mint(16), spanId: mint(8), flags: DEFAULT_FLAGS } : {
|
|
22
|
-
traceId: inbound.traceId,
|
|
23
|
-
spanId: mint(8),
|
|
24
|
-
parentSpanId: inbound.spanId,
|
|
25
|
-
flags: inbound.flags,
|
|
26
|
-
...state === null ? {} : { state }
|
|
27
|
-
};
|
|
28
|
-
req[TRACE] = trace;
|
|
29
|
-
if (expose)
|
|
30
|
-
req[EXPOSE] = true;
|
|
31
|
-
return trace;
|
|
32
|
-
}
|
|
33
|
-
static of(req) {
|
|
34
|
-
return req[TRACE];
|
|
35
|
-
}
|
|
36
|
-
static header(trace) {
|
|
37
|
-
return `00-${trace.traceId}-${trace.spanId}-${trace.flags}`;
|
|
38
|
-
}
|
|
39
|
-
static stamp(response, req) {
|
|
40
|
-
const traced = req;
|
|
41
|
-
const trace = traced[TRACE];
|
|
42
|
-
if (trace !== undefined && traced[EXPOSE] === true) {
|
|
43
|
-
response.headers.set(TRACERESPONSE_HEADER2, TraceContext2.header(trace));
|
|
44
|
-
}
|
|
45
|
-
return response;
|
|
46
|
-
}
|
|
47
|
-
static sampled(trace) {
|
|
48
|
-
return (Number.parseInt(trace.flags, 16) & SAMPLED) === SAMPLED;
|
|
49
|
-
}
|
|
50
|
-
static #parse(header) {
|
|
51
|
-
if (header === null)
|
|
52
|
-
return;
|
|
53
|
-
const parts = header.split("-");
|
|
54
|
-
if (parts.length < 4)
|
|
55
|
-
return;
|
|
56
|
-
const [version, traceId, spanId, flags] = parts;
|
|
57
|
-
if (!HEX_2.test(version) || version === "ff")
|
|
58
|
-
return;
|
|
59
|
-
if (version === "00" && parts.length !== 4)
|
|
60
|
-
return;
|
|
61
|
-
if (!HEX_32.test(traceId) || traceId === ZERO_TRACE)
|
|
62
|
-
return;
|
|
63
|
-
if (!HEX_16.test(spanId) || spanId === ZERO_SPAN)
|
|
64
|
-
return;
|
|
65
|
-
if (!HEX_2.test(flags))
|
|
66
|
-
return;
|
|
67
|
-
return { traceId, spanId, flags };
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export { TRACEPARENT_HEADER2, TRACESTATE_HEADER2, TRACERESPONSE_HEADER2, TraceContext2 };
|