@dunx/http 3.1.3 → 3.2.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 +14 -4
- package/dist/{chunk-25g22350.js → chunk-3j2n1n11.js} +22 -9
- package/dist/chunk-3nbj06q8.js +174 -0
- package/dist/client/options.d.ts +7 -13
- package/dist/client/service.d.ts +2 -2
- package/dist/client.d.ts +1 -1
- package/dist/client.js +127 -18
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1325 -110
- package/dist/inspect.d.ts +9 -0
- package/dist/internal.d.ts +8 -25
- package/dist/internal.js +11 -83
- package/dist/server/application.d.ts +10 -3
- package/dist/server/html.d.ts +10 -0
- package/dist/server/metrics.d.ts +80 -0
- package/dist/server/options-provider.d.ts +2 -0
- package/dist/server/request-logging.d.ts +26 -12
- package/dist/server/trace-context.d.ts +41 -10
- package/dist/ws/decorators.d.ts +1 -1
- package/dist/ws/postgres-relay.d.ts +0 -2
- package/dist/ws/relay.d.ts +8 -0
- package/package.json +7 -4
- package/dist/chunk-53cs6qek.js +0 -1316
- package/dist/chunk-7xrtwbx3.js +0 -130
- package/dist/server/request-id.d.ts +0 -22
package/dist/index.js
CHANGED
|
@@ -1,74 +1,58 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
TRACEPARENT_HEADER,
|
|
4
|
+
TRACERESPONSE_HEADER,
|
|
4
5
|
TRACESTATE_HEADER,
|
|
5
6
|
TraceContext
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-3j2n1n11.js";
|
|
7
8
|
import {
|
|
8
9
|
ApiHidden,
|
|
9
|
-
CompressionEncoding,
|
|
10
|
-
CompressionOptions,
|
|
11
|
-
Controller,
|
|
12
|
-
DEFAULT_RELAY_CHANNEL,
|
|
13
|
-
Delete,
|
|
14
|
-
ErrorFilter,
|
|
15
|
-
Get,
|
|
16
|
-
HEALTH_REPORT_SCHEMA,
|
|
17
10
|
HIDDEN,
|
|
18
11
|
HandlerKind,
|
|
19
|
-
HealthController,
|
|
20
|
-
HealthOptions,
|
|
21
|
-
HealthRegistry,
|
|
22
|
-
HiddenHealthController,
|
|
23
|
-
HttpError,
|
|
24
12
|
PUBLIC,
|
|
25
|
-
Patch,
|
|
26
|
-
Post,
|
|
27
13
|
Public,
|
|
28
|
-
Put,
|
|
29
|
-
REQUEST_ID_HEADER,
|
|
30
14
|
ROLES,
|
|
31
|
-
RawBody,
|
|
32
|
-
RedisRelay,
|
|
33
|
-
RequestIds,
|
|
34
15
|
Roles,
|
|
35
|
-
StaticOptions,
|
|
36
16
|
UNMATCHED,
|
|
37
17
|
UseGuards,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
assertNoCollisions,
|
|
41
|
-
assertNoGatewayCollisions,
|
|
42
|
-
buildFallback,
|
|
43
|
-
buildRoutes,
|
|
44
|
-
buildWebSocket,
|
|
45
|
-
decodeRelay,
|
|
46
|
-
defaultErrorMapper,
|
|
47
|
-
defaultRelayError,
|
|
18
|
+
buildContext,
|
|
19
|
+
defaultStatusFor,
|
|
48
20
|
discoverGateways,
|
|
49
21
|
discoverRoutes,
|
|
50
|
-
encode,
|
|
51
|
-
encodeRelay,
|
|
52
|
-
errorMapper,
|
|
53
22
|
joinPath,
|
|
23
|
+
markController,
|
|
54
24
|
markGateway,
|
|
55
25
|
markHandler,
|
|
26
|
+
markRoute,
|
|
56
27
|
mergeMeta,
|
|
57
28
|
meta,
|
|
58
29
|
metaKey,
|
|
59
|
-
metaOf
|
|
60
|
-
|
|
61
|
-
observe,
|
|
62
|
-
toErrorMapper,
|
|
63
|
-
withUpgradeRoutes
|
|
64
|
-
} from "./chunk-53cs6qek.js";
|
|
30
|
+
metaOf
|
|
31
|
+
} from "./chunk-3nbj06q8.js";
|
|
65
32
|
import {
|
|
66
33
|
HttpStatusCode,
|
|
67
34
|
__decorateElement,
|
|
68
35
|
__decoratorMetadata,
|
|
69
36
|
__decoratorStart,
|
|
37
|
+
__privateAdd,
|
|
38
|
+
__privateGet,
|
|
70
39
|
__runInitializers
|
|
71
40
|
} from "./chunk-sz4pvqxy.js";
|
|
41
|
+
|
|
42
|
+
// src/route/decorators.ts
|
|
43
|
+
var Controller = (prefix = "") => (target) => {
|
|
44
|
+
markController(target, prefix);
|
|
45
|
+
return target;
|
|
46
|
+
};
|
|
47
|
+
var verb = (method) => (path = "/", options) => (value, _context) => {
|
|
48
|
+
markRoute(value, { method, path, options });
|
|
49
|
+
return value;
|
|
50
|
+
};
|
|
51
|
+
var Get = verb("GET");
|
|
52
|
+
var Post = verb("POST");
|
|
53
|
+
var Put = verb("PUT");
|
|
54
|
+
var Patch = verb("PATCH");
|
|
55
|
+
var Delete = verb("DELETE");
|
|
72
56
|
// src/server/client-address.ts
|
|
73
57
|
import { AppError } from "@dunx/core";
|
|
74
58
|
var trustedHops = (setting) => {
|
|
@@ -101,10 +85,67 @@ class ClientAddress {
|
|
|
101
85
|
var attachAddressSource = (target, source) => {
|
|
102
86
|
sources.set(target, source);
|
|
103
87
|
};
|
|
88
|
+
// src/server/errors.ts
|
|
89
|
+
import { AppError as AppError2, ConsoleLogger } from "@dunx/core";
|
|
90
|
+
class HttpError extends AppError2 {
|
|
91
|
+
status;
|
|
92
|
+
name = "HttpError";
|
|
93
|
+
headers;
|
|
94
|
+
constructor(status, message, options) {
|
|
95
|
+
super(message, options);
|
|
96
|
+
this.status = status;
|
|
97
|
+
this.headers = options?.headers;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
Object.defineProperty(HttpError, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "override readonly status: number" }, { unresolved: "message: string" }, { unresolved: "options?: HttpErrorOptions" }] });
|
|
101
|
+
|
|
102
|
+
class ValidationError extends HttpError {
|
|
103
|
+
source;
|
|
104
|
+
issues;
|
|
105
|
+
name = "ValidationError";
|
|
106
|
+
constructor(source, issues) {
|
|
107
|
+
super(HttpStatusCode.BAD_REQUEST, `Invalid ${source}`);
|
|
108
|
+
this.source = source;
|
|
109
|
+
this.issues = issues;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
Object.defineProperty(ValidationError, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "readonly source: InputSource" }, { unresolved: "readonly issues: readonly ValidationIssue[]" }] });
|
|
113
|
+
|
|
114
|
+
class ErrorFilter {
|
|
115
|
+
}
|
|
116
|
+
var isErrorFilter = (handler) => typeof handler === "function" && typeof handler.prototype?.catch === "function";
|
|
117
|
+
var toErrorMapper = (handler, resolve) => isErrorFilter(handler) ? (error, req) => resolve(handler).catch(error, req) : handler;
|
|
118
|
+
var errorMapper = (logger) => (error) => {
|
|
119
|
+
if (error instanceof ValidationError) {
|
|
120
|
+
return Response.json({ error: error.message, status: error.status, issues: error.issues }, {
|
|
121
|
+
status: error.status,
|
|
122
|
+
...error.headers && { headers: error.headers }
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (error instanceof HttpError) {
|
|
126
|
+
return Response.json({ error: error.message, status: error.status }, {
|
|
127
|
+
status: error.status,
|
|
128
|
+
...error.headers && { headers: error.headers }
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (error instanceof AppError2 && isStatus(error.status)) {
|
|
132
|
+
if (error.status >= HttpStatusCode.INTERNAL_SERVER_ERROR) {
|
|
133
|
+
logger.error("Unhandled error", error);
|
|
134
|
+
}
|
|
135
|
+
return Response.json({ error: error.message, status: error.status }, { status: error.status });
|
|
136
|
+
}
|
|
137
|
+
logger.error("Unhandled error", error);
|
|
138
|
+
return Response.json({
|
|
139
|
+
error: "Internal Server Error",
|
|
140
|
+
status: HttpStatusCode.INTERNAL_SERVER_ERROR
|
|
141
|
+
}, { status: HttpStatusCode.INTERNAL_SERVER_ERROR });
|
|
142
|
+
};
|
|
143
|
+
var isStatus = (value) => value !== undefined && Number.isInteger(value) && value >= 200 && value <= 599;
|
|
144
|
+
var defaultErrorMapper = errorMapper(new ConsoleLogger);
|
|
104
145
|
// src/server/factory.ts
|
|
105
146
|
import {
|
|
106
147
|
collectModules,
|
|
107
|
-
AppError as
|
|
148
|
+
AppError as AppError8,
|
|
108
149
|
AppFactory,
|
|
109
150
|
Logger as Logger4,
|
|
110
151
|
provide,
|
|
@@ -112,6 +153,395 @@ import {
|
|
|
112
153
|
RequestContext as RequestContext3
|
|
113
154
|
} from "@dunx/core";
|
|
114
155
|
|
|
156
|
+
// src/server/metrics.ts
|
|
157
|
+
import { Durations } from "@dunx/core";
|
|
158
|
+
var UNMATCHED_ROUTE = "(unmatched)";
|
|
159
|
+
var seriesFor = (route, method) => ({
|
|
160
|
+
route,
|
|
161
|
+
method,
|
|
162
|
+
count: 0,
|
|
163
|
+
byStatus: {},
|
|
164
|
+
duration: new Durations,
|
|
165
|
+
slowestNs: 0,
|
|
166
|
+
slowestTraceId: undefined
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
class RequestMetrics {
|
|
170
|
+
#series = new Map;
|
|
171
|
+
#unmatched = new Map;
|
|
172
|
+
#since = new Date;
|
|
173
|
+
#server;
|
|
174
|
+
observe(ctx, status, durationNs, traceId) {
|
|
175
|
+
let series = this.#series.get(ctx);
|
|
176
|
+
if (series === undefined) {
|
|
177
|
+
if (ctx.get(UNMATCHED) === true) {
|
|
178
|
+
series = this.#unmatched.get(ctx.method);
|
|
179
|
+
if (series === undefined) {
|
|
180
|
+
series = seriesFor(UNMATCHED_ROUTE, ctx.method);
|
|
181
|
+
this.#unmatched.set(ctx.method, series);
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
series = seriesFor(ctx.path, ctx.method);
|
|
185
|
+
this.#series.set(ctx, series);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
series.count += 1;
|
|
189
|
+
const key = String(status);
|
|
190
|
+
series.byStatus[key] = (series.byStatus[key] ?? 0) + 1;
|
|
191
|
+
series.duration.record(durationNs);
|
|
192
|
+
if (durationNs > series.slowestNs) {
|
|
193
|
+
series.slowestNs = durationNs;
|
|
194
|
+
series.slowestTraceId = traceId;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
snapshot() {
|
|
198
|
+
const routes = [];
|
|
199
|
+
for (const series of [
|
|
200
|
+
...this.#series.values(),
|
|
201
|
+
...this.#unmatched.values()
|
|
202
|
+
]) {
|
|
203
|
+
routes.push({
|
|
204
|
+
route: series.route,
|
|
205
|
+
method: series.method,
|
|
206
|
+
count: series.count,
|
|
207
|
+
byStatus: { ...series.byStatus },
|
|
208
|
+
duration: series.duration.snapshot(),
|
|
209
|
+
...series.slowestTraceId === undefined ? {} : { slowestTraceId: series.slowestTraceId }
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
routes,
|
|
214
|
+
inFlight: this.#server?.pendingRequests ?? 0,
|
|
215
|
+
pendingWebSockets: this.#server?.pendingWebSockets ?? 0,
|
|
216
|
+
since: this.#since.toISOString()
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
reset() {
|
|
220
|
+
this.#series.clear();
|
|
221
|
+
this.#unmatched.clear();
|
|
222
|
+
this.#since = new Date;
|
|
223
|
+
}
|
|
224
|
+
attach(server) {
|
|
225
|
+
this.#server = server;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
var usesMetricsMiddleware = (options) => options.metrics === true && options.requestLogging === false;
|
|
229
|
+
|
|
230
|
+
class MetricsMiddleware {
|
|
231
|
+
metrics;
|
|
232
|
+
constructor(metrics) {
|
|
233
|
+
this.metrics = metrics;
|
|
234
|
+
}
|
|
235
|
+
handle(_req, ctx, next) {
|
|
236
|
+
const started = Bun.nanoseconds();
|
|
237
|
+
return next().then((response) => {
|
|
238
|
+
this.metrics.observe(ctx, response.status, Bun.nanoseconds() - started);
|
|
239
|
+
return response;
|
|
240
|
+
}, (error) => {
|
|
241
|
+
this.metrics.observe(ctx, error instanceof HttpError ? error.status : HttpStatusCode.INTERNAL_SERVER_ERROR, Bun.nanoseconds() - started);
|
|
242
|
+
throw error;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
Object.defineProperty(MetricsMiddleware, Symbol.for("dunx.deps"), { value: () => [RequestMetrics] });
|
|
247
|
+
|
|
248
|
+
// src/ws/envelope.ts
|
|
249
|
+
var encode = (event, data) => JSON.stringify({ event, data });
|
|
250
|
+
var decode = (message) => {
|
|
251
|
+
if (typeof message !== "string")
|
|
252
|
+
return;
|
|
253
|
+
let parsed;
|
|
254
|
+
try {
|
|
255
|
+
parsed = JSON.parse(message);
|
|
256
|
+
} catch {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
260
|
+
return;
|
|
261
|
+
const { event, data } = parsed;
|
|
262
|
+
return typeof event === "string" ? { event, data } : undefined;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// src/ws/middleware.ts
|
|
266
|
+
var composeSocket = (middleware, ctx) => middleware.reduceRight((next, current) => (frame, run) => current.handle(frame, ctx, () => next(frame, run)), (_frame, run) => run());
|
|
267
|
+
var observe = (next, done) => {
|
|
268
|
+
let result;
|
|
269
|
+
try {
|
|
270
|
+
result = next();
|
|
271
|
+
} catch (error) {
|
|
272
|
+
done(error, undefined);
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
if (result instanceof Promise) {
|
|
276
|
+
return result.then((value) => {
|
|
277
|
+
done(undefined, value);
|
|
278
|
+
return value;
|
|
279
|
+
}, (error) => {
|
|
280
|
+
done(error, undefined);
|
|
281
|
+
throw error;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
done(undefined, result);
|
|
285
|
+
return result;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// src/ws/runtime.ts
|
|
289
|
+
import { AppError as AppError3 } from "@dunx/core";
|
|
290
|
+
var slotOf = (handler) => handler.kind === HandlerKind.MESSAGE && handler.event !== undefined ? `message ${JSON.stringify(handler.event)}` : handler.kind;
|
|
291
|
+
var buildRuntime = (gateway) => {
|
|
292
|
+
if (gateway.handlers.length === 0) {
|
|
293
|
+
throw new AppError3(`${gateway.name} is registered as a gateway but declares no handlers. ` + "Add an @OnMessage/@OnOpen/... method, or drop the @Gateway decorator.");
|
|
294
|
+
}
|
|
295
|
+
const owners = new Map;
|
|
296
|
+
const events = new Map;
|
|
297
|
+
for (const handler of gateway.handlers) {
|
|
298
|
+
const slot = slotOf(handler);
|
|
299
|
+
const existing = owners.get(slot);
|
|
300
|
+
if (existing) {
|
|
301
|
+
throw new AppError3(`Handler collision in ${gateway.name}: ${slot} is claimed by ` + `${existing.method}() and by ${handler.method}(). One handler per event.`);
|
|
302
|
+
}
|
|
303
|
+
owners.set(slot, handler);
|
|
304
|
+
if (handler.kind === HandlerKind.MESSAGE && handler.event !== undefined) {
|
|
305
|
+
events.set(handler.event, handler.invoke);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const at = (slot) => owners.get(slot)?.invoke;
|
|
309
|
+
return {
|
|
310
|
+
name: gateway.name,
|
|
311
|
+
path: gateway.path,
|
|
312
|
+
upgrade: at(HandlerKind.UPGRADE),
|
|
313
|
+
open: at(HandlerKind.OPEN),
|
|
314
|
+
close: at(HandlerKind.CLOSE),
|
|
315
|
+
drain: at(HandlerKind.DRAIN),
|
|
316
|
+
ping: at(HandlerKind.PING),
|
|
317
|
+
pong: at(HandlerKind.PONG),
|
|
318
|
+
raw: at(HandlerKind.MESSAGE),
|
|
319
|
+
events
|
|
320
|
+
};
|
|
321
|
+
};
|
|
322
|
+
var buildGateways = (discovered) => {
|
|
323
|
+
const byPath = new Map;
|
|
324
|
+
for (const gateway of discovered) {
|
|
325
|
+
const existing = byPath.get(gateway.path);
|
|
326
|
+
if (existing) {
|
|
327
|
+
throw new AppError3(`Gateway path collision: ${gateway.path} is served by ${existing.name} ` + `and by ${gateway.name}. One gateway per path.`);
|
|
328
|
+
}
|
|
329
|
+
byPath.set(gateway.path, buildRuntime(gateway));
|
|
330
|
+
}
|
|
331
|
+
return byPath;
|
|
332
|
+
};
|
|
333
|
+
var someHandler = (gateways, pick) => {
|
|
334
|
+
for (const gateway of gateways)
|
|
335
|
+
if (pick(gateway) !== undefined)
|
|
336
|
+
return true;
|
|
337
|
+
return false;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// src/ws/adapter.ts
|
|
341
|
+
var RUNTIME = Symbol.for("dunx.ws.runtime");
|
|
342
|
+
var UNCLAIMED = Symbol.for("dunx.ws.unclaimed");
|
|
343
|
+
var defaultOnError = (error, socket) => {
|
|
344
|
+
console.error(`[dunx/http] ${socket.data.path} handler failed:`, error);
|
|
345
|
+
};
|
|
346
|
+
var reportedByMiddleware = () => {
|
|
347
|
+
return;
|
|
348
|
+
};
|
|
349
|
+
var unreported = (middleware) => "Socket middleware is installed and none of it sets reportsErrors, so a " + "throwing gateway handler is reported nowhere: the console fallback is off " + "whenever middleware wraps the handler. Set reportsErrors on the one that " + "records a failure, or pass websocket.onError. Installed: " + `${middleware.map((entry) => entry.constructor.name).join(", ")}.`;
|
|
350
|
+
var runtimeOf = (socket) => socket.data[RUNTIME];
|
|
351
|
+
var isBinary = (value) => value instanceof ArrayBuffer || ArrayBuffer.isView(value);
|
|
352
|
+
var replyRaw = (socket, value) => {
|
|
353
|
+
if (value === undefined)
|
|
354
|
+
return;
|
|
355
|
+
socket.send(typeof value === "string" || isBinary(value) ? value : JSON.stringify(value));
|
|
356
|
+
};
|
|
357
|
+
var settle = (result, socket, onError, then) => {
|
|
358
|
+
if (result instanceof Promise) {
|
|
359
|
+
result.then((value) => {
|
|
360
|
+
if (!then)
|
|
361
|
+
return;
|
|
362
|
+
try {
|
|
363
|
+
then(value);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
onError(error, socket);
|
|
366
|
+
}
|
|
367
|
+
}, (error) => onError(error, socket));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (then)
|
|
371
|
+
then(result);
|
|
372
|
+
};
|
|
373
|
+
var framing = (kind) => {
|
|
374
|
+
if (kind === HandlerKind.CLOSE) {
|
|
375
|
+
return (args) => ({
|
|
376
|
+
socket: args[0],
|
|
377
|
+
data: { code: args[1], reason: args[2] }
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
if (kind === HandlerKind.OPEN || kind === HandlerKind.DRAIN) {
|
|
381
|
+
return (args) => ({ socket: args[0], data: undefined });
|
|
382
|
+
}
|
|
383
|
+
return (args) => ({ socket: args[1], data: args[0] });
|
|
384
|
+
};
|
|
385
|
+
var NOTHING = () => {
|
|
386
|
+
return;
|
|
387
|
+
};
|
|
388
|
+
var through = (gateway, middleware, kind, event, invoke) => {
|
|
389
|
+
const ctx = {
|
|
390
|
+
gateway: gateway.name,
|
|
391
|
+
path: gateway.path,
|
|
392
|
+
kind,
|
|
393
|
+
event
|
|
394
|
+
};
|
|
395
|
+
const dispatch = composeSocket(middleware, ctx);
|
|
396
|
+
const frameOf = framing(kind);
|
|
397
|
+
const run = invoke ?? NOTHING;
|
|
398
|
+
return (...args) => dispatch(frameOf(args), () => run(...args));
|
|
399
|
+
};
|
|
400
|
+
var withMiddleware = (gateway, middleware) => {
|
|
401
|
+
const wrap = (kind, event, invoke) => through(gateway, middleware, kind, event, invoke);
|
|
402
|
+
const optional = (kind, invoke) => invoke === undefined ? undefined : wrap(kind, undefined, invoke);
|
|
403
|
+
return {
|
|
404
|
+
...gateway,
|
|
405
|
+
open: wrap(HandlerKind.OPEN, undefined, gateway.open),
|
|
406
|
+
close: wrap(HandlerKind.CLOSE, undefined, gateway.close),
|
|
407
|
+
drain: optional(HandlerKind.DRAIN, gateway.drain),
|
|
408
|
+
ping: optional(HandlerKind.PING, gateway.ping),
|
|
409
|
+
pong: optional(HandlerKind.PONG, gateway.pong),
|
|
410
|
+
raw: optional(HandlerKind.MESSAGE, gateway.raw),
|
|
411
|
+
events: new Map([...gateway.events].map(([event, invoke]) => [
|
|
412
|
+
event,
|
|
413
|
+
wrap(HandlerKind.MESSAGE, event, invoke)
|
|
414
|
+
]))
|
|
415
|
+
};
|
|
416
|
+
};
|
|
417
|
+
var unclaimedDispatch = (gateway, middleware) => (frame, event) => composeSocket(middleware, {
|
|
418
|
+
gateway: gateway.name,
|
|
419
|
+
path: gateway.path,
|
|
420
|
+
kind: HandlerKind.MESSAGE,
|
|
421
|
+
event
|
|
422
|
+
})(frame, () => {
|
|
423
|
+
return;
|
|
424
|
+
});
|
|
425
|
+
var buildWebSocket = (discovered, options = {}, middleware = []) => {
|
|
426
|
+
const byPath = buildGateways(discovered);
|
|
427
|
+
const wrapped = middleware.length === 0 ? byPath : new Map([...byPath].map(([path, gateway]) => [
|
|
428
|
+
path,
|
|
429
|
+
withMiddleware(gateway, middleware)
|
|
430
|
+
]));
|
|
431
|
+
const gateways = [...wrapped.values()];
|
|
432
|
+
const onError = options.onError ?? (middleware.length === 0 ? defaultOnError : reportedByMiddleware);
|
|
433
|
+
const reports = options.onError !== undefined || middleware.some((entry) => entry.reportsErrors === true);
|
|
434
|
+
const { onError: _onError, ...socketOptions } = options;
|
|
435
|
+
const run = (invoke, args, ws, then) => {
|
|
436
|
+
try {
|
|
437
|
+
settle(invoke(...args), ws, onError, then);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
onError(error, ws);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
const websocket = {
|
|
443
|
+
...socketOptions,
|
|
444
|
+
message(ws, message) {
|
|
445
|
+
const gateway = runtimeOf(ws);
|
|
446
|
+
let event;
|
|
447
|
+
if (gateway.events.size > 0) {
|
|
448
|
+
const envelope = decode(message);
|
|
449
|
+
const handler = envelope && gateway.events.get(envelope.event);
|
|
450
|
+
if (envelope && handler) {
|
|
451
|
+
run(handler, [envelope.data, ws], ws, (value) => {
|
|
452
|
+
if (value !== undefined)
|
|
453
|
+
ws.send(encode(envelope.event, value));
|
|
454
|
+
});
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
event = envelope?.event;
|
|
458
|
+
}
|
|
459
|
+
if (gateway.raw) {
|
|
460
|
+
run(gateway.raw, [message, ws], ws, (value) => replyRaw(ws, value));
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const unclaimed2 = ws.data[UNCLAIMED];
|
|
464
|
+
if (!unclaimed2)
|
|
465
|
+
return;
|
|
466
|
+
try {
|
|
467
|
+
settle(unclaimed2({ socket: ws, data: message }, event), ws, onError, undefined);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
onError(error, ws);
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
...someHandler(gateways, (g) => g.open) && {
|
|
473
|
+
open(ws) {
|
|
474
|
+
const { open } = runtimeOf(ws);
|
|
475
|
+
if (open)
|
|
476
|
+
run(open, [ws], ws, undefined);
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
...someHandler(gateways, (g) => g.close) && {
|
|
480
|
+
close(ws, code, reason) {
|
|
481
|
+
const { close } = runtimeOf(ws);
|
|
482
|
+
if (close)
|
|
483
|
+
run(close, [ws, code, reason], ws, undefined);
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
...someHandler(gateways, (g) => g.drain) && {
|
|
487
|
+
drain(ws) {
|
|
488
|
+
const { drain } = runtimeOf(ws);
|
|
489
|
+
if (drain)
|
|
490
|
+
run(drain, [ws], ws, undefined);
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
...someHandler(gateways, (g) => g.ping) && {
|
|
494
|
+
ping(ws, data) {
|
|
495
|
+
const { ping } = runtimeOf(ws);
|
|
496
|
+
if (ping)
|
|
497
|
+
run(ping, [data, ws], ws, undefined);
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
...someHandler(gateways, (g) => g.pong) && {
|
|
501
|
+
pong(ws, data) {
|
|
502
|
+
const { pong } = runtimeOf(ws);
|
|
503
|
+
if (pong)
|
|
504
|
+
run(pong, [data, ws], ws, undefined);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
const unclaimed = new Map(middleware.length === 0 ? [] : gateways.map((gateway) => [
|
|
509
|
+
gateway,
|
|
510
|
+
unclaimedDispatch(gateway, middleware)
|
|
511
|
+
]));
|
|
512
|
+
const accept = (req, server, gateway, context) => {
|
|
513
|
+
const fallback = unclaimed.get(gateway);
|
|
514
|
+
const data = {
|
|
515
|
+
path: gateway.path,
|
|
516
|
+
context,
|
|
517
|
+
id: crypto.randomUUID(),
|
|
518
|
+
[RUNTIME]: gateway,
|
|
519
|
+
...fallback === undefined ? {} : { [UNCLAIMED]: fallback }
|
|
520
|
+
};
|
|
521
|
+
return server.upgrade(req, { data }) ? undefined : new Response("Expected a WebSocket upgrade", { status: 426 });
|
|
522
|
+
};
|
|
523
|
+
const upgradeHandler = (gateway) => (req, server) => {
|
|
524
|
+
if (!gateway.upgrade)
|
|
525
|
+
return accept(req, server, gateway, undefined);
|
|
526
|
+
const result = gateway.upgrade(req);
|
|
527
|
+
if (result instanceof Promise) {
|
|
528
|
+
return result.then((value) => value instanceof Response ? value : accept(req, server, gateway, value));
|
|
529
|
+
}
|
|
530
|
+
return result instanceof Response ? result : accept(req, server, gateway, result);
|
|
531
|
+
};
|
|
532
|
+
return {
|
|
533
|
+
websocket,
|
|
534
|
+
routes: new Map(gateways.map((gateway) => [gateway.path, upgradeHandler(gateway)])),
|
|
535
|
+
paths: [...byPath.keys()],
|
|
536
|
+
warnings: middleware.length > 0 && !reports ? [unreported(middleware)] : [],
|
|
537
|
+
gateways: gateways.map((gateway) => ({
|
|
538
|
+
name: gateway.name,
|
|
539
|
+
path: gateway.path,
|
|
540
|
+
events: [...gateway.events.keys()]
|
|
541
|
+
}))
|
|
542
|
+
};
|
|
543
|
+
};
|
|
544
|
+
|
|
115
545
|
// src/ws/logging.ts
|
|
116
546
|
import { Logger, LogLevel, RequestContext } from "@dunx/core";
|
|
117
547
|
var LIFECYCLE_LABEL = {
|
|
@@ -210,7 +640,64 @@ class SocketLoggingMiddleware {
|
|
|
210
640
|
Object.defineProperty(SocketLoggingMiddleware, Symbol.for("dunx.deps"), { value: () => [Logger, RequestContext, { unresolved: "options: SocketLoggingOptions = {}", optional: true }] });
|
|
211
641
|
|
|
212
642
|
// src/ws/pubsub.ts
|
|
213
|
-
import { AppError as
|
|
643
|
+
import { AppError as AppError5 } from "@dunx/core";
|
|
644
|
+
|
|
645
|
+
// src/ws/relay.ts
|
|
646
|
+
import { AppError as AppError4 } from "@dunx/core";
|
|
647
|
+
var assertRelayUrl = (url, protocols, example) => {
|
|
648
|
+
let parsed;
|
|
649
|
+
try {
|
|
650
|
+
parsed = new URL(url);
|
|
651
|
+
} catch {
|
|
652
|
+
throw new AppError4(`${JSON.stringify(url)} is not a valid URL for the websocket relay. ` + `Expected something like ${example}.`);
|
|
653
|
+
}
|
|
654
|
+
if (!protocols.includes(parsed.protocol)) {
|
|
655
|
+
throw new AppError4(`Unsupported protocol ${JSON.stringify(parsed.protocol)} in ` + `${JSON.stringify(url)}. Expected one of ${protocols.join(", ")}.`);
|
|
656
|
+
}
|
|
657
|
+
return url;
|
|
658
|
+
};
|
|
659
|
+
var redactUrl = (url) => {
|
|
660
|
+
const parsed = new URL(url);
|
|
661
|
+
if (parsed.password)
|
|
662
|
+
parsed.password = "***";
|
|
663
|
+
return parsed.toString();
|
|
664
|
+
};
|
|
665
|
+
var DEFAULT_RELAY_CHANNEL = "dunx:ws";
|
|
666
|
+
var defaultRelayError = (error, phase) => {
|
|
667
|
+
console.warn(`[dunx/http] the websocket relay could not ${phase}. Fan-out is local to ` + "this process until it recovers:", error);
|
|
668
|
+
};
|
|
669
|
+
var toBytes = (data) => ArrayBuffer.isView(data) ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : new Uint8Array(data);
|
|
670
|
+
var encodeRelay = (origin, topic, data) => typeof data === "string" ? JSON.stringify({ o: origin, t: topic, d: data }) : JSON.stringify({
|
|
671
|
+
o: origin,
|
|
672
|
+
t: topic,
|
|
673
|
+
d: Buffer.from(toBytes(data)).toString("base64"),
|
|
674
|
+
b: 1
|
|
675
|
+
});
|
|
676
|
+
var decodeRelay = (message) => {
|
|
677
|
+
let parsed;
|
|
678
|
+
try {
|
|
679
|
+
parsed = JSON.parse(message);
|
|
680
|
+
} catch {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
684
|
+
return;
|
|
685
|
+
const { o, t, d, b } = parsed;
|
|
686
|
+
if (typeof o !== "string" || typeof t !== "string" || typeof d !== "string") {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
return { origin: o, topic: t, data: b ? Buffer.from(d, "base64") : d };
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
class WsRelay {
|
|
693
|
+
constructor() {
|
|
694
|
+
if (new.target === WsRelay) {
|
|
695
|
+
throw new AppError4("WsRelay is a contract, not an implementation. Bind one with " + "WsRelayModule.forRoot() for Redis or WsRelayModule.forPostgres() " + "for Postgres, or extend it with a relay of your own.");
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/ws/pubsub.ts
|
|
214
701
|
class PubSub {
|
|
215
702
|
#origin = Bun.randomUUIDv7();
|
|
216
703
|
#server;
|
|
@@ -235,7 +722,7 @@ class PubSub {
|
|
|
235
722
|
}
|
|
236
723
|
async relayThrough(relay, options = {}) {
|
|
237
724
|
if (this.#relay) {
|
|
238
|
-
throw new
|
|
725
|
+
throw new AppError5("PubSub already relays. Two subscriptions on one channel would deliver " + "every relayed message twice - pass HttpOptions.relay or call " + "relayThrough(), not both.");
|
|
239
726
|
}
|
|
240
727
|
this.#relay = relay;
|
|
241
728
|
this.#channel = options.channel ?? DEFAULT_RELAY_CHANNEL;
|
|
@@ -331,7 +818,7 @@ class PubSub {
|
|
|
331
818
|
}
|
|
332
819
|
#live() {
|
|
333
820
|
if (!this.#server) {
|
|
334
|
-
throw new
|
|
821
|
+
throw new AppError5("PubSub has no server yet. Publish once the server is listening: " + "HttpApp.listen() is what attaches it.");
|
|
335
822
|
}
|
|
336
823
|
return this.#server;
|
|
337
824
|
}
|
|
@@ -339,10 +826,10 @@ class PubSub {
|
|
|
339
826
|
|
|
340
827
|
// src/server/application.ts
|
|
341
828
|
import {
|
|
342
|
-
AppError as
|
|
829
|
+
AppError as AppError7,
|
|
343
830
|
Logger as Logger3,
|
|
344
831
|
runtimeInfo,
|
|
345
|
-
|
|
832
|
+
ShutdownAware,
|
|
346
833
|
teardownError,
|
|
347
834
|
teardownFailures as toFailures
|
|
348
835
|
} from "@dunx/core";
|
|
@@ -352,6 +839,27 @@ import {
|
|
|
352
839
|
Logger as Logger2,
|
|
353
840
|
RequestContext as RequestContext2
|
|
354
841
|
} from "@dunx/core";
|
|
842
|
+
|
|
843
|
+
// src/server/raw-body.ts
|
|
844
|
+
var WANTED = Symbol.for("dunx.http.rawBody.wanted");
|
|
845
|
+
var TEXT = Symbol.for("dunx.http.rawBody.text");
|
|
846
|
+
|
|
847
|
+
class RawBody {
|
|
848
|
+
static want(req) {
|
|
849
|
+
req[WANTED] = true;
|
|
850
|
+
}
|
|
851
|
+
static wanted(req) {
|
|
852
|
+
return req[WANTED] === true;
|
|
853
|
+
}
|
|
854
|
+
static record(req, text) {
|
|
855
|
+
req[TEXT] = text;
|
|
856
|
+
}
|
|
857
|
+
static read(req) {
|
|
858
|
+
return req[TEXT];
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// src/server/request-logging.ts
|
|
355
863
|
var parse = (text, limit) => {
|
|
356
864
|
if (limit === 0)
|
|
357
865
|
return;
|
|
@@ -378,7 +886,9 @@ class RequestLoggingMiddleware {
|
|
|
378
886
|
#correlateIgnored;
|
|
379
887
|
#correlate;
|
|
380
888
|
#trace;
|
|
381
|
-
|
|
889
|
+
#traceResponse;
|
|
890
|
+
#metrics;
|
|
891
|
+
constructor(logger, context, options = {}, metrics) {
|
|
382
892
|
this.logger = logger;
|
|
383
893
|
this.context = context;
|
|
384
894
|
this.#limit = options.maxBodyLength ?? 2048;
|
|
@@ -388,7 +898,9 @@ class RequestLoggingMiddleware {
|
|
|
388
898
|
this.#ignorePrefix = options.ignorePrefix ?? [];
|
|
389
899
|
this.#correlateIgnored = options.correlateIgnored ?? false;
|
|
390
900
|
this.#correlate = options.correlate ?? true;
|
|
391
|
-
this.#trace = options.trace ??
|
|
901
|
+
this.#trace = options.trace ?? true;
|
|
902
|
+
this.#traceResponse = options.traceResponse ?? true;
|
|
903
|
+
this.#metrics = metrics;
|
|
392
904
|
}
|
|
393
905
|
#ignored(path) {
|
|
394
906
|
if (this.#ignore.size > 0 && this.#ignore.has(path))
|
|
@@ -403,28 +915,32 @@ class RequestLoggingMiddleware {
|
|
|
403
915
|
const mark = from === -1 ? -1 : url.indexOf("?", from);
|
|
404
916
|
const path = from === -1 ? "/" : mark === -1 ? url.slice(from) : url.slice(from, mark);
|
|
405
917
|
if (this.#ignored(path)) {
|
|
918
|
+
if (this.#metrics !== undefined) {
|
|
919
|
+
return this.#ignoredWithMetrics(req, ctx, path, next);
|
|
920
|
+
}
|
|
406
921
|
return this.#correlateIgnored ? this.#correlated(req, ctx, path, next) : next();
|
|
407
922
|
}
|
|
408
923
|
const started = Bun.nanoseconds();
|
|
409
|
-
const requestId = RequestIds.assign(req);
|
|
410
924
|
const scope = {
|
|
411
|
-
requestId,
|
|
412
925
|
method: ctx.method,
|
|
413
926
|
event: path,
|
|
414
927
|
flow: "http",
|
|
415
928
|
context: `${ctx.controller}.${ctx.handler}`
|
|
416
929
|
};
|
|
417
930
|
if (this.#trace) {
|
|
418
|
-
const trace = TraceContext.adopt(req,
|
|
931
|
+
const trace = TraceContext.adopt(req, this.#traceResponse);
|
|
419
932
|
scope.traceId = trace.traceId;
|
|
420
933
|
scope.spanId = trace.spanId;
|
|
934
|
+
scope.traceFlags = trace.flags;
|
|
421
935
|
if (trace.parentSpanId !== undefined) {
|
|
422
936
|
scope.parentSpanId = trace.parentSpanId;
|
|
423
937
|
}
|
|
938
|
+
if (trace.state !== undefined)
|
|
939
|
+
scope.traceState = trace.state;
|
|
424
940
|
}
|
|
425
|
-
return this.#correlate ? this.context.runWithContext(scope, () => this.#begin(req, ctx, url, mark, path,
|
|
941
|
+
return this.#correlate ? this.context.runWithContext(scope, () => this.#begin(req, ctx, url, mark, path, started, next, undefined)) : this.#begin(req, ctx, url, mark, path, started, next, scope);
|
|
426
942
|
}
|
|
427
|
-
#begin(req, ctx, url, mark, path,
|
|
943
|
+
#begin(req, ctx, url, mark, path, started, next, scope) {
|
|
428
944
|
const request = {};
|
|
429
945
|
if (mark !== -1) {
|
|
430
946
|
request["query"] = Object.fromEntries(new URLSearchParams(url.slice(mark + 1)));
|
|
@@ -432,13 +948,13 @@ class RequestLoggingMiddleware {
|
|
|
432
948
|
const body = this.#body(req, ctx);
|
|
433
949
|
if (body === undefined) {
|
|
434
950
|
request["userAgent"] = req.headers.get("user-agent");
|
|
435
|
-
return this.#dispatch(req,
|
|
951
|
+
return this.#dispatch(req, ctx, path, started, request, next, scope);
|
|
436
952
|
}
|
|
437
953
|
return body.then((value) => {
|
|
438
954
|
if (value !== undefined)
|
|
439
955
|
request["body"] = value;
|
|
440
956
|
request["userAgent"] = req.headers.get("user-agent");
|
|
441
|
-
return this.#dispatch(req,
|
|
957
|
+
return this.#dispatch(req, ctx, path, started, request, next, scope);
|
|
442
958
|
});
|
|
443
959
|
}
|
|
444
960
|
#shared(req, request) {
|
|
@@ -453,36 +969,56 @@ class RequestLoggingMiddleware {
|
|
|
453
969
|
if (value !== undefined)
|
|
454
970
|
request["body"] = value;
|
|
455
971
|
}
|
|
456
|
-
#
|
|
457
|
-
const
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
972
|
+
#ignoredWithMetrics(req, ctx, path, next) {
|
|
973
|
+
const started = Bun.nanoseconds();
|
|
974
|
+
const failed = (error) => {
|
|
975
|
+
this.#observe(req, ctx, error instanceof HttpError ? error.status : HttpStatusCode.INTERNAL_SERVER_ERROR, started);
|
|
976
|
+
throw error;
|
|
461
977
|
};
|
|
978
|
+
let settled;
|
|
979
|
+
try {
|
|
980
|
+
settled = this.#correlateIgnored ? this.#correlated(req, ctx, path, next) : next();
|
|
981
|
+
} catch (error) {
|
|
982
|
+
return failed(error);
|
|
983
|
+
}
|
|
984
|
+
return settled.then((response) => {
|
|
985
|
+
this.#observe(req, ctx, response.status, started);
|
|
986
|
+
return response;
|
|
987
|
+
}, failed);
|
|
988
|
+
}
|
|
989
|
+
#correlated(req, ctx, path, next) {
|
|
990
|
+
const trace = this.#trace ? TraceContext.adopt(req, this.#traceResponse) : undefined;
|
|
991
|
+
const stamp = (response) => trace === undefined ? response : TraceContext.stamp(response, req);
|
|
462
992
|
if (!this.#correlate)
|
|
463
993
|
return next().then(stamp);
|
|
464
994
|
return this.context.runWithContext({
|
|
465
|
-
|
|
995
|
+
...trace === undefined ? {} : {
|
|
996
|
+
traceId: trace.traceId,
|
|
997
|
+
spanId: trace.spanId,
|
|
998
|
+
traceFlags: trace.flags,
|
|
999
|
+
...trace.parentSpanId === undefined ? {} : { parentSpanId: trace.parentSpanId },
|
|
1000
|
+
...trace.state === undefined ? {} : { traceState: trace.state }
|
|
1001
|
+
},
|
|
466
1002
|
method: ctx.method,
|
|
467
1003
|
event: path,
|
|
468
1004
|
flow: "http",
|
|
469
1005
|
context: `${ctx.controller}.${ctx.handler}`
|
|
470
1006
|
}, () => next().then(stamp));
|
|
471
1007
|
}
|
|
472
|
-
#dispatch(req,
|
|
1008
|
+
#dispatch(req, ctx, path, started, request, next, scope) {
|
|
473
1009
|
let settled;
|
|
474
1010
|
try {
|
|
475
1011
|
settled = next();
|
|
476
1012
|
} catch (error) {
|
|
477
|
-
this.#failed(req, path, started, request, error, scope);
|
|
1013
|
+
this.#failed(req, ctx, path, started, request, error, scope);
|
|
478
1014
|
throw error;
|
|
479
1015
|
}
|
|
480
|
-
return settled.then((response) => this.#succeeded(req,
|
|
481
|
-
this.#failed(req, path, started, request, error, scope);
|
|
1016
|
+
return settled.then((response) => this.#succeeded(req, ctx, path, started, request, response, scope), (error) => {
|
|
1017
|
+
this.#failed(req, ctx, path, started, request, error, scope);
|
|
482
1018
|
throw error;
|
|
483
1019
|
});
|
|
484
1020
|
}
|
|
485
|
-
#failed(req, path, started, request, error, scope) {
|
|
1021
|
+
#failed(req, ctx, path, started, request, error, scope) {
|
|
486
1022
|
this.#shared(req, request);
|
|
487
1023
|
const status = error instanceof HttpError ? error.status : HttpStatusCode.INTERNAL_SERVER_ERROR;
|
|
488
1024
|
const entry = {
|
|
@@ -492,6 +1028,7 @@ class RequestLoggingMiddleware {
|
|
|
492
1028
|
statusCode: status,
|
|
493
1029
|
elapsedMs: elapsedMs2(started)
|
|
494
1030
|
};
|
|
1031
|
+
this.#observe(req, ctx, status, started);
|
|
495
1032
|
const line = `${req.method} ${path} ${status}`;
|
|
496
1033
|
if (status < HttpStatusCode.INTERNAL_SERVER_ERROR) {
|
|
497
1034
|
this.logger.warn(line, entry);
|
|
@@ -499,8 +1036,9 @@ class RequestLoggingMiddleware {
|
|
|
499
1036
|
this.logger.error(line, entry);
|
|
500
1037
|
}
|
|
501
1038
|
}
|
|
502
|
-
#succeeded(req,
|
|
1039
|
+
#succeeded(req, ctx, path, started, request, response, scope) {
|
|
503
1040
|
this.#shared(req, request);
|
|
1041
|
+
this.#observe(req, ctx, response.status, started);
|
|
504
1042
|
const body = this.#responseFields(response);
|
|
505
1043
|
if (body === undefined) {
|
|
506
1044
|
this.logger.info(`${req.method} ${path} ${response.status}`, {
|
|
@@ -509,8 +1047,7 @@ class RequestLoggingMiddleware {
|
|
|
509
1047
|
statusCode: response.status,
|
|
510
1048
|
elapsedMs: elapsedMs2(started)
|
|
511
1049
|
});
|
|
512
|
-
|
|
513
|
-
return response;
|
|
1050
|
+
return TraceContext.stamp(response, req);
|
|
514
1051
|
}
|
|
515
1052
|
return body.then((value) => {
|
|
516
1053
|
this.logger.info(`${req.method} ${path} ${response.status}`, {
|
|
@@ -520,10 +1057,14 @@ class RequestLoggingMiddleware {
|
|
|
520
1057
|
...value === undefined ? {} : { responseBody: value },
|
|
521
1058
|
elapsedMs: elapsedMs2(started)
|
|
522
1059
|
});
|
|
523
|
-
|
|
524
|
-
return response;
|
|
1060
|
+
return TraceContext.stamp(response, req);
|
|
525
1061
|
});
|
|
526
1062
|
}
|
|
1063
|
+
#observe(req, ctx, status, started) {
|
|
1064
|
+
if (this.#metrics === undefined)
|
|
1065
|
+
return;
|
|
1066
|
+
this.#metrics.observe(ctx, status, Bun.nanoseconds() - started, TraceContext.of(req)?.traceId);
|
|
1067
|
+
}
|
|
527
1068
|
#body(req, ctx) {
|
|
528
1069
|
if (!this.#requestBody)
|
|
529
1070
|
return;
|
|
@@ -547,13 +1088,304 @@ class RequestLoggingMiddleware {
|
|
|
547
1088
|
return response.clone().text().then((text) => parse(text, this.#limit));
|
|
548
1089
|
}
|
|
549
1090
|
}
|
|
550
|
-
Object.defineProperty(RequestLoggingMiddleware, Symbol.for("dunx.deps"), { value: () => [Logger2, RequestContext2, { unresolved: "options: RequestLoggingOptions = {}", optional: true }] });
|
|
1091
|
+
Object.defineProperty(RequestLoggingMiddleware, Symbol.for("dunx.deps"), { value: () => [Logger2, RequestContext2, { unresolved: "options: RequestLoggingOptions = {}", optional: true }, { unresolved: "metrics?: RequestMetrics", typeOnly: "RequestMetrics" }] });
|
|
1092
|
+
|
|
1093
|
+
// src/server/routes.ts
|
|
1094
|
+
import { AppError as AppError6 } from "@dunx/core";
|
|
1095
|
+
|
|
1096
|
+
// src/server/cors.ts
|
|
1097
|
+
var ORIGIN = "access-control-allow-origin";
|
|
1098
|
+
var allowedOrigin = (options, requested) => {
|
|
1099
|
+
const origin = options.origin ?? "*";
|
|
1100
|
+
if (typeof origin === "string") {
|
|
1101
|
+
if (origin !== "*")
|
|
1102
|
+
return origin === requested ? origin : undefined;
|
|
1103
|
+
if (!options.credentials)
|
|
1104
|
+
return "*";
|
|
1105
|
+
return requested ?? undefined;
|
|
1106
|
+
}
|
|
1107
|
+
if (requested === null)
|
|
1108
|
+
return;
|
|
1109
|
+
const allowed = typeof origin === "function" ? origin(requested) : origin.includes(requested);
|
|
1110
|
+
return allowed ? requested : undefined;
|
|
1111
|
+
};
|
|
1112
|
+
var applyCors = (options, req, response) => {
|
|
1113
|
+
const origin = allowedOrigin(options, req.headers.get("origin"));
|
|
1114
|
+
if (origin === undefined)
|
|
1115
|
+
return response;
|
|
1116
|
+
response.headers.set(ORIGIN, origin);
|
|
1117
|
+
if (origin !== "*")
|
|
1118
|
+
response.headers.append("vary", "Origin");
|
|
1119
|
+
if (options.credentials) {
|
|
1120
|
+
response.headers.set("access-control-allow-credentials", "true");
|
|
1121
|
+
}
|
|
1122
|
+
if (options.exposedHeaders?.length) {
|
|
1123
|
+
response.headers.set("access-control-expose-headers", options.exposedHeaders.join(", "));
|
|
1124
|
+
}
|
|
1125
|
+
return response;
|
|
1126
|
+
};
|
|
1127
|
+
var withCors = (options, handler) => {
|
|
1128
|
+
return async (req) => applyCors(options, req, await handler(req));
|
|
1129
|
+
};
|
|
1130
|
+
var preflight = (options, methods) => {
|
|
1131
|
+
const allowMethods = (options.methods ?? methods).join(", ");
|
|
1132
|
+
return async (req) => {
|
|
1133
|
+
const response = applyCors(options, req, new Response(null, { status: HttpStatusCode.NO_CONTENT }));
|
|
1134
|
+
if (!response.headers.has(ORIGIN))
|
|
1135
|
+
return response;
|
|
1136
|
+
response.headers.set("access-control-allow-methods", allowMethods);
|
|
1137
|
+
const allowHeaders = options.allowedHeaders ?? (req.headers.get("access-control-request-headers") ?? "").split(",").map((header) => header.trim()).filter((header) => header.length > 0);
|
|
1138
|
+
if (allowHeaders.length > 0) {
|
|
1139
|
+
response.headers.set("access-control-allow-headers", allowHeaders.join(", "));
|
|
1140
|
+
}
|
|
1141
|
+
if (options.maxAge !== undefined) {
|
|
1142
|
+
response.headers.set("access-control-max-age", String(options.maxAge));
|
|
1143
|
+
}
|
|
1144
|
+
return response;
|
|
1145
|
+
};
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
// src/server/input.ts
|
|
1149
|
+
var grouped = (entries) => {
|
|
1150
|
+
const collected = {};
|
|
1151
|
+
entries.forEach((value, key) => {
|
|
1152
|
+
const existing = collected[key];
|
|
1153
|
+
if (existing === undefined)
|
|
1154
|
+
collected[key] = value;
|
|
1155
|
+
else if (Array.isArray(existing))
|
|
1156
|
+
existing.push(value);
|
|
1157
|
+
else
|
|
1158
|
+
collected[key] = [existing, value];
|
|
1159
|
+
});
|
|
1160
|
+
return collected;
|
|
1161
|
+
};
|
|
1162
|
+
var asJson = (req) => req.json();
|
|
1163
|
+
var asUrlEncoded = async (req) => grouped(new URLSearchParams(await req.text()));
|
|
1164
|
+
var asMultipart = async (req) => grouped(await req.formData());
|
|
1165
|
+
var asText = (req) => req.text();
|
|
1166
|
+
var parserFor = (media) => {
|
|
1167
|
+
if (media === "application/json" || media.endsWith("+json"))
|
|
1168
|
+
return asJson;
|
|
1169
|
+
if (media === "application/x-www-form-urlencoded")
|
|
1170
|
+
return asUrlEncoded;
|
|
1171
|
+
if (media === "multipart/form-data")
|
|
1172
|
+
return asMultipart;
|
|
1173
|
+
if (media.startsWith("text/"))
|
|
1174
|
+
return asText;
|
|
1175
|
+
return;
|
|
1176
|
+
};
|
|
1177
|
+
var JSON_MEDIA = "application/json";
|
|
1178
|
+
var mediaTypeOf = (req) => {
|
|
1179
|
+
const header = req.headers.get("content-type");
|
|
1180
|
+
if (header === JSON_MEDIA || header === null)
|
|
1181
|
+
return JSON_MEDIA;
|
|
1182
|
+
const end = header.indexOf(";");
|
|
1183
|
+
const media = (end === -1 ? header : header.slice(0, end)).trim();
|
|
1184
|
+
return media === "" ? JSON_MEDIA : media.toLowerCase();
|
|
1185
|
+
};
|
|
1186
|
+
var flatten = (issue) => {
|
|
1187
|
+
const path = issue.path?.map((segment) => String(typeof segment === "object" ? segment.key : segment)).join(".");
|
|
1188
|
+
return path === undefined || path === "" ? { message: issue.message } : { message: issue.message, path };
|
|
1189
|
+
};
|
|
1190
|
+
var accept = (source, result) => {
|
|
1191
|
+
if (result.issues !== undefined) {
|
|
1192
|
+
throw new ValidationError(source, result.issues.map(flatten));
|
|
1193
|
+
}
|
|
1194
|
+
return result.value;
|
|
1195
|
+
};
|
|
1196
|
+
var fillWith = (draft, source, schema, value) => {
|
|
1197
|
+
const result = schema["~standard"].validate(value);
|
|
1198
|
+
if (result instanceof Promise) {
|
|
1199
|
+
return result.then((settled) => {
|
|
1200
|
+
draft[source] = accept(source, settled);
|
|
1201
|
+
return draft;
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
draft[source] = accept(source, result);
|
|
1205
|
+
return draft;
|
|
1206
|
+
};
|
|
1207
|
+
var bodyFill = (schema) => (draft) => {
|
|
1208
|
+
const media = mediaTypeOf(draft.req);
|
|
1209
|
+
const parse2 = parserFor(media);
|
|
1210
|
+
if (parse2 === undefined) {
|
|
1211
|
+
throw new HttpError(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE, `Unsupported content type "${media}". Declared bodies accept ` + "application/json, application/x-www-form-urlencoded, multipart/form-data or text/*.");
|
|
1212
|
+
}
|
|
1213
|
+
const read = parse2 === asJson && RawBody.wanted(draft.req) ? draft.req.text().then((text) => {
|
|
1214
|
+
RawBody.record(draft.req, text);
|
|
1215
|
+
return JSON.parse(text);
|
|
1216
|
+
}) : parse2(draft.req);
|
|
1217
|
+
return read.then((value) => fillWith(draft, "body", schema, value), (error) => {
|
|
1218
|
+
throw new HttpError(HttpStatusCode.BAD_REQUEST, `Malformed ${media} body`, { cause: error });
|
|
1219
|
+
});
|
|
1220
|
+
};
|
|
1221
|
+
var searchOf = (url) => {
|
|
1222
|
+
const start = url.indexOf("?");
|
|
1223
|
+
if (start === -1)
|
|
1224
|
+
return "";
|
|
1225
|
+
const end = url.indexOf("#", start + 1);
|
|
1226
|
+
return end === -1 ? url.slice(start + 1) : url.slice(start + 1, end);
|
|
1227
|
+
};
|
|
1228
|
+
var queryFill = (schema) => (draft) => {
|
|
1229
|
+
const params = new URLSearchParams(searchOf(draft.req.url));
|
|
1230
|
+
return fillWith(draft, "query", schema, grouped(params));
|
|
1231
|
+
};
|
|
1232
|
+
var paramsFill = (schema) => (draft) => fillWith(draft, "params", schema, draft.req.params);
|
|
1233
|
+
var then = (first, second) => (draft) => {
|
|
1234
|
+
const started = first(draft);
|
|
1235
|
+
return started instanceof Promise ? started.then(second) : second(started);
|
|
1236
|
+
};
|
|
1237
|
+
var buildInputReader = (options) => {
|
|
1238
|
+
const fills = [];
|
|
1239
|
+
if (options?.body !== undefined)
|
|
1240
|
+
fills.push(bodyFill(options.body));
|
|
1241
|
+
if (options?.query !== undefined)
|
|
1242
|
+
fills.push(queryFill(options.query));
|
|
1243
|
+
if (options?.params !== undefined)
|
|
1244
|
+
fills.push(paramsFill(options.params));
|
|
1245
|
+
if (fills.length === 0)
|
|
1246
|
+
return (req) => ({ req });
|
|
1247
|
+
const fill = fills.reduce(then);
|
|
1248
|
+
return (req) => fill({ req });
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1251
|
+
// src/server/middleware.ts
|
|
1252
|
+
var compose = (middleware, ctx, handler) => middleware.reduceRight((next, current) => (req) => current.handle(req, ctx, () => next(req)), handler);
|
|
1253
|
+
|
|
1254
|
+
// src/server/routes.ts
|
|
1255
|
+
var construct = (guard) => new guard;
|
|
1256
|
+
var toResponse = (value, status) => {
|
|
1257
|
+
if (value instanceof Response)
|
|
1258
|
+
return value;
|
|
1259
|
+
if (value === undefined || value === null) {
|
|
1260
|
+
return new Response(null, { status: HttpStatusCode.NO_CONTENT });
|
|
1261
|
+
}
|
|
1262
|
+
return Response.json(value, { status });
|
|
1263
|
+
};
|
|
1264
|
+
var statusFor = (route) => route.options?.status ?? defaultStatusFor(route.method);
|
|
1265
|
+
var assertNoCollisions = (discovered) => {
|
|
1266
|
+
const owners = new Map;
|
|
1267
|
+
for (const route of discovered) {
|
|
1268
|
+
const key = `${route.method} ${route.path}`;
|
|
1269
|
+
const owner = `${route.controller}.${route.handlerName}`;
|
|
1270
|
+
const existing = owners.get(key);
|
|
1271
|
+
if (existing !== undefined) {
|
|
1272
|
+
throw new AppError6(`Route collision: ${key} is declared by ${existing} and by ${owner}. ` + "Bun would keep only one of them.");
|
|
1273
|
+
}
|
|
1274
|
+
owners.set(key, owner);
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
var assertNoGatewayCollisions = (discovered, gatewayPaths) => {
|
|
1278
|
+
const gateways = new Set(gatewayPaths);
|
|
1279
|
+
for (const route of discovered) {
|
|
1280
|
+
if (gateways.has(route.path)) {
|
|
1281
|
+
throw new AppError6(`Gateway path collision: ${route.path} is served by a gateway and by ` + `${route.controller}.${route.handlerName}(). The upgrade is a route too, ` + "so one of them would be dropped.");
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1285
|
+
var withUpgradeRoutes = (routes, gateways) => {
|
|
1286
|
+
const merged = { ...routes };
|
|
1287
|
+
for (const [path, upgrade] of gateways)
|
|
1288
|
+
merged[path] = { GET: upgrade };
|
|
1289
|
+
return merged;
|
|
1290
|
+
};
|
|
1291
|
+
var unmatchedContext = (req, isPublic) => Object.freeze({
|
|
1292
|
+
controller: "(unmatched)",
|
|
1293
|
+
handler: "(none)",
|
|
1294
|
+
method: req.method,
|
|
1295
|
+
path: new URL(req.url).pathname,
|
|
1296
|
+
parsesBody: false,
|
|
1297
|
+
get: (key) => {
|
|
1298
|
+
if (key.id === UNMATCHED.id)
|
|
1299
|
+
return true;
|
|
1300
|
+
if (key.id === PUBLIC.id && isPublic)
|
|
1301
|
+
return true;
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
var buildFallback = (middleware = [], onError = defaultErrorMapper, cors, notFound = "guarded") => {
|
|
1306
|
+
const miss = () => {
|
|
1307
|
+
throw new HttpError(HttpStatusCode.NOT_FOUND, "NOT_FOUND");
|
|
1308
|
+
};
|
|
1309
|
+
const run = async (req) => {
|
|
1310
|
+
try {
|
|
1311
|
+
return await compose(middleware, unmatchedContext(req, notFound === "public"), miss)(req);
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
return TraceContext.stamp(onError(error, req), req);
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
return cors ? withCors(cors, run) : run;
|
|
1317
|
+
};
|
|
1318
|
+
var directOr = (guarded, route, read, status, onError, noMiddleware) => {
|
|
1319
|
+
if (!noMiddleware)
|
|
1320
|
+
return guarded;
|
|
1321
|
+
const settle2 = (value, req) => {
|
|
1322
|
+
try {
|
|
1323
|
+
return toResponse(value, status);
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
return onError(error, req);
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
const invoke = (input, req) => {
|
|
1329
|
+
try {
|
|
1330
|
+
const value = route.handler(input);
|
|
1331
|
+
return value instanceof Promise ? value.then((resolved) => settle2(resolved, req), (error) => onError(error, req)) : settle2(value, req);
|
|
1332
|
+
} catch (error) {
|
|
1333
|
+
return onError(error, req);
|
|
1334
|
+
}
|
|
1335
|
+
};
|
|
1336
|
+
return (req) => {
|
|
1337
|
+
try {
|
|
1338
|
+
const input = read(req);
|
|
1339
|
+
return input instanceof Promise ? input.then((resolved) => invoke(resolved, req), (error) => onError(error, req)) : invoke(input, req);
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
return onError(error, req);
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
};
|
|
1345
|
+
var buildRoutes = (discovered, middleware = [], onError = defaultErrorMapper, cors, resolve = construct) => {
|
|
1346
|
+
assertNoCollisions(discovered);
|
|
1347
|
+
const routes = {};
|
|
1348
|
+
const instances = new Map;
|
|
1349
|
+
const guardOf = (guard, from) => {
|
|
1350
|
+
const existing = instances.get(guard);
|
|
1351
|
+
if (existing)
|
|
1352
|
+
return existing;
|
|
1353
|
+
const created = resolve(guard, from);
|
|
1354
|
+
instances.set(guard, created);
|
|
1355
|
+
return created;
|
|
1356
|
+
};
|
|
1357
|
+
for (const route of discovered) {
|
|
1358
|
+
const read = buildInputReader(route.options);
|
|
1359
|
+
const status = statusFor(route);
|
|
1360
|
+
const chain = [
|
|
1361
|
+
...middleware,
|
|
1362
|
+
...(route.moduleMiddleware ?? []).map((entry) => guardOf(entry, route.module)),
|
|
1363
|
+
...(route.guards ?? []).map((guard) => guardOf(guard, route.module))
|
|
1364
|
+
];
|
|
1365
|
+
const chained = compose(chain, buildContext(route), async (req) => toResponse(await route.handler(await read(req)), status));
|
|
1366
|
+
const guarded = async (req) => {
|
|
1367
|
+
try {
|
|
1368
|
+
return await chained(req);
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
return TraceContext.stamp(onError(error, req), req);
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
const byMethod = routes[route.path] ??= {};
|
|
1374
|
+
byMethod[route.method] = cors ? withCors(cors, guarded) : directOr(guarded, route, read, status, onError, chain.length === 0);
|
|
1375
|
+
}
|
|
1376
|
+
if (cors) {
|
|
1377
|
+
for (const byMethod of Object.values(routes)) {
|
|
1378
|
+
byMethod.OPTIONS = preflight(cors, Object.keys(byMethod));
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return routes;
|
|
1382
|
+
};
|
|
551
1383
|
|
|
552
1384
|
// src/server/settings.ts
|
|
553
1385
|
var defaultSettings = () => ({ "trust proxy": false });
|
|
554
1386
|
|
|
555
1387
|
// src/server/application.ts
|
|
556
|
-
class HttpApplication {
|
|
1388
|
+
class HttpApplication extends ShutdownAware {
|
|
557
1389
|
warnings;
|
|
558
1390
|
#root;
|
|
559
1391
|
closed;
|
|
@@ -576,14 +1408,15 @@ class HttpApplication {
|
|
|
576
1408
|
#server;
|
|
577
1409
|
#resolveClosed;
|
|
578
1410
|
#shuttingDown;
|
|
579
|
-
#hooks = new ShutdownHooks;
|
|
580
1411
|
constructor(app, discovered, options, root, websocket) {
|
|
1412
|
+
super();
|
|
581
1413
|
this.#app = app;
|
|
582
1414
|
this.#root = root;
|
|
583
1415
|
this.warnings = app.warnings;
|
|
584
1416
|
this.#discovered = discovered;
|
|
585
1417
|
this.#middleware = [
|
|
586
1418
|
...options.requestLogging === false ? [] : [RequestLoggingMiddleware],
|
|
1419
|
+
...usesMetricsMiddleware(options) ? [MetricsMiddleware] : [],
|
|
587
1420
|
...options.middleware ?? []
|
|
588
1421
|
];
|
|
589
1422
|
this.#onError = options.onError === undefined ? errorMapper(app.get(Logger3)) : toErrorMapper(options.onError, (token) => app.get(token, root));
|
|
@@ -666,6 +1499,7 @@ class HttpApplication {
|
|
|
666
1499
|
server: this.#server,
|
|
667
1500
|
trustProxy: this.#settings["trust proxy"]
|
|
668
1501
|
});
|
|
1502
|
+
this.#app.get(RequestMetrics).attach(this.#server);
|
|
669
1503
|
const pubsub = this.#app.get(PubSub);
|
|
670
1504
|
pubsub.attach(this.#server);
|
|
671
1505
|
if (this.#relay) {
|
|
@@ -732,10 +1566,6 @@ class HttpApplication {
|
|
|
732
1566
|
})();
|
|
733
1567
|
return this.#shuttingDown;
|
|
734
1568
|
}
|
|
735
|
-
enableShutdownHooks(signals = ["SIGTERM", "SIGINT"], options = {}) {
|
|
736
|
-
this.#hooks.install(() => this.shutdown(), signals, options);
|
|
737
|
-
return this;
|
|
738
|
-
}
|
|
739
1569
|
#prefixed() {
|
|
740
1570
|
if (this.#globalPrefix === "")
|
|
741
1571
|
return this.#discovered;
|
|
@@ -747,7 +1577,7 @@ class HttpApplication {
|
|
|
747
1577
|
#assertNotStarted(hook) {
|
|
748
1578
|
if (!this.#started)
|
|
749
1579
|
return;
|
|
750
|
-
throw new
|
|
1580
|
+
throw new AppError7(`${hook} must be called before listen(). The route table and the middleware ` + "chain are folded into one closure per route when the server binds, so " + "this call could not take effect.");
|
|
751
1581
|
}
|
|
752
1582
|
}
|
|
753
1583
|
Object.defineProperty(HttpApplication, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "app: App", typeOnly: "App" }, { unresolved: "discovered: readonly DiscoveredRoute[]" }, { unresolved: "options: HttpOptions" }, { unresolved: "root: ModuleRef", typeOnly: "ModuleRef" }, { unresolved: "websocket?: WebSocketRuntime", typeOnly: "WebSocketRuntime" }] });
|
|
@@ -776,6 +1606,9 @@ class HttpOptionsProvider {
|
|
|
776
1606
|
get socketLogging() {
|
|
777
1607
|
return true;
|
|
778
1608
|
}
|
|
1609
|
+
get metrics() {
|
|
1610
|
+
return false;
|
|
1611
|
+
}
|
|
779
1612
|
get onError() {
|
|
780
1613
|
return;
|
|
781
1614
|
}
|
|
@@ -806,6 +1639,7 @@ function resolveHttpOptions(settings, given) {
|
|
|
806
1639
|
cors: settings.cors,
|
|
807
1640
|
requestLogging: settings.requestLogging,
|
|
808
1641
|
socketLogging: settings.socketLogging,
|
|
1642
|
+
metrics: settings.metrics,
|
|
809
1643
|
onError: settings.onError,
|
|
810
1644
|
websocket: settings.websocket,
|
|
811
1645
|
relay: settings.relay,
|
|
@@ -828,15 +1662,24 @@ var pick = (given, fallback) => {
|
|
|
828
1662
|
class HttpFactory {
|
|
829
1663
|
static async create(root, options = {}) {
|
|
830
1664
|
const logging = provide(RequestLoggingMiddleware, {
|
|
831
|
-
useFactory: (logger, context, settings) => new RequestLoggingMiddleware(logger, context, pick(options.requestLogging, settings.requestLogging)),
|
|
832
|
-
inject: [
|
|
1665
|
+
useFactory: (logger, context, settings, metrics) => new RequestLoggingMiddleware(logger, context, pick(options.requestLogging, settings.requestLogging), options.metrics ?? settings.metrics ? metrics : undefined),
|
|
1666
|
+
inject: [
|
|
1667
|
+
Logger4,
|
|
1668
|
+
RequestContext3,
|
|
1669
|
+
HttpOptionsProvider,
|
|
1670
|
+
RequestMetrics
|
|
1671
|
+
]
|
|
1672
|
+
});
|
|
1673
|
+
const metricsMiddleware = provide(MetricsMiddleware, {
|
|
1674
|
+
useFactory: (metrics) => new MetricsMiddleware(metrics),
|
|
1675
|
+
inject: [RequestMetrics]
|
|
833
1676
|
});
|
|
834
1677
|
const socketLogging = provide(SocketLoggingMiddleware, {
|
|
835
1678
|
useFactory: (logger, context, settings) => new SocketLoggingMiddleware(logger, context, pick(options.socketLogging, settings.socketLogging)),
|
|
836
1679
|
inject: [Logger4, RequestContext3, HttpOptionsProvider]
|
|
837
1680
|
});
|
|
838
|
-
const services = [PubSub, ClientAddress];
|
|
839
|
-
const providers = [...services, logging, socketLogging];
|
|
1681
|
+
const services = [PubSub, ClientAddress, RequestMetrics];
|
|
1682
|
+
const providers = [...services, logging, metricsMiddleware, socketLogging];
|
|
840
1683
|
const scope = {
|
|
841
1684
|
module: HttpModule,
|
|
842
1685
|
global: true,
|
|
@@ -856,7 +1699,7 @@ class HttpFactory {
|
|
|
856
1699
|
for (const controller of readControllers(module)) {
|
|
857
1700
|
const routes = discoverRoutes(app.get(controller, module.ref));
|
|
858
1701
|
if (routes.length === 0) {
|
|
859
|
-
throw new
|
|
1702
|
+
throw new AppError8(`${controller.name} is registered as a controller but declares no routes. ` + "Add a @Get/@Post/... method, or move it to providers.");
|
|
860
1703
|
}
|
|
861
1704
|
discovered.push(...routes.map((route) => ({
|
|
862
1705
|
...route,
|
|
@@ -883,6 +1726,27 @@ class HttpFactory {
|
|
|
883
1726
|
}
|
|
884
1727
|
// src/static/files.ts
|
|
885
1728
|
import { join, normalize, resolve } from "path";
|
|
1729
|
+
|
|
1730
|
+
// src/static/options.ts
|
|
1731
|
+
class StaticOptions {
|
|
1732
|
+
root;
|
|
1733
|
+
path;
|
|
1734
|
+
maxAge;
|
|
1735
|
+
immutable;
|
|
1736
|
+
constructor(init) {
|
|
1737
|
+
this.root = init.root;
|
|
1738
|
+
this.path = normalizePrefix(init.path ?? "/");
|
|
1739
|
+
this.maxAge = init.maxAge ?? 60;
|
|
1740
|
+
this.immutable = init.immutable ?? (() => false);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
Object.defineProperty(StaticOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: StaticOptionsInit" }] });
|
|
1744
|
+
var normalizePrefix = (path) => {
|
|
1745
|
+
const trimmed = path.split("/").filter(Boolean).join("/");
|
|
1746
|
+
return trimmed === "" ? "/" : `/${trimmed}`;
|
|
1747
|
+
};
|
|
1748
|
+
|
|
1749
|
+
// src/static/files.ts
|
|
886
1750
|
class StaticFiles {
|
|
887
1751
|
#options;
|
|
888
1752
|
#root;
|
|
@@ -979,6 +1843,99 @@ StaticModule = __decorateElement(_init, 0, "StaticModule", _dec, StaticModule);
|
|
|
979
1843
|
__runInitializers(_init, 1, StaticModule);
|
|
980
1844
|
__decoratorMetadata(_init, StaticModule);
|
|
981
1845
|
let _StaticModule = StaticModule;
|
|
1846
|
+
// src/compression/negotiate.ts
|
|
1847
|
+
var quality = (params) => {
|
|
1848
|
+
for (const param of params) {
|
|
1849
|
+
const [key, value] = param.split("=");
|
|
1850
|
+
if (key?.trim().toLowerCase() !== "q")
|
|
1851
|
+
continue;
|
|
1852
|
+
const q = Number.parseFloat(value ?? "");
|
|
1853
|
+
return Number.isFinite(q) && q >= 0 && q <= 1 ? q : 1;
|
|
1854
|
+
}
|
|
1855
|
+
return 1;
|
|
1856
|
+
};
|
|
1857
|
+
var negotiate = (header, offered) => {
|
|
1858
|
+
if (header === null)
|
|
1859
|
+
return;
|
|
1860
|
+
const accepted = new Map;
|
|
1861
|
+
for (const element of header.split(",")) {
|
|
1862
|
+
const [name, ...params] = element.split(";");
|
|
1863
|
+
const token = name?.trim().toLowerCase();
|
|
1864
|
+
if (token === undefined || token === "")
|
|
1865
|
+
continue;
|
|
1866
|
+
accepted.set(token, quality(params));
|
|
1867
|
+
}
|
|
1868
|
+
const wildcard = accepted.get("*");
|
|
1869
|
+
let best;
|
|
1870
|
+
let bestQuality = 0;
|
|
1871
|
+
for (const encoding of offered) {
|
|
1872
|
+
const q = accepted.get(encoding) ?? wildcard ?? 0;
|
|
1873
|
+
if (q > bestQuality) {
|
|
1874
|
+
best = encoding;
|
|
1875
|
+
bestQuality = q;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
return best;
|
|
1879
|
+
};
|
|
1880
|
+
|
|
1881
|
+
// src/compression/options.ts
|
|
1882
|
+
var CompressionEncoding = Object.freeze({
|
|
1883
|
+
ZSTD: "zstd",
|
|
1884
|
+
GZIP: "gzip"
|
|
1885
|
+
});
|
|
1886
|
+
var COMPRESSIBLE = new Set([
|
|
1887
|
+
"application/graphql",
|
|
1888
|
+
"application/graphql-response+json",
|
|
1889
|
+
"application/javascript",
|
|
1890
|
+
"application/json",
|
|
1891
|
+
"application/manifest+json",
|
|
1892
|
+
"application/wasm",
|
|
1893
|
+
"application/x-javascript",
|
|
1894
|
+
"application/x-ndjson",
|
|
1895
|
+
"application/xml",
|
|
1896
|
+
"image/svg+xml"
|
|
1897
|
+
]);
|
|
1898
|
+
var isCompressibleType = (contentType) => {
|
|
1899
|
+
if (contentType === null)
|
|
1900
|
+
return false;
|
|
1901
|
+
const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
1902
|
+
if (type.startsWith("text/"))
|
|
1903
|
+
return true;
|
|
1904
|
+
if (type.endsWith("+json") || type.endsWith("+xml"))
|
|
1905
|
+
return true;
|
|
1906
|
+
return COMPRESSIBLE.has(type);
|
|
1907
|
+
};
|
|
1908
|
+
var encodable = (encoding) => {
|
|
1909
|
+
const sync = encoding === CompressionEncoding.ZSTD ? Bun.zstdCompressSync : Bun.gzipSync;
|
|
1910
|
+
if (typeof sync !== "function")
|
|
1911
|
+
return false;
|
|
1912
|
+
try {
|
|
1913
|
+
new CompressionStream(encoding);
|
|
1914
|
+
return true;
|
|
1915
|
+
} catch {
|
|
1916
|
+
return false;
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
|
|
1920
|
+
class CompressionOptions {
|
|
1921
|
+
encodings;
|
|
1922
|
+
threshold;
|
|
1923
|
+
filter;
|
|
1924
|
+
constructor(init = {}) {
|
|
1925
|
+
this.encodings = init.encodings ?? [
|
|
1926
|
+
CompressionEncoding.ZSTD,
|
|
1927
|
+
CompressionEncoding.GZIP
|
|
1928
|
+
];
|
|
1929
|
+
const missing = this.encodings.filter((encoding) => !encodable(encoding));
|
|
1930
|
+
if (missing.length > 0) {
|
|
1931
|
+
throw new Error(`Bun ${Bun.version} cannot encode ${missing.join(", ")}. ` + "Pass `encodings` without it, or upgrade Bun.");
|
|
1932
|
+
}
|
|
1933
|
+
this.threshold = init.threshold ?? 1024;
|
|
1934
|
+
this.filter = init.filter ?? isCompressibleType;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
Object.defineProperty(CompressionOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: CompressionOptionsInit = {}", optional: true }] });
|
|
1938
|
+
|
|
982
1939
|
// src/compression/compression.ts
|
|
983
1940
|
var BODYLESS = new Set([204, 205, 304]);
|
|
984
1941
|
var BUFFER_LIMIT = 1024 * 1024;
|
|
@@ -1159,7 +2116,7 @@ var SkipThrottle = () => meta(SKIP_THROTTLE, true);
|
|
|
1159
2116
|
import { Logger as Logger5 } from "@dunx/core";
|
|
1160
2117
|
|
|
1161
2118
|
// src/throttle/options.ts
|
|
1162
|
-
import { AppError as
|
|
2119
|
+
import { AppError as AppError9 } from "@dunx/core";
|
|
1163
2120
|
|
|
1164
2121
|
class ThrottleOptions {
|
|
1165
2122
|
limit;
|
|
@@ -1170,13 +2127,13 @@ class ThrottleOptions {
|
|
|
1170
2127
|
store;
|
|
1171
2128
|
constructor(init) {
|
|
1172
2129
|
if (init.prefix.trim() === "") {
|
|
1173
|
-
throw new
|
|
2130
|
+
throw new AppError9("ThrottleModule needs a prefix naming this application, and it has no " + "default: two apps sharing one Redis with one throttle namespace each " + "spend the other's budget. Pass something like { prefix: 'orders-api' }.");
|
|
1174
2131
|
}
|
|
1175
2132
|
if (!Number.isInteger(init.limit) || init.limit < 1) {
|
|
1176
|
-
throw new
|
|
2133
|
+
throw new AppError9(`ThrottleModule needs a limit of at least 1; got ${init.limit}.`);
|
|
1177
2134
|
}
|
|
1178
2135
|
if (!Number.isInteger(init.windowSeconds) || init.windowSeconds < 1) {
|
|
1179
|
-
throw new
|
|
2136
|
+
throw new AppError9("ThrottleModule needs a windowSeconds of at least 1; got " + `${init.windowSeconds}.`);
|
|
1180
2137
|
}
|
|
1181
2138
|
this.limit = init.limit;
|
|
1182
2139
|
this.windowSeconds = init.windowSeconds;
|
|
@@ -1189,12 +2146,12 @@ class ThrottleOptions {
|
|
|
1189
2146
|
Object.defineProperty(ThrottleOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: ThrottleOptionsInit" }] });
|
|
1190
2147
|
|
|
1191
2148
|
// src/throttle/store.ts
|
|
1192
|
-
import { AppError as
|
|
2149
|
+
import { AppError as AppError10 } from "@dunx/core";
|
|
1193
2150
|
|
|
1194
2151
|
class ThrottleStore {
|
|
1195
2152
|
constructor() {
|
|
1196
2153
|
if (new.target === ThrottleStore) {
|
|
1197
|
-
throw new
|
|
2154
|
+
throw new AppError10("ThrottleStore is a contract, not an implementation. Bind one with " + "ThrottleModule.forRoot({ store: new RedisThrottleStore(redis) }), or " + "leave it out for the in-process MemoryThrottleStore.");
|
|
1198
2155
|
}
|
|
1199
2156
|
}
|
|
1200
2157
|
}
|
|
@@ -1398,21 +2355,8 @@ var OnMessage = (event) => (value) => {
|
|
|
1398
2355
|
return value;
|
|
1399
2356
|
};
|
|
1400
2357
|
// src/ws/postgres-relay.ts
|
|
1401
|
-
import { AppError as AppError7 } from "@dunx/core";
|
|
1402
2358
|
var PROTOCOLS = ["postgres:", "postgresql:"];
|
|
1403
2359
|
var defaultPostgresRelayUrl = () => process.env["POSTGRES_URL"] ?? process.env["DATABASE_URL"] ?? "postgres://localhost:5432";
|
|
1404
|
-
var assertUrl = (url) => {
|
|
1405
|
-
let parsed;
|
|
1406
|
-
try {
|
|
1407
|
-
parsed = new URL(url);
|
|
1408
|
-
} catch {
|
|
1409
|
-
throw new AppError7(`${JSON.stringify(url)} is not a valid URL for the websocket relay. ` + "Expected something like postgres://localhost:5432/app.");
|
|
1410
|
-
}
|
|
1411
|
-
if (!PROTOCOLS.includes(parsed.protocol)) {
|
|
1412
|
-
throw new AppError7(`Unsupported protocol ${JSON.stringify(parsed.protocol)} in ` + `${JSON.stringify(url)}. Expected one of ${PROTOCOLS.join(", ")}.`);
|
|
1413
|
-
}
|
|
1414
|
-
return url;
|
|
1415
|
-
};
|
|
1416
2360
|
|
|
1417
2361
|
class PostgresRelay extends WsRelay {
|
|
1418
2362
|
#url;
|
|
@@ -1421,14 +2365,11 @@ class PostgresRelay extends WsRelay {
|
|
|
1421
2365
|
#subscription;
|
|
1422
2366
|
constructor(options = {}) {
|
|
1423
2367
|
super();
|
|
1424
|
-
this.#url =
|
|
2368
|
+
this.#url = assertRelayUrl(options.url ?? defaultPostgresRelayUrl(), PROTOCOLS, "postgres://localhost:5432/app");
|
|
1425
2369
|
this.#max = options.max ?? 1;
|
|
1426
2370
|
}
|
|
1427
2371
|
get url() {
|
|
1428
|
-
|
|
1429
|
-
if (parsed.password)
|
|
1430
|
-
parsed.password = "***";
|
|
1431
|
-
return parsed.toString();
|
|
2372
|
+
return redactUrl(this.#url);
|
|
1432
2373
|
}
|
|
1433
2374
|
#client() {
|
|
1434
2375
|
return this.#sql ??= new Bun.SQL({ url: this.#url, max: this.#max });
|
|
@@ -1462,6 +2403,82 @@ class PostgresRelay extends WsRelay {
|
|
|
1462
2403
|
}
|
|
1463
2404
|
}
|
|
1464
2405
|
Object.defineProperty(PostgresRelay, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "options: PostgresRelayOptions = {}", optional: true }] });
|
|
2406
|
+
// src/ws/redis-relay.ts
|
|
2407
|
+
var PROTOCOLS2 = [
|
|
2408
|
+
"redis:",
|
|
2409
|
+
"rediss:",
|
|
2410
|
+
"valkey:",
|
|
2411
|
+
"valkeys:",
|
|
2412
|
+
"redis+tls:",
|
|
2413
|
+
"redis+unix:",
|
|
2414
|
+
"redis+tls+unix:"
|
|
2415
|
+
];
|
|
2416
|
+
var defaultRelayUrl = () => process.env["VALKEY_URL"] ?? process.env["REDIS_URL"] ?? "redis://localhost:6379";
|
|
2417
|
+
|
|
2418
|
+
class RedisRelay extends WsRelay {
|
|
2419
|
+
#url;
|
|
2420
|
+
#options;
|
|
2421
|
+
#pub;
|
|
2422
|
+
#sub;
|
|
2423
|
+
#channel;
|
|
2424
|
+
constructor(options = {}) {
|
|
2425
|
+
super();
|
|
2426
|
+
this.#url = assertRelayUrl(options.url ?? defaultRelayUrl(), PROTOCOLS2, "redis://localhost:6379");
|
|
2427
|
+
this.#options = {
|
|
2428
|
+
maxRetries: options.maxRetries ?? 0,
|
|
2429
|
+
...options.connectionTimeout !== undefined && {
|
|
2430
|
+
connectionTimeout: options.connectionTimeout
|
|
2431
|
+
},
|
|
2432
|
+
...options.tls !== undefined && { tls: options.tls }
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
get url() {
|
|
2436
|
+
return redactUrl(this.#url);
|
|
2437
|
+
}
|
|
2438
|
+
async publish(channel, message) {
|
|
2439
|
+
const client = this.#pub ??= new Bun.RedisClient(this.#url, this.#options);
|
|
2440
|
+
try {
|
|
2441
|
+
return await client.publish(channel, message);
|
|
2442
|
+
} catch (error) {
|
|
2443
|
+
if (this.#pub === client) {
|
|
2444
|
+
this.#pub = undefined;
|
|
2445
|
+
client.close();
|
|
2446
|
+
}
|
|
2447
|
+
throw error;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
async subscribe(channel, listener) {
|
|
2451
|
+
const client = this.#sub ??= new Bun.RedisClient(this.#url, this.#options);
|
|
2452
|
+
try {
|
|
2453
|
+
await client.connect();
|
|
2454
|
+
await client.subscribe(channel, listener);
|
|
2455
|
+
this.#channel = channel;
|
|
2456
|
+
} catch (error) {
|
|
2457
|
+
if (this.#sub === client) {
|
|
2458
|
+
this.#sub = undefined;
|
|
2459
|
+
client.close();
|
|
2460
|
+
}
|
|
2461
|
+
throw error;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
async close() {
|
|
2465
|
+
const sub = this.#sub;
|
|
2466
|
+
const channel = this.#channel;
|
|
2467
|
+
this.#pub?.close();
|
|
2468
|
+
this.#pub = undefined;
|
|
2469
|
+
this.#sub = undefined;
|
|
2470
|
+
this.#channel = undefined;
|
|
2471
|
+
if (!sub)
|
|
2472
|
+
return;
|
|
2473
|
+
if (channel !== undefined) {
|
|
2474
|
+
try {
|
|
2475
|
+
await sub.unsubscribe(channel);
|
|
2476
|
+
} catch {}
|
|
2477
|
+
}
|
|
2478
|
+
sub.close();
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
Object.defineProperty(RedisRelay, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "options: RedisRelayOptions = {}", optional: true }] });
|
|
1465
2482
|
// src/ws/relay-module.ts
|
|
1466
2483
|
import {
|
|
1467
2484
|
provide as provide5
|
|
@@ -1603,6 +2620,201 @@ class PingProbe {
|
|
|
1603
2620
|
|
|
1604
2621
|
class QueryProbe {
|
|
1605
2622
|
}
|
|
2623
|
+
// src/health/controller.ts
|
|
2624
|
+
import { inject } from "@dunx/core";
|
|
2625
|
+
|
|
2626
|
+
// src/health/report-schema.ts
|
|
2627
|
+
var state = {
|
|
2628
|
+
type: "string",
|
|
2629
|
+
enum: ["up", "down", "unknown"],
|
|
2630
|
+
description: "`unknown` is not `down`: a probe that timed out has told you nothing."
|
|
2631
|
+
};
|
|
2632
|
+
var HEALTH_REPORT_SCHEMA = Object.freeze({
|
|
2633
|
+
$id: "HealthReport",
|
|
2634
|
+
type: "object",
|
|
2635
|
+
description: "What the probe found. `up` answers 200 and anything else answers 503.",
|
|
2636
|
+
properties: {
|
|
2637
|
+
status: state,
|
|
2638
|
+
draining: {
|
|
2639
|
+
type: "boolean",
|
|
2640
|
+
description: "The process is shutting down, or something holds it out."
|
|
2641
|
+
},
|
|
2642
|
+
uptimeMs: {
|
|
2643
|
+
type: "integer",
|
|
2644
|
+
description: "Measured on a monotonic clock, so it never goes backwards."
|
|
2645
|
+
},
|
|
2646
|
+
checks: {
|
|
2647
|
+
type: "array",
|
|
2648
|
+
items: {
|
|
2649
|
+
type: "object",
|
|
2650
|
+
properties: {
|
|
2651
|
+
name: { type: "string" },
|
|
2652
|
+
state,
|
|
2653
|
+
critical: {
|
|
2654
|
+
type: "boolean",
|
|
2655
|
+
description: "A failure here sheds traffic. Memory and disk do not."
|
|
2656
|
+
},
|
|
2657
|
+
ms: { type: "integer", description: "How long the check took." },
|
|
2658
|
+
detail: {
|
|
2659
|
+
type: "string",
|
|
2660
|
+
description: "A latency, a version, or a failure message."
|
|
2661
|
+
}
|
|
2662
|
+
},
|
|
2663
|
+
required: ["name", "state", "critical", "ms"]
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
},
|
|
2667
|
+
required: ["status", "draining", "uptimeMs", "checks"]
|
|
2668
|
+
});
|
|
2669
|
+
|
|
2670
|
+
// src/health/registry.ts
|
|
2671
|
+
var bounded = async (indicator, timeoutMs) => {
|
|
2672
|
+
let timer;
|
|
2673
|
+
const timeout = new Promise((resolve2) => {
|
|
2674
|
+
timer = setTimeout(() => resolve2({ state: "unknown", detail: `no answer in ${timeoutMs} ms` }), timeoutMs);
|
|
2675
|
+
timer.unref?.();
|
|
2676
|
+
});
|
|
2677
|
+
try {
|
|
2678
|
+
return await Promise.race([
|
|
2679
|
+
Promise.resolve().then(() => indicator.check()).catch((error) => ({
|
|
2680
|
+
state: "down",
|
|
2681
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
2682
|
+
})),
|
|
2683
|
+
timeout
|
|
2684
|
+
]);
|
|
2685
|
+
} finally {
|
|
2686
|
+
if (timer)
|
|
2687
|
+
clearTimeout(timer);
|
|
2688
|
+
}
|
|
2689
|
+
};
|
|
2690
|
+
var worst = (checks) => {
|
|
2691
|
+
const critical = checks.filter((check) => check.critical);
|
|
2692
|
+
if (critical.some((check) => check.state === "down"))
|
|
2693
|
+
return "down";
|
|
2694
|
+
if (critical.some((check) => check.state === "unknown"))
|
|
2695
|
+
return "unknown";
|
|
2696
|
+
return "up";
|
|
2697
|
+
};
|
|
2698
|
+
|
|
2699
|
+
class HealthOptions {
|
|
2700
|
+
liveness;
|
|
2701
|
+
readiness;
|
|
2702
|
+
timeoutMs;
|
|
2703
|
+
routes;
|
|
2704
|
+
documented;
|
|
2705
|
+
drainDelayMs;
|
|
2706
|
+
constructor(init = {}) {
|
|
2707
|
+
this.liveness = init.liveness ?? [];
|
|
2708
|
+
this.readiness = init.readiness ?? [];
|
|
2709
|
+
this.timeoutMs = init.timeoutMs ?? 2000;
|
|
2710
|
+
this.routes = init.routes ?? true;
|
|
2711
|
+
this.documented = init.documented ?? true;
|
|
2712
|
+
this.drainDelayMs = Math.max(0, init.drainDelayMs ?? 0);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
Object.defineProperty(HealthOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: HealthOptionsInit = {}", optional: true }] });
|
|
2716
|
+
|
|
2717
|
+
class HealthRegistry {
|
|
2718
|
+
options;
|
|
2719
|
+
readiness_;
|
|
2720
|
+
#startedAt = performance.now();
|
|
2721
|
+
constructor(options, readiness_) {
|
|
2722
|
+
this.options = options;
|
|
2723
|
+
this.readiness_ = readiness_;
|
|
2724
|
+
}
|
|
2725
|
+
async report(indicators) {
|
|
2726
|
+
const checks = await Promise.all(indicators.map(async (indicator) => {
|
|
2727
|
+
const started = performance.now();
|
|
2728
|
+
const result = await bounded(indicator, this.options.timeoutMs);
|
|
2729
|
+
return {
|
|
2730
|
+
name: indicator.name,
|
|
2731
|
+
state: result.state,
|
|
2732
|
+
critical: indicator.critical,
|
|
2733
|
+
ms: Math.round(performance.now() - started),
|
|
2734
|
+
...result.detail === undefined ? {} : { detail: result.detail }
|
|
2735
|
+
};
|
|
2736
|
+
}));
|
|
2737
|
+
return {
|
|
2738
|
+
status: worst(checks),
|
|
2739
|
+
draining: this.readiness_.draining,
|
|
2740
|
+
uptimeMs: Math.round(performance.now() - this.#startedAt),
|
|
2741
|
+
checks
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
liveness() {
|
|
2745
|
+
return this.report(this.options.liveness);
|
|
2746
|
+
}
|
|
2747
|
+
async readiness() {
|
|
2748
|
+
const report = await this.report(this.options.readiness);
|
|
2749
|
+
if (!this.readiness_.draining)
|
|
2750
|
+
return report;
|
|
2751
|
+
return {
|
|
2752
|
+
...report,
|
|
2753
|
+
status: "down",
|
|
2754
|
+
checks: [
|
|
2755
|
+
{
|
|
2756
|
+
name: "readiness",
|
|
2757
|
+
state: "down",
|
|
2758
|
+
critical: true,
|
|
2759
|
+
ms: 0,
|
|
2760
|
+
detail: this.readiness_.reason ?? "not accepting traffic"
|
|
2761
|
+
},
|
|
2762
|
+
...report.checks
|
|
2763
|
+
]
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
Object.defineProperty(HealthRegistry, Symbol.for("dunx.deps"), { value: () => [HealthOptions, { unresolved: "private readonly readiness_: Readiness", typeOnly: "Readiness" }] });
|
|
2768
|
+
|
|
2769
|
+
// src/health/controller.ts
|
|
2770
|
+
var probeResponses = {
|
|
2771
|
+
response: { 200: HEALTH_REPORT_SCHEMA, 503: HEALTH_REPORT_SCHEMA }
|
|
2772
|
+
};
|
|
2773
|
+
var answer = (report) => Response.json(report, { status: report.status === "up" ? 200 : 503 });
|
|
2774
|
+
var _dec = [
|
|
2775
|
+
Controller("health")
|
|
2776
|
+
];
|
|
2777
|
+
var _dec2 = [
|
|
2778
|
+
Public(),
|
|
2779
|
+
Get("/live", probeResponses)
|
|
2780
|
+
];
|
|
2781
|
+
var _dec3 = [
|
|
2782
|
+
Public(),
|
|
2783
|
+
Get("/ready", probeResponses)
|
|
2784
|
+
];
|
|
2785
|
+
var _health = new WeakMap;
|
|
2786
|
+
var _init = __decoratorStart(undefined);
|
|
2787
|
+
|
|
2788
|
+
class HealthController {
|
|
2789
|
+
constructor() {
|
|
2790
|
+
__privateAdd(this, _health, inject(HealthRegistry));
|
|
2791
|
+
__runInitializers(_init, 5, this);
|
|
2792
|
+
}
|
|
2793
|
+
async live() {
|
|
2794
|
+
return answer(await __privateGet(this, _health).liveness());
|
|
2795
|
+
}
|
|
2796
|
+
async ready() {
|
|
2797
|
+
return answer(await __privateGet(this, _health).readiness());
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
__decorateElement(_init, 1, "live", _dec2, HealthController);
|
|
2801
|
+
__decorateElement(_init, 1, "ready", _dec3, HealthController);
|
|
2802
|
+
HealthController = __decorateElement(_init, 0, "HealthController", _dec, HealthController);
|
|
2803
|
+
__runInitializers(_init, 1, HealthController);
|
|
2804
|
+
__decoratorMetadata(_init, HealthController);
|
|
2805
|
+
let _HealthController = HealthController;
|
|
2806
|
+
var _dec = [
|
|
2807
|
+
ApiHidden()
|
|
2808
|
+
];
|
|
2809
|
+
var _base = HealthController;
|
|
2810
|
+
var _init = __decoratorStart(_base);
|
|
2811
|
+
|
|
2812
|
+
class HiddenHealthController extends _base {
|
|
2813
|
+
}
|
|
2814
|
+
HiddenHealthController = __decorateElement(_init, 0, "HiddenHealthController", _dec, HiddenHealthController);
|
|
2815
|
+
__runInitializers(_init, 1, HiddenHealthController);
|
|
2816
|
+
__decoratorMetadata(_init, HiddenHealthController);
|
|
2817
|
+
let _HiddenHealthController = HiddenHealthController;
|
|
1606
2818
|
// src/health/indicators.ts
|
|
1607
2819
|
import { statfs } from "fs/promises";
|
|
1608
2820
|
var ms = (started) => Math.round(performance.now() - started);
|
|
@@ -1819,6 +3031,7 @@ export {
|
|
|
1819
3031
|
MemoryIndicator,
|
|
1820
3032
|
MemoryOptions,
|
|
1821
3033
|
MemoryThrottleStore,
|
|
3034
|
+
MetricsMiddleware,
|
|
1822
3035
|
OnClose,
|
|
1823
3036
|
OnDrain,
|
|
1824
3037
|
OnMessage,
|
|
@@ -1836,7 +3049,6 @@ export {
|
|
|
1836
3049
|
Public,
|
|
1837
3050
|
Put,
|
|
1838
3051
|
QueryProbe,
|
|
1839
|
-
REQUEST_ID_HEADER,
|
|
1840
3052
|
ROLES,
|
|
1841
3053
|
Readiness,
|
|
1842
3054
|
ReadinessOptions,
|
|
@@ -1845,6 +3057,7 @@ export {
|
|
|
1845
3057
|
RedisThrottleStore,
|
|
1846
3058
|
RelayConnectionOptions,
|
|
1847
3059
|
RequestLoggingMiddleware,
|
|
3060
|
+
RequestMetrics,
|
|
1848
3061
|
Roles,
|
|
1849
3062
|
SKIP_THROTTLE,
|
|
1850
3063
|
SkipThrottle,
|
|
@@ -1854,6 +3067,7 @@ export {
|
|
|
1854
3067
|
StaticOptions,
|
|
1855
3068
|
THROTTLE,
|
|
1856
3069
|
TRACEPARENT_HEADER,
|
|
3070
|
+
TRACERESPONSE_HEADER,
|
|
1857
3071
|
TRACESTATE_HEADER,
|
|
1858
3072
|
Throttle,
|
|
1859
3073
|
ThrottleGuard,
|
|
@@ -1862,6 +3076,7 @@ export {
|
|
|
1862
3076
|
ThrottleStore,
|
|
1863
3077
|
TraceContext,
|
|
1864
3078
|
UNMATCHED,
|
|
3079
|
+
UNMATCHED_ROUTE,
|
|
1865
3080
|
UseGuards,
|
|
1866
3081
|
ValidationError,
|
|
1867
3082
|
WsRelay,
|