@dunx/http 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-8939brh2.js +174 -0
- package/dist/chunk-bg0dr54z.js +37 -0
- package/dist/chunk-gmtwad7f.js +71 -0
- package/dist/client.js +128 -12
- package/dist/index.js +1411 -155
- package/dist/inspect.d.ts +9 -0
- package/dist/internal.d.ts +8 -25
- package/dist/internal.js +27 -99
- package/dist/server/application.d.ts +7 -99
- package/dist/server/binding.d.ts +67 -0
- package/dist/server/factory.d.ts +4 -2
- package/dist/server/html.d.ts +10 -0
- package/dist/server/metrics.d.ts +8 -3
- package/dist/server/options-provider.d.ts +14 -1
- package/dist/server/options.d.ts +141 -0
- 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/dist/ws/socket.d.ts +16 -0
- package/package.json +4 -4
- package/dist/chunk-9x3evk19.js +0 -177
- package/dist/chunk-e8a9c6j2.js +0 -130
- package/dist/chunk-y85wcdhw.js +0 -1297
package/dist/index.js
CHANGED
|
@@ -1,71 +1,122 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
HttpStatusCode2
|
|
4
|
+
} from "./chunk-bg0dr54z.js";
|
|
5
|
+
import {
|
|
6
|
+
defaultStatusFor2,
|
|
7
|
+
markRoute,
|
|
8
|
+
markController,
|
|
9
|
+
metaKey2,
|
|
10
|
+
meta2,
|
|
11
|
+
ROLES2,
|
|
12
|
+
PUBLIC2,
|
|
13
|
+
HIDDEN2,
|
|
14
|
+
UNMATCHED2,
|
|
15
|
+
Roles2,
|
|
16
|
+
Public2,
|
|
17
|
+
ApiHidden2,
|
|
18
|
+
UseGuards2,
|
|
19
|
+
metaOf2,
|
|
20
|
+
mergeMeta2,
|
|
21
|
+
joinPath2,
|
|
22
|
+
discoverRoutes2,
|
|
13
23
|
HandlerKind,
|
|
14
|
-
HealthController,
|
|
15
|
-
HealthOptions,
|
|
16
|
-
HealthRegistry,
|
|
17
|
-
HiddenHealthController,
|
|
18
|
-
HttpError,
|
|
19
|
-
PUBLIC,
|
|
20
|
-
Patch,
|
|
21
|
-
Post,
|
|
22
|
-
Public,
|
|
23
|
-
Put,
|
|
24
|
-
ROLES,
|
|
25
|
-
RawBody,
|
|
26
|
-
RedisRelay,
|
|
27
|
-
Roles,
|
|
28
|
-
StaticOptions,
|
|
29
|
-
UNMATCHED,
|
|
30
|
-
UseGuards,
|
|
31
|
-
ValidationError,
|
|
32
|
-
WsRelay,
|
|
33
|
-
assertNoCollisions,
|
|
34
|
-
assertNoGatewayCollisions,
|
|
35
|
-
buildFallback,
|
|
36
|
-
buildRoutes,
|
|
37
|
-
buildWebSocket,
|
|
38
|
-
decodeRelay,
|
|
39
|
-
defaultErrorMapper,
|
|
40
|
-
defaultRelayError,
|
|
41
|
-
discoverGateways,
|
|
42
|
-
discoverRoutes,
|
|
43
|
-
encode,
|
|
44
|
-
encodeRelay,
|
|
45
|
-
errorMapper,
|
|
46
|
-
joinPath,
|
|
47
|
-
markGateway,
|
|
48
24
|
markHandler,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
negotiate,
|
|
54
|
-
observe,
|
|
55
|
-
toErrorMapper,
|
|
56
|
-
withUpgradeRoutes
|
|
57
|
-
} from "./chunk-y85wcdhw.js";
|
|
25
|
+
markGateway,
|
|
26
|
+
discoverGateways,
|
|
27
|
+
buildContext2
|
|
28
|
+
} from "./chunk-8939brh2.js";
|
|
58
29
|
import {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
30
|
+
TRACEPARENT_HEADER2,
|
|
31
|
+
TRACESTATE_HEADER2,
|
|
32
|
+
TRACERESPONSE_HEADER2,
|
|
33
|
+
TraceContext2
|
|
34
|
+
} from "./chunk-gmtwad7f.js";
|
|
35
|
+
var __create = Object.create;
|
|
36
|
+
var __defProp = Object.defineProperty;
|
|
37
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
38
|
+
var __name = (target, name) => {
|
|
39
|
+
Object.defineProperty(target, "name", {
|
|
40
|
+
value: name,
|
|
41
|
+
enumerable: false,
|
|
42
|
+
configurable: true
|
|
43
|
+
});
|
|
44
|
+
return target;
|
|
45
|
+
};
|
|
46
|
+
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
|
|
47
|
+
var __typeError = (msg) => {
|
|
48
|
+
throw TypeError(msg);
|
|
49
|
+
};
|
|
50
|
+
var __defNormalProp = (obj, key, value) => (key in obj) ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
51
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
52
|
+
var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
|
|
53
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
54
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
55
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
56
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
57
|
+
var __decoratorStart = (base) => [, , , __create(base?.[__knownSymbol("metadata")] ?? null)];
|
|
58
|
+
var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
|
|
59
|
+
var __expectFn = (fn) => fn !== undefined && typeof fn !== "function" ? __typeError("Function expected") : fn;
|
|
60
|
+
var __decoratorContext = (kind, name, done, metadata, fns) => ({
|
|
61
|
+
kind: __decoratorStrings[kind],
|
|
62
|
+
name,
|
|
63
|
+
metadata,
|
|
64
|
+
addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null))
|
|
65
|
+
});
|
|
66
|
+
var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
|
|
67
|
+
var __runInitializers = (array, flags, self, value) => {
|
|
68
|
+
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length;i < n; i++)
|
|
69
|
+
flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
|
|
70
|
+
return value;
|
|
71
|
+
};
|
|
72
|
+
var __decorateElement = (array, flags, name, decorators, target, extra) => {
|
|
73
|
+
var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
|
|
74
|
+
var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
|
|
75
|
+
var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
|
|
76
|
+
var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : {
|
|
77
|
+
get [name]() {
|
|
78
|
+
return __privateGet(this, extra);
|
|
79
|
+
},
|
|
80
|
+
set [name](x) {
|
|
81
|
+
__privateSet(this, extra, x);
|
|
82
|
+
}
|
|
83
|
+
}, name));
|
|
84
|
+
k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
|
|
85
|
+
for (var i = decorators.length - 1;i >= 0; i--) {
|
|
86
|
+
ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
|
|
87
|
+
if (k) {
|
|
88
|
+
ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => (name in x) };
|
|
89
|
+
if (k ^ 3)
|
|
90
|
+
access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
|
|
91
|
+
if (k > 2)
|
|
92
|
+
access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
|
|
93
|
+
}
|
|
94
|
+
it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? undefined : { get: desc.get, set: desc.set } : target, ctx);
|
|
95
|
+
done._ = 1;
|
|
96
|
+
if (k ^ 4 || it === undefined)
|
|
97
|
+
__expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
|
|
98
|
+
else if (typeof it !== "object" || it === null)
|
|
99
|
+
__typeError("Object expected");
|
|
100
|
+
else
|
|
101
|
+
__expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
|
|
102
|
+
}
|
|
103
|
+
return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// src/route/decorators.ts
|
|
107
|
+
var Controller = (prefix = "") => (target) => {
|
|
108
|
+
markController(target, prefix);
|
|
109
|
+
return target;
|
|
110
|
+
};
|
|
111
|
+
var verb = (method) => (path = "/", options) => (value, _context) => {
|
|
112
|
+
markRoute(value, { method, path, options });
|
|
113
|
+
return value;
|
|
114
|
+
};
|
|
115
|
+
var Get = verb("GET");
|
|
116
|
+
var Post = verb("POST");
|
|
117
|
+
var Put = verb("PUT");
|
|
118
|
+
var Patch = verb("PATCH");
|
|
119
|
+
var Delete = verb("DELETE");
|
|
69
120
|
// src/server/client-address.ts
|
|
70
121
|
import { AppError } from "@dunx/core";
|
|
71
122
|
var trustedHops = (setting) => {
|
|
@@ -87,7 +138,7 @@ class ClientAddress {
|
|
|
87
138
|
}
|
|
88
139
|
const hops = trustedHops(source.trustProxy);
|
|
89
140
|
if (hops > 0) {
|
|
90
|
-
const entries = (req.headers.get("x-forwarded-for") ?? "").split(",").map((
|
|
141
|
+
const entries = (req.headers.get("x-forwarded-for") ?? "").split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
91
142
|
const entry = entries[Math.max(0, entries.length - hops)];
|
|
92
143
|
if (entry)
|
|
93
144
|
return unmap(entry);
|
|
@@ -98,10 +149,67 @@ class ClientAddress {
|
|
|
98
149
|
var attachAddressSource = (target, source) => {
|
|
99
150
|
sources.set(target, source);
|
|
100
151
|
};
|
|
152
|
+
// src/server/errors.ts
|
|
153
|
+
import { AppError as AppError2, ConsoleLogger } from "@dunx/core";
|
|
154
|
+
class HttpError extends AppError2 {
|
|
155
|
+
status;
|
|
156
|
+
name = "HttpError";
|
|
157
|
+
headers;
|
|
158
|
+
constructor(status, message, options) {
|
|
159
|
+
super(message, options);
|
|
160
|
+
this.status = status;
|
|
161
|
+
this.headers = options?.headers;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
Object.defineProperty(HttpError, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "override readonly status: number" }, { unresolved: "message: string" }, { unresolved: "options?: HttpErrorOptions" }] });
|
|
165
|
+
|
|
166
|
+
class ValidationError extends HttpError {
|
|
167
|
+
source;
|
|
168
|
+
issues;
|
|
169
|
+
name = "ValidationError";
|
|
170
|
+
constructor(source, issues) {
|
|
171
|
+
super(HttpStatusCode2.BAD_REQUEST, `Invalid ${source}`);
|
|
172
|
+
this.source = source;
|
|
173
|
+
this.issues = issues;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
Object.defineProperty(ValidationError, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "readonly source: InputSource" }, { unresolved: "readonly issues: readonly ValidationIssue[]" }] });
|
|
177
|
+
|
|
178
|
+
class ErrorFilter {
|
|
179
|
+
}
|
|
180
|
+
var isErrorFilter = (handler) => typeof handler === "function" && typeof handler.prototype?.catch === "function";
|
|
181
|
+
var toErrorMapper = (handler, resolve) => isErrorFilter(handler) ? (error, req) => resolve(handler).catch(error, req) : handler;
|
|
182
|
+
var errorMapper = (logger) => (error) => {
|
|
183
|
+
if (error instanceof ValidationError) {
|
|
184
|
+
return Response.json({ error: error.message, status: error.status, issues: error.issues }, {
|
|
185
|
+
status: error.status,
|
|
186
|
+
...error.headers && { headers: error.headers }
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (error instanceof HttpError) {
|
|
190
|
+
return Response.json({ error: error.message, status: error.status }, {
|
|
191
|
+
status: error.status,
|
|
192
|
+
...error.headers && { headers: error.headers }
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (error instanceof AppError2 && isStatus(error.status)) {
|
|
196
|
+
if (error.status >= HttpStatusCode2.INTERNAL_SERVER_ERROR) {
|
|
197
|
+
logger.error("Unhandled error", error);
|
|
198
|
+
}
|
|
199
|
+
return Response.json({ error: error.message, status: error.status }, { status: error.status });
|
|
200
|
+
}
|
|
201
|
+
logger.error("Unhandled error", error);
|
|
202
|
+
return Response.json({
|
|
203
|
+
error: "Internal Server Error",
|
|
204
|
+
status: HttpStatusCode2.INTERNAL_SERVER_ERROR
|
|
205
|
+
}, { status: HttpStatusCode2.INTERNAL_SERVER_ERROR });
|
|
206
|
+
};
|
|
207
|
+
var isStatus = (value) => value !== undefined && Number.isInteger(value) && value >= 200 && value <= 599;
|
|
208
|
+
var defaultErrorMapper = errorMapper(new ConsoleLogger);
|
|
101
209
|
// src/server/factory.ts
|
|
102
210
|
import {
|
|
103
211
|
collectModules,
|
|
104
|
-
AppError as
|
|
212
|
+
AppError as AppError8,
|
|
105
213
|
AppFactory,
|
|
106
214
|
Logger as Logger4,
|
|
107
215
|
provide,
|
|
@@ -130,7 +238,7 @@ class RequestMetrics {
|
|
|
130
238
|
observe(ctx, status, durationNs, traceId) {
|
|
131
239
|
let series = this.#series.get(ctx);
|
|
132
240
|
if (series === undefined) {
|
|
133
|
-
if (ctx.get(
|
|
241
|
+
if (ctx.get(UNMATCHED2) === true) {
|
|
134
242
|
series = this.#unmatched.get(ctx.method);
|
|
135
243
|
if (series === undefined) {
|
|
136
244
|
series = seriesFor(UNMATCHED_ROUTE, ctx.method);
|
|
@@ -194,13 +302,332 @@ class MetricsMiddleware {
|
|
|
194
302
|
this.metrics.observe(ctx, response.status, Bun.nanoseconds() - started);
|
|
195
303
|
return response;
|
|
196
304
|
}, (error) => {
|
|
197
|
-
this.metrics.observe(ctx, error instanceof HttpError ? error.status :
|
|
305
|
+
this.metrics.observe(ctx, error instanceof HttpError ? error.status : HttpStatusCode2.INTERNAL_SERVER_ERROR, Bun.nanoseconds() - started);
|
|
198
306
|
throw error;
|
|
199
307
|
});
|
|
200
308
|
}
|
|
201
309
|
}
|
|
202
310
|
Object.defineProperty(MetricsMiddleware, Symbol.for("dunx.deps"), { value: () => [RequestMetrics] });
|
|
203
311
|
|
|
312
|
+
// src/ws/envelope.ts
|
|
313
|
+
var encode = (event, data) => JSON.stringify({ event, data });
|
|
314
|
+
var decode = (message) => {
|
|
315
|
+
if (typeof message !== "string")
|
|
316
|
+
return;
|
|
317
|
+
let parsed;
|
|
318
|
+
try {
|
|
319
|
+
parsed = JSON.parse(message);
|
|
320
|
+
} catch {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
324
|
+
return;
|
|
325
|
+
const { event, data } = parsed;
|
|
326
|
+
return typeof event === "string" ? { event, data } : undefined;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// src/ws/middleware.ts
|
|
330
|
+
var composeSocket = (middleware, ctx) => middleware.reduceRight((next, current) => (frame, run) => current.handle(frame, ctx, () => next(frame, run)), (_frame, run) => run());
|
|
331
|
+
var observe = (next, done) => {
|
|
332
|
+
let result;
|
|
333
|
+
try {
|
|
334
|
+
result = next();
|
|
335
|
+
} catch (error) {
|
|
336
|
+
done(error, undefined);
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
if (result instanceof Promise) {
|
|
340
|
+
return result.then((value) => {
|
|
341
|
+
done(undefined, value);
|
|
342
|
+
return value;
|
|
343
|
+
}, (error) => {
|
|
344
|
+
done(error, undefined);
|
|
345
|
+
throw error;
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
done(undefined, result);
|
|
349
|
+
return result;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// src/ws/runtime.ts
|
|
353
|
+
import { AppError as AppError3 } from "@dunx/core";
|
|
354
|
+
var slotOf = (handler) => handler.kind === HandlerKind.MESSAGE && handler.event !== undefined ? `message ${JSON.stringify(handler.event)}` : handler.kind;
|
|
355
|
+
var buildRuntime = (gateway) => {
|
|
356
|
+
if (gateway.handlers.length === 0) {
|
|
357
|
+
throw new AppError3(`${gateway.name} is registered as a gateway but declares no handlers. ` + "Add an @OnMessage/@OnOpen/... method, or drop the @Gateway decorator.");
|
|
358
|
+
}
|
|
359
|
+
const owners = new Map;
|
|
360
|
+
const events = new Map;
|
|
361
|
+
for (const handler of gateway.handlers) {
|
|
362
|
+
const slot = slotOf(handler);
|
|
363
|
+
const existing = owners.get(slot);
|
|
364
|
+
if (existing) {
|
|
365
|
+
throw new AppError3(`Handler collision in ${gateway.name}: ${slot} is claimed by ` + `${existing.method}() and by ${handler.method}(). One handler per event.`);
|
|
366
|
+
}
|
|
367
|
+
owners.set(slot, handler);
|
|
368
|
+
if (handler.kind === HandlerKind.MESSAGE && handler.event !== undefined) {
|
|
369
|
+
events.set(handler.event, handler.invoke);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const at = (slot) => owners.get(slot)?.invoke;
|
|
373
|
+
return {
|
|
374
|
+
name: gateway.name,
|
|
375
|
+
path: gateway.path,
|
|
376
|
+
upgrade: at(HandlerKind.UPGRADE),
|
|
377
|
+
open: at(HandlerKind.OPEN),
|
|
378
|
+
close: at(HandlerKind.CLOSE),
|
|
379
|
+
drain: at(HandlerKind.DRAIN),
|
|
380
|
+
ping: at(HandlerKind.PING),
|
|
381
|
+
pong: at(HandlerKind.PONG),
|
|
382
|
+
raw: at(HandlerKind.MESSAGE),
|
|
383
|
+
events
|
|
384
|
+
};
|
|
385
|
+
};
|
|
386
|
+
var buildGateways = (discovered) => {
|
|
387
|
+
const byPath = new Map;
|
|
388
|
+
for (const gateway of discovered) {
|
|
389
|
+
const existing = byPath.get(gateway.path);
|
|
390
|
+
if (existing) {
|
|
391
|
+
throw new AppError3(`Gateway path collision: ${gateway.path} is served by ${existing.name} ` + `and by ${gateway.name}. One gateway per path.`);
|
|
392
|
+
}
|
|
393
|
+
byPath.set(gateway.path, buildRuntime(gateway));
|
|
394
|
+
}
|
|
395
|
+
return byPath;
|
|
396
|
+
};
|
|
397
|
+
var someHandler = (gateways, pick) => {
|
|
398
|
+
for (const gateway of gateways)
|
|
399
|
+
if (pick(gateway) !== undefined)
|
|
400
|
+
return true;
|
|
401
|
+
return false;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// src/ws/adapter.ts
|
|
405
|
+
var RUNTIME = Symbol.for("dunx.ws.runtime");
|
|
406
|
+
var UNCLAIMED = Symbol.for("dunx.ws.unclaimed");
|
|
407
|
+
var defaultOnError = (error, socket) => {
|
|
408
|
+
console.error(`[dunx/http] ${socket.data.path} handler failed:`, error);
|
|
409
|
+
};
|
|
410
|
+
var reportedByMiddleware = () => {
|
|
411
|
+
return;
|
|
412
|
+
};
|
|
413
|
+
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(", ")}.`;
|
|
414
|
+
var runtimeOf = (socket) => socket.data[RUNTIME];
|
|
415
|
+
var isBinary = (value) => value instanceof ArrayBuffer || ArrayBuffer.isView(value) || value instanceof Blob;
|
|
416
|
+
var replyRaw = (socket, value) => {
|
|
417
|
+
if (value === undefined)
|
|
418
|
+
return;
|
|
419
|
+
socket.send(typeof value === "string" || isBinary(value) ? value : JSON.stringify(value));
|
|
420
|
+
};
|
|
421
|
+
var settle = (result, socket, onError, then) => {
|
|
422
|
+
if (result instanceof Promise) {
|
|
423
|
+
result.then((value) => {
|
|
424
|
+
if (!then)
|
|
425
|
+
return;
|
|
426
|
+
try {
|
|
427
|
+
then(value);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
onError(error, socket);
|
|
430
|
+
}
|
|
431
|
+
}, (error) => onError(error, socket));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (then)
|
|
435
|
+
then(result);
|
|
436
|
+
};
|
|
437
|
+
var framing = (kind) => {
|
|
438
|
+
if (kind === HandlerKind.CLOSE) {
|
|
439
|
+
return (args) => ({
|
|
440
|
+
socket: args[0],
|
|
441
|
+
data: { code: args[1], reason: args[2] }
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
if (kind === HandlerKind.OPEN || kind === HandlerKind.DRAIN) {
|
|
445
|
+
return (args) => ({ socket: args[0], data: undefined });
|
|
446
|
+
}
|
|
447
|
+
return (args) => ({ socket: args[1], data: args[0] });
|
|
448
|
+
};
|
|
449
|
+
var NOTHING = () => {
|
|
450
|
+
return;
|
|
451
|
+
};
|
|
452
|
+
var through = (gateway, middleware, kind, event, invoke) => {
|
|
453
|
+
const ctx = {
|
|
454
|
+
gateway: gateway.name,
|
|
455
|
+
path: gateway.path,
|
|
456
|
+
kind,
|
|
457
|
+
event
|
|
458
|
+
};
|
|
459
|
+
const dispatch = composeSocket(middleware, ctx);
|
|
460
|
+
const frameOf = framing(kind);
|
|
461
|
+
const run = invoke ?? NOTHING;
|
|
462
|
+
return (...args) => dispatch(frameOf(args), () => run(...args));
|
|
463
|
+
};
|
|
464
|
+
var withMiddleware = (gateway, middleware) => {
|
|
465
|
+
const wrap = (kind, event, invoke) => through(gateway, middleware, kind, event, invoke);
|
|
466
|
+
const optional = (kind, invoke) => invoke === undefined ? undefined : wrap(kind, undefined, invoke);
|
|
467
|
+
return {
|
|
468
|
+
...gateway,
|
|
469
|
+
open: wrap(HandlerKind.OPEN, undefined, gateway.open),
|
|
470
|
+
close: wrap(HandlerKind.CLOSE, undefined, gateway.close),
|
|
471
|
+
drain: optional(HandlerKind.DRAIN, gateway.drain),
|
|
472
|
+
ping: optional(HandlerKind.PING, gateway.ping),
|
|
473
|
+
pong: optional(HandlerKind.PONG, gateway.pong),
|
|
474
|
+
raw: optional(HandlerKind.MESSAGE, gateway.raw),
|
|
475
|
+
events: new Map([...gateway.events].map(([event, invoke]) => [
|
|
476
|
+
event,
|
|
477
|
+
wrap(HandlerKind.MESSAGE, event, invoke)
|
|
478
|
+
]))
|
|
479
|
+
};
|
|
480
|
+
};
|
|
481
|
+
var unclaimedDispatch = (gateway, middleware) => (frame, event) => composeSocket(middleware, {
|
|
482
|
+
gateway: gateway.name,
|
|
483
|
+
path: gateway.path,
|
|
484
|
+
kind: HandlerKind.MESSAGE,
|
|
485
|
+
event
|
|
486
|
+
})(frame, () => {
|
|
487
|
+
return;
|
|
488
|
+
});
|
|
489
|
+
var buildWebSocket = (discovered, options = {}, middleware = []) => {
|
|
490
|
+
const byPath = buildGateways(discovered);
|
|
491
|
+
const wrapped = middleware.length === 0 ? byPath : new Map([...byPath].map(([path, gateway]) => [
|
|
492
|
+
path,
|
|
493
|
+
withMiddleware(gateway, middleware)
|
|
494
|
+
]));
|
|
495
|
+
const gateways = [...wrapped.values()];
|
|
496
|
+
const onError = options.onError ?? (middleware.length === 0 ? defaultOnError : reportedByMiddleware);
|
|
497
|
+
const reports = options.onError !== undefined || middleware.some((entry) => entry.reportsErrors === true);
|
|
498
|
+
const {
|
|
499
|
+
onError: _onError,
|
|
500
|
+
binaryType,
|
|
501
|
+
backpressureLimit,
|
|
502
|
+
closeOnBackpressureLimit,
|
|
503
|
+
idleTimeout,
|
|
504
|
+
maxPayloadLength,
|
|
505
|
+
perMessageDeflate,
|
|
506
|
+
publishToSelf,
|
|
507
|
+
sendPings,
|
|
508
|
+
...rest
|
|
509
|
+
} = options;
|
|
510
|
+
const handlerOptions = {
|
|
511
|
+
...backpressureLimit !== undefined && { backpressureLimit },
|
|
512
|
+
...closeOnBackpressureLimit !== undefined && { closeOnBackpressureLimit },
|
|
513
|
+
...idleTimeout !== undefined && { idleTimeout },
|
|
514
|
+
...maxPayloadLength !== undefined && { maxPayloadLength },
|
|
515
|
+
...perMessageDeflate !== undefined && { perMessageDeflate },
|
|
516
|
+
...publishToSelf !== undefined && { publishToSelf },
|
|
517
|
+
...sendPings !== undefined && { sendPings }
|
|
518
|
+
};
|
|
519
|
+
const run = (invoke, args, ws, then) => {
|
|
520
|
+
try {
|
|
521
|
+
settle(invoke(...args), ws, onError, then);
|
|
522
|
+
} catch (error) {
|
|
523
|
+
onError(error, ws);
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
const websocket = {
|
|
527
|
+
...handlerOptions,
|
|
528
|
+
message(ws, message) {
|
|
529
|
+
const gateway = runtimeOf(ws);
|
|
530
|
+
let event;
|
|
531
|
+
if (gateway.events.size > 0) {
|
|
532
|
+
const envelope = decode(message);
|
|
533
|
+
const handler = envelope && gateway.events.get(envelope.event);
|
|
534
|
+
if (envelope && handler) {
|
|
535
|
+
run(handler, [envelope.data, ws], ws, (value) => {
|
|
536
|
+
if (value !== undefined)
|
|
537
|
+
ws.send(encode(envelope.event, value));
|
|
538
|
+
});
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
event = envelope?.event;
|
|
542
|
+
}
|
|
543
|
+
if (gateway.raw) {
|
|
544
|
+
run(gateway.raw, [message, ws], ws, (value) => replyRaw(ws, value));
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const unclaimed = ws.data[UNCLAIMED];
|
|
548
|
+
if (!unclaimed)
|
|
549
|
+
return;
|
|
550
|
+
try {
|
|
551
|
+
settle(unclaimed({ socket: ws, data: message }, event), ws, onError, undefined);
|
|
552
|
+
} catch (error) {
|
|
553
|
+
onError(error, ws);
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
...(someHandler(gateways, (g) => g.open) || binaryType !== undefined) && {
|
|
557
|
+
open(ws) {
|
|
558
|
+
if (binaryType !== undefined)
|
|
559
|
+
ws.binaryType = binaryType;
|
|
560
|
+
const { open } = runtimeOf(ws);
|
|
561
|
+
if (open)
|
|
562
|
+
run(open, [ws], ws, undefined);
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
...someHandler(gateways, (g) => g.close) && {
|
|
566
|
+
close(ws, code, reason) {
|
|
567
|
+
const { close } = runtimeOf(ws);
|
|
568
|
+
if (close)
|
|
569
|
+
run(close, [ws, code, reason], ws, undefined);
|
|
570
|
+
}
|
|
571
|
+
},
|
|
572
|
+
...someHandler(gateways, (g) => g.drain) && {
|
|
573
|
+
drain(ws) {
|
|
574
|
+
const { drain } = runtimeOf(ws);
|
|
575
|
+
if (drain)
|
|
576
|
+
run(drain, [ws], ws, undefined);
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
...someHandler(gateways, (g) => g.ping) && {
|
|
580
|
+
ping(ws, data) {
|
|
581
|
+
const { ping } = runtimeOf(ws);
|
|
582
|
+
if (ping)
|
|
583
|
+
run(ping, [data, ws], ws, undefined);
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
...someHandler(gateways, (g) => g.pong) && {
|
|
587
|
+
pong(ws, data) {
|
|
588
|
+
const { pong } = runtimeOf(ws);
|
|
589
|
+
if (pong)
|
|
590
|
+
run(pong, [data, ws], ws, undefined);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
const unclaimed = new Map(middleware.length === 0 ? [] : gateways.map((gateway) => [
|
|
595
|
+
gateway,
|
|
596
|
+
unclaimedDispatch(gateway, middleware)
|
|
597
|
+
]));
|
|
598
|
+
const accept = (req, server, gateway, context) => {
|
|
599
|
+
const fallback = unclaimed.get(gateway);
|
|
600
|
+
const data = {
|
|
601
|
+
path: gateway.path,
|
|
602
|
+
context,
|
|
603
|
+
id: crypto.randomUUID(),
|
|
604
|
+
[RUNTIME]: gateway,
|
|
605
|
+
...fallback === undefined ? {} : { [UNCLAIMED]: fallback }
|
|
606
|
+
};
|
|
607
|
+
return server.upgrade(req, { data }) ? undefined : new Response("Expected a WebSocket upgrade", { status: 426 });
|
|
608
|
+
};
|
|
609
|
+
const upgradeHandler = (gateway) => (req, server) => {
|
|
610
|
+
if (!gateway.upgrade)
|
|
611
|
+
return accept(req, server, gateway, undefined);
|
|
612
|
+
const result = gateway.upgrade(req);
|
|
613
|
+
if (result instanceof Promise) {
|
|
614
|
+
return result.then((value) => value instanceof Response ? value : accept(req, server, gateway, value));
|
|
615
|
+
}
|
|
616
|
+
return result instanceof Response ? result : accept(req, server, gateway, result);
|
|
617
|
+
};
|
|
618
|
+
return {
|
|
619
|
+
websocket,
|
|
620
|
+
routes: new Map(gateways.map((gateway) => [gateway.path, upgradeHandler(gateway)])),
|
|
621
|
+
paths: [...byPath.keys()],
|
|
622
|
+
warnings: middleware.length > 0 && !reports ? [unreported(middleware)] : [],
|
|
623
|
+
gateways: gateways.map((gateway) => ({
|
|
624
|
+
name: gateway.name,
|
|
625
|
+
path: gateway.path,
|
|
626
|
+
events: [...gateway.events.keys()]
|
|
627
|
+
}))
|
|
628
|
+
};
|
|
629
|
+
};
|
|
630
|
+
|
|
204
631
|
// src/ws/logging.ts
|
|
205
632
|
import { Logger, LogLevel, RequestContext } from "@dunx/core";
|
|
206
633
|
var LIFECYCLE_LABEL = {
|
|
@@ -292,6 +719,8 @@ class SocketLoggingMiddleware {
|
|
|
292
719
|
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
|
293
720
|
return `[${data.byteLength} bytes]`;
|
|
294
721
|
}
|
|
722
|
+
if (data instanceof Blob)
|
|
723
|
+
return `[${data.size} bytes]`;
|
|
295
724
|
const text = JSON.stringify(data) ?? "";
|
|
296
725
|
return text.length > this.#limit ? `[${text.length} chars]` : data;
|
|
297
726
|
}
|
|
@@ -299,7 +728,64 @@ class SocketLoggingMiddleware {
|
|
|
299
728
|
Object.defineProperty(SocketLoggingMiddleware, Symbol.for("dunx.deps"), { value: () => [Logger, RequestContext, { unresolved: "options: SocketLoggingOptions = {}", optional: true }] });
|
|
300
729
|
|
|
301
730
|
// src/ws/pubsub.ts
|
|
302
|
-
import { AppError as
|
|
731
|
+
import { AppError as AppError5 } from "@dunx/core";
|
|
732
|
+
|
|
733
|
+
// src/ws/relay.ts
|
|
734
|
+
import { AppError as AppError4 } from "@dunx/core";
|
|
735
|
+
var assertRelayUrl = (url, protocols, example) => {
|
|
736
|
+
let parsed;
|
|
737
|
+
try {
|
|
738
|
+
parsed = new URL(url);
|
|
739
|
+
} catch {
|
|
740
|
+
throw new AppError4(`${JSON.stringify(url)} is not a valid URL for the websocket relay. ` + `Expected something like ${example}.`);
|
|
741
|
+
}
|
|
742
|
+
if (!protocols.includes(parsed.protocol)) {
|
|
743
|
+
throw new AppError4(`Unsupported protocol ${JSON.stringify(parsed.protocol)} in ` + `${JSON.stringify(url)}. Expected one of ${protocols.join(", ")}.`);
|
|
744
|
+
}
|
|
745
|
+
return url;
|
|
746
|
+
};
|
|
747
|
+
var redactUrl = (url) => {
|
|
748
|
+
const parsed = new URL(url);
|
|
749
|
+
if (parsed.password)
|
|
750
|
+
parsed.password = "***";
|
|
751
|
+
return parsed.toString();
|
|
752
|
+
};
|
|
753
|
+
var DEFAULT_RELAY_CHANNEL = "dunx:ws";
|
|
754
|
+
var defaultRelayError = (error, phase) => {
|
|
755
|
+
console.warn(`[dunx/http] the websocket relay could not ${phase}. Fan-out is local to ` + "this process until it recovers:", error);
|
|
756
|
+
};
|
|
757
|
+
var toBytes = (data) => ArrayBuffer.isView(data) ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : new Uint8Array(data);
|
|
758
|
+
var encodeRelay = (origin, topic, data) => typeof data === "string" ? JSON.stringify({ o: origin, t: topic, d: data }) : JSON.stringify({
|
|
759
|
+
o: origin,
|
|
760
|
+
t: topic,
|
|
761
|
+
d: Buffer.from(toBytes(data)).toString("base64"),
|
|
762
|
+
b: 1
|
|
763
|
+
});
|
|
764
|
+
var decodeRelay = (message) => {
|
|
765
|
+
let parsed;
|
|
766
|
+
try {
|
|
767
|
+
parsed = JSON.parse(message);
|
|
768
|
+
} catch {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
772
|
+
return;
|
|
773
|
+
const { o, t, d, b } = parsed;
|
|
774
|
+
if (typeof o !== "string" || typeof t !== "string" || typeof d !== "string") {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
return { origin: o, topic: t, data: b ? Buffer.from(d, "base64") : d };
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
class WsRelay {
|
|
781
|
+
constructor() {
|
|
782
|
+
if (new.target === WsRelay) {
|
|
783
|
+
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.");
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/ws/pubsub.ts
|
|
303
789
|
class PubSub {
|
|
304
790
|
#origin = Bun.randomUUIDv7();
|
|
305
791
|
#server;
|
|
@@ -324,7 +810,7 @@ class PubSub {
|
|
|
324
810
|
}
|
|
325
811
|
async relayThrough(relay, options = {}) {
|
|
326
812
|
if (this.#relay) {
|
|
327
|
-
throw new
|
|
813
|
+
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.");
|
|
328
814
|
}
|
|
329
815
|
this.#relay = relay;
|
|
330
816
|
this.#channel = options.channel ?? DEFAULT_RELAY_CHANNEL;
|
|
@@ -420,7 +906,7 @@ class PubSub {
|
|
|
420
906
|
}
|
|
421
907
|
#live() {
|
|
422
908
|
if (!this.#server) {
|
|
423
|
-
throw new
|
|
909
|
+
throw new AppError5("PubSub has no server yet. Publish once the server is listening: " + "HttpApp.listen() is what attaches it.");
|
|
424
910
|
}
|
|
425
911
|
return this.#server;
|
|
426
912
|
}
|
|
@@ -428,10 +914,10 @@ class PubSub {
|
|
|
428
914
|
|
|
429
915
|
// src/server/application.ts
|
|
430
916
|
import {
|
|
431
|
-
AppError as
|
|
917
|
+
AppError as AppError7,
|
|
432
918
|
Logger as Logger3,
|
|
433
919
|
runtimeInfo,
|
|
434
|
-
|
|
920
|
+
ShutdownAware,
|
|
435
921
|
teardownError,
|
|
436
922
|
teardownFailures as toFailures
|
|
437
923
|
} from "@dunx/core";
|
|
@@ -441,6 +927,27 @@ import {
|
|
|
441
927
|
Logger as Logger2,
|
|
442
928
|
RequestContext as RequestContext2
|
|
443
929
|
} from "@dunx/core";
|
|
930
|
+
|
|
931
|
+
// src/server/raw-body.ts
|
|
932
|
+
var WANTED = Symbol.for("dunx.http.rawBody.wanted");
|
|
933
|
+
var TEXT = Symbol.for("dunx.http.rawBody.text");
|
|
934
|
+
|
|
935
|
+
class RawBody {
|
|
936
|
+
static want(req) {
|
|
937
|
+
req[WANTED] = true;
|
|
938
|
+
}
|
|
939
|
+
static wanted(req) {
|
|
940
|
+
return req[WANTED] === true;
|
|
941
|
+
}
|
|
942
|
+
static record(req, text) {
|
|
943
|
+
req[TEXT] = text;
|
|
944
|
+
}
|
|
945
|
+
static read(req) {
|
|
946
|
+
return req[TEXT];
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/server/request-logging.ts
|
|
444
951
|
var parse = (text, limit) => {
|
|
445
952
|
if (limit === 0)
|
|
446
953
|
return;
|
|
@@ -509,7 +1016,7 @@ class RequestLoggingMiddleware {
|
|
|
509
1016
|
context: `${ctx.controller}.${ctx.handler}`
|
|
510
1017
|
};
|
|
511
1018
|
if (this.#trace) {
|
|
512
|
-
const trace =
|
|
1019
|
+
const trace = TraceContext2.adopt(req, this.#traceResponse);
|
|
513
1020
|
scope.traceId = trace.traceId;
|
|
514
1021
|
scope.spanId = trace.spanId;
|
|
515
1022
|
scope.traceFlags = trace.flags;
|
|
@@ -553,7 +1060,7 @@ class RequestLoggingMiddleware {
|
|
|
553
1060
|
#ignoredWithMetrics(req, ctx, path, next) {
|
|
554
1061
|
const started = Bun.nanoseconds();
|
|
555
1062
|
const failed = (error) => {
|
|
556
|
-
this.#observe(req, ctx, error instanceof HttpError ? error.status :
|
|
1063
|
+
this.#observe(req, ctx, error instanceof HttpError ? error.status : HttpStatusCode2.INTERNAL_SERVER_ERROR, started);
|
|
557
1064
|
throw error;
|
|
558
1065
|
};
|
|
559
1066
|
let settled;
|
|
@@ -568,8 +1075,8 @@ class RequestLoggingMiddleware {
|
|
|
568
1075
|
}, failed);
|
|
569
1076
|
}
|
|
570
1077
|
#correlated(req, ctx, path, next) {
|
|
571
|
-
const trace = this.#trace ?
|
|
572
|
-
const stamp = (response) => trace === undefined ? response :
|
|
1078
|
+
const trace = this.#trace ? TraceContext2.adopt(req, this.#traceResponse) : undefined;
|
|
1079
|
+
const stamp = (response) => trace === undefined ? response : TraceContext2.stamp(response, req);
|
|
573
1080
|
if (!this.#correlate)
|
|
574
1081
|
return next().then(stamp);
|
|
575
1082
|
return this.context.runWithContext({
|
|
@@ -601,7 +1108,7 @@ class RequestLoggingMiddleware {
|
|
|
601
1108
|
}
|
|
602
1109
|
#failed(req, ctx, path, started, request, error, scope) {
|
|
603
1110
|
this.#shared(req, request);
|
|
604
|
-
const status = error instanceof HttpError ? error.status :
|
|
1111
|
+
const status = error instanceof HttpError ? error.status : HttpStatusCode2.INTERNAL_SERVER_ERROR;
|
|
605
1112
|
const entry = {
|
|
606
1113
|
...scope,
|
|
607
1114
|
request,
|
|
@@ -611,7 +1118,7 @@ class RequestLoggingMiddleware {
|
|
|
611
1118
|
};
|
|
612
1119
|
this.#observe(req, ctx, status, started);
|
|
613
1120
|
const line = `${req.method} ${path} ${status}`;
|
|
614
|
-
if (status <
|
|
1121
|
+
if (status < HttpStatusCode2.INTERNAL_SERVER_ERROR) {
|
|
615
1122
|
this.logger.warn(line, entry);
|
|
616
1123
|
} else {
|
|
617
1124
|
this.logger.error(line, entry);
|
|
@@ -628,7 +1135,7 @@ class RequestLoggingMiddleware {
|
|
|
628
1135
|
statusCode: response.status,
|
|
629
1136
|
elapsedMs: elapsedMs2(started)
|
|
630
1137
|
});
|
|
631
|
-
return
|
|
1138
|
+
return TraceContext2.stamp(response, req);
|
|
632
1139
|
}
|
|
633
1140
|
return body.then((value) => {
|
|
634
1141
|
this.logger.info(`${req.method} ${path} ${response.status}`, {
|
|
@@ -638,13 +1145,13 @@ class RequestLoggingMiddleware {
|
|
|
638
1145
|
...value === undefined ? {} : { responseBody: value },
|
|
639
1146
|
elapsedMs: elapsedMs2(started)
|
|
640
1147
|
});
|
|
641
|
-
return
|
|
1148
|
+
return TraceContext2.stamp(response, req);
|
|
642
1149
|
});
|
|
643
1150
|
}
|
|
644
1151
|
#observe(req, ctx, status, started) {
|
|
645
1152
|
if (this.#metrics === undefined)
|
|
646
1153
|
return;
|
|
647
|
-
this.#metrics.observe(ctx, status, Bun.nanoseconds() - started,
|
|
1154
|
+
this.#metrics.observe(ctx, status, Bun.nanoseconds() - started, TraceContext2.of(req)?.traceId);
|
|
648
1155
|
}
|
|
649
1156
|
#body(req, ctx) {
|
|
650
1157
|
if (!this.#requestBody)
|
|
@@ -671,11 +1178,370 @@ class RequestLoggingMiddleware {
|
|
|
671
1178
|
}
|
|
672
1179
|
Object.defineProperty(RequestLoggingMiddleware, Symbol.for("dunx.deps"), { value: () => [Logger2, RequestContext2, { unresolved: "options: RequestLoggingOptions = {}", optional: true }, { unresolved: "metrics?: RequestMetrics", typeOnly: "RequestMetrics" }] });
|
|
673
1180
|
|
|
1181
|
+
// src/server/routes.ts
|
|
1182
|
+
import { AppError as AppError6 } from "@dunx/core";
|
|
1183
|
+
|
|
1184
|
+
// src/server/cors.ts
|
|
1185
|
+
var ORIGIN = "access-control-allow-origin";
|
|
1186
|
+
var allowedOrigin = (options, requested) => {
|
|
1187
|
+
const origin = options.origin ?? "*";
|
|
1188
|
+
if (typeof origin === "string") {
|
|
1189
|
+
if (origin !== "*")
|
|
1190
|
+
return origin === requested ? origin : undefined;
|
|
1191
|
+
if (!options.credentials)
|
|
1192
|
+
return "*";
|
|
1193
|
+
return requested ?? undefined;
|
|
1194
|
+
}
|
|
1195
|
+
if (requested === null)
|
|
1196
|
+
return;
|
|
1197
|
+
const allowed = typeof origin === "function" ? origin(requested) : origin.includes(requested);
|
|
1198
|
+
return allowed ? requested : undefined;
|
|
1199
|
+
};
|
|
1200
|
+
var applyCors = (options, req, response) => {
|
|
1201
|
+
const origin = allowedOrigin(options, req.headers.get("origin"));
|
|
1202
|
+
if (origin === undefined)
|
|
1203
|
+
return response;
|
|
1204
|
+
response.headers.set(ORIGIN, origin);
|
|
1205
|
+
if (origin !== "*")
|
|
1206
|
+
response.headers.append("vary", "Origin");
|
|
1207
|
+
if (options.credentials) {
|
|
1208
|
+
response.headers.set("access-control-allow-credentials", "true");
|
|
1209
|
+
}
|
|
1210
|
+
if (options.exposedHeaders?.length) {
|
|
1211
|
+
response.headers.set("access-control-expose-headers", options.exposedHeaders.join(", "));
|
|
1212
|
+
}
|
|
1213
|
+
return response;
|
|
1214
|
+
};
|
|
1215
|
+
var withCors = (options, handler) => {
|
|
1216
|
+
return async (req) => applyCors(options, req, await handler(req));
|
|
1217
|
+
};
|
|
1218
|
+
var preflight = (options, methods) => {
|
|
1219
|
+
const allowMethods = (options.methods ?? methods).join(", ");
|
|
1220
|
+
return async (req) => {
|
|
1221
|
+
const response = applyCors(options, req, new Response(null, { status: HttpStatusCode2.NO_CONTENT }));
|
|
1222
|
+
if (!response.headers.has(ORIGIN))
|
|
1223
|
+
return response;
|
|
1224
|
+
response.headers.set("access-control-allow-methods", allowMethods);
|
|
1225
|
+
const allowHeaders = options.allowedHeaders ?? (req.headers.get("access-control-request-headers") ?? "").split(",").map((header) => header.trim()).filter((header) => header.length > 0);
|
|
1226
|
+
if (allowHeaders.length > 0) {
|
|
1227
|
+
response.headers.set("access-control-allow-headers", allowHeaders.join(", "));
|
|
1228
|
+
}
|
|
1229
|
+
if (options.maxAge !== undefined) {
|
|
1230
|
+
response.headers.set("access-control-max-age", String(options.maxAge));
|
|
1231
|
+
}
|
|
1232
|
+
return response;
|
|
1233
|
+
};
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
// src/server/input.ts
|
|
1237
|
+
var grouped = (entries) => {
|
|
1238
|
+
const collected = {};
|
|
1239
|
+
entries.forEach((value, key) => {
|
|
1240
|
+
const existing = collected[key];
|
|
1241
|
+
if (existing === undefined)
|
|
1242
|
+
collected[key] = value;
|
|
1243
|
+
else if (Array.isArray(existing))
|
|
1244
|
+
existing.push(value);
|
|
1245
|
+
else
|
|
1246
|
+
collected[key] = [existing, value];
|
|
1247
|
+
});
|
|
1248
|
+
return collected;
|
|
1249
|
+
};
|
|
1250
|
+
var asJson = (req) => req.json();
|
|
1251
|
+
var asUrlEncoded = async (req) => grouped(new URLSearchParams(await req.text()));
|
|
1252
|
+
var asMultipart = async (req) => grouped(await req.formData());
|
|
1253
|
+
var asText = (req) => req.text();
|
|
1254
|
+
var parserFor = (media) => {
|
|
1255
|
+
if (media === "application/json" || media.endsWith("+json"))
|
|
1256
|
+
return asJson;
|
|
1257
|
+
if (media === "application/x-www-form-urlencoded")
|
|
1258
|
+
return asUrlEncoded;
|
|
1259
|
+
if (media === "multipart/form-data")
|
|
1260
|
+
return asMultipart;
|
|
1261
|
+
if (media.startsWith("text/"))
|
|
1262
|
+
return asText;
|
|
1263
|
+
return;
|
|
1264
|
+
};
|
|
1265
|
+
var JSON_MEDIA = "application/json";
|
|
1266
|
+
var mediaTypeOf = (req) => {
|
|
1267
|
+
const header = req.headers.get("content-type");
|
|
1268
|
+
if (header === JSON_MEDIA || header === null)
|
|
1269
|
+
return JSON_MEDIA;
|
|
1270
|
+
const end = header.indexOf(";");
|
|
1271
|
+
const media = (end === -1 ? header : header.slice(0, end)).trim();
|
|
1272
|
+
return media === "" ? JSON_MEDIA : media.toLowerCase();
|
|
1273
|
+
};
|
|
1274
|
+
var flatten = (issue) => {
|
|
1275
|
+
const path = issue.path?.map((segment) => String(typeof segment === "object" ? segment.key : segment)).join(".");
|
|
1276
|
+
return path === undefined || path === "" ? { message: issue.message } : { message: issue.message, path };
|
|
1277
|
+
};
|
|
1278
|
+
var accept = (source, result) => {
|
|
1279
|
+
if (result.issues !== undefined) {
|
|
1280
|
+
throw new ValidationError(source, result.issues.map(flatten));
|
|
1281
|
+
}
|
|
1282
|
+
return result.value;
|
|
1283
|
+
};
|
|
1284
|
+
var fillWith = (draft, source, schema, value) => {
|
|
1285
|
+
const result = schema["~standard"].validate(value);
|
|
1286
|
+
if (result instanceof Promise) {
|
|
1287
|
+
return result.then((settled) => {
|
|
1288
|
+
draft[source] = accept(source, settled);
|
|
1289
|
+
return draft;
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
draft[source] = accept(source, result);
|
|
1293
|
+
return draft;
|
|
1294
|
+
};
|
|
1295
|
+
var bodyFill = (schema) => (draft) => {
|
|
1296
|
+
const media = mediaTypeOf(draft.req);
|
|
1297
|
+
const parse = parserFor(media);
|
|
1298
|
+
if (parse === undefined) {
|
|
1299
|
+
throw new HttpError(HttpStatusCode2.UNSUPPORTED_MEDIA_TYPE, `Unsupported content type "${media}". Declared bodies accept ` + "application/json, application/x-www-form-urlencoded, multipart/form-data or text/*.");
|
|
1300
|
+
}
|
|
1301
|
+
const read = parse === asJson && RawBody.wanted(draft.req) ? draft.req.text().then((text) => {
|
|
1302
|
+
RawBody.record(draft.req, text);
|
|
1303
|
+
return JSON.parse(text);
|
|
1304
|
+
}) : parse(draft.req);
|
|
1305
|
+
return read.then((value) => fillWith(draft, "body", schema, value), (error) => {
|
|
1306
|
+
throw new HttpError(HttpStatusCode2.BAD_REQUEST, `Malformed ${media} body`, { cause: error });
|
|
1307
|
+
});
|
|
1308
|
+
};
|
|
1309
|
+
var searchOf = (url) => {
|
|
1310
|
+
const start = url.indexOf("?");
|
|
1311
|
+
if (start === -1)
|
|
1312
|
+
return "";
|
|
1313
|
+
const end = url.indexOf("#", start + 1);
|
|
1314
|
+
return end === -1 ? url.slice(start + 1) : url.slice(start + 1, end);
|
|
1315
|
+
};
|
|
1316
|
+
var queryFill = (schema) => (draft) => {
|
|
1317
|
+
const params = new URLSearchParams(searchOf(draft.req.url));
|
|
1318
|
+
return fillWith(draft, "query", schema, grouped(params));
|
|
1319
|
+
};
|
|
1320
|
+
var paramsFill = (schema) => (draft) => fillWith(draft, "params", schema, draft.req.params);
|
|
1321
|
+
var then = (first, second) => (draft) => {
|
|
1322
|
+
const started = first(draft);
|
|
1323
|
+
return started instanceof Promise ? started.then(second) : second(started);
|
|
1324
|
+
};
|
|
1325
|
+
var buildInputReader = (options) => {
|
|
1326
|
+
const fills = [];
|
|
1327
|
+
if (options?.body !== undefined)
|
|
1328
|
+
fills.push(bodyFill(options.body));
|
|
1329
|
+
if (options?.query !== undefined)
|
|
1330
|
+
fills.push(queryFill(options.query));
|
|
1331
|
+
if (options?.params !== undefined)
|
|
1332
|
+
fills.push(paramsFill(options.params));
|
|
1333
|
+
if (fills.length === 0)
|
|
1334
|
+
return (req) => ({ req });
|
|
1335
|
+
const fill = fills.reduce(then);
|
|
1336
|
+
return (req) => fill({ req });
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
// src/server/middleware.ts
|
|
1340
|
+
var compose = (middleware, ctx, handler) => middleware.reduceRight((next, current) => (req) => current.handle(req, ctx, () => next(req)), handler);
|
|
1341
|
+
|
|
1342
|
+
// src/server/routes.ts
|
|
1343
|
+
var construct = (guard) => new guard;
|
|
1344
|
+
var toResponse = (value, status) => {
|
|
1345
|
+
if (value instanceof Response)
|
|
1346
|
+
return value;
|
|
1347
|
+
if (value === undefined || value === null) {
|
|
1348
|
+
return new Response(null, { status: HttpStatusCode2.NO_CONTENT });
|
|
1349
|
+
}
|
|
1350
|
+
return Response.json(value, { status });
|
|
1351
|
+
};
|
|
1352
|
+
var statusFor = (route) => route.options?.status ?? defaultStatusFor2(route.method);
|
|
1353
|
+
var assertNoCollisions = (discovered) => {
|
|
1354
|
+
const owners = new Map;
|
|
1355
|
+
for (const route of discovered) {
|
|
1356
|
+
const key = `${route.method} ${route.path}`;
|
|
1357
|
+
const owner = `${route.controller}.${route.handlerName}`;
|
|
1358
|
+
const existing = owners.get(key);
|
|
1359
|
+
if (existing !== undefined) {
|
|
1360
|
+
throw new AppError6(`Route collision: ${key} is declared by ${existing} and by ${owner}. ` + "Bun would keep only one of them.");
|
|
1361
|
+
}
|
|
1362
|
+
owners.set(key, owner);
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
var assertNoGatewayCollisions = (discovered, gatewayPaths) => {
|
|
1366
|
+
const gateways = new Set(gatewayPaths);
|
|
1367
|
+
for (const route of discovered) {
|
|
1368
|
+
if (gateways.has(route.path)) {
|
|
1369
|
+
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.");
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
var withUpgradeRoutes = (routes, gateways) => {
|
|
1374
|
+
const merged = { ...routes };
|
|
1375
|
+
for (const [path, upgrade] of gateways)
|
|
1376
|
+
merged[path] = { GET: upgrade };
|
|
1377
|
+
return merged;
|
|
1378
|
+
};
|
|
1379
|
+
var unmatchedContext = (req, isPublic) => Object.freeze({
|
|
1380
|
+
controller: "(unmatched)",
|
|
1381
|
+
handler: "(none)",
|
|
1382
|
+
method: req.method,
|
|
1383
|
+
path: new URL(req.url).pathname,
|
|
1384
|
+
parsesBody: false,
|
|
1385
|
+
get: (key) => {
|
|
1386
|
+
if (key.id === UNMATCHED2.id)
|
|
1387
|
+
return true;
|
|
1388
|
+
if (key.id === PUBLIC2.id && isPublic)
|
|
1389
|
+
return true;
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
var buildFallback = (middleware = [], onError = defaultErrorMapper, cors, notFound = "guarded") => {
|
|
1394
|
+
const miss = () => {
|
|
1395
|
+
throw new HttpError(HttpStatusCode2.NOT_FOUND, "NOT_FOUND");
|
|
1396
|
+
};
|
|
1397
|
+
const run = async (req) => {
|
|
1398
|
+
try {
|
|
1399
|
+
return await compose(middleware, unmatchedContext(req, notFound === "public"), miss)(req);
|
|
1400
|
+
} catch (error) {
|
|
1401
|
+
return TraceContext2.stamp(onError(error, req), req);
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
return cors ? withCors(cors, run) : run;
|
|
1405
|
+
};
|
|
1406
|
+
var directOr = (guarded, route, read, status, onError, noMiddleware) => {
|
|
1407
|
+
if (!noMiddleware)
|
|
1408
|
+
return guarded;
|
|
1409
|
+
const settle = (value, req) => {
|
|
1410
|
+
try {
|
|
1411
|
+
return toResponse(value, status);
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
return onError(error, req);
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
1416
|
+
const invoke = (input, req) => {
|
|
1417
|
+
try {
|
|
1418
|
+
const value = route.handler(input);
|
|
1419
|
+
return value instanceof Promise ? value.then((resolved) => settle(resolved, req), (error) => onError(error, req)) : settle(value, req);
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
return onError(error, req);
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
return (req) => {
|
|
1425
|
+
try {
|
|
1426
|
+
const input = read(req);
|
|
1427
|
+
return input instanceof Promise ? input.then((resolved) => invoke(resolved, req), (error) => onError(error, req)) : invoke(input, req);
|
|
1428
|
+
} catch (error) {
|
|
1429
|
+
return onError(error, req);
|
|
1430
|
+
}
|
|
1431
|
+
};
|
|
1432
|
+
};
|
|
1433
|
+
var buildRoutes = (discovered, middleware = [], onError = defaultErrorMapper, cors, resolve = construct) => {
|
|
1434
|
+
assertNoCollisions(discovered);
|
|
1435
|
+
const routes = {};
|
|
1436
|
+
const instances = new Map;
|
|
1437
|
+
const guardOf = (guard, from) => {
|
|
1438
|
+
const existing = instances.get(guard);
|
|
1439
|
+
if (existing)
|
|
1440
|
+
return existing;
|
|
1441
|
+
const created = resolve(guard, from);
|
|
1442
|
+
instances.set(guard, created);
|
|
1443
|
+
return created;
|
|
1444
|
+
};
|
|
1445
|
+
for (const route of discovered) {
|
|
1446
|
+
const read = buildInputReader(route.options);
|
|
1447
|
+
const status = statusFor(route);
|
|
1448
|
+
const chain = [
|
|
1449
|
+
...middleware,
|
|
1450
|
+
...(route.moduleMiddleware ?? []).map((entry) => guardOf(entry, route.module)),
|
|
1451
|
+
...(route.guards ?? []).map((guard) => guardOf(guard, route.module))
|
|
1452
|
+
];
|
|
1453
|
+
const chained = compose(chain, buildContext2(route), async (req) => toResponse(await route.handler(await read(req)), status));
|
|
1454
|
+
const guarded = async (req) => {
|
|
1455
|
+
try {
|
|
1456
|
+
return await chained(req);
|
|
1457
|
+
} catch (error) {
|
|
1458
|
+
return TraceContext2.stamp(onError(error, req), req);
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1461
|
+
const byMethod = routes[route.path] ??= {};
|
|
1462
|
+
byMethod[route.method] = cors ? withCors(cors, guarded) : directOr(guarded, route, read, status, onError, chain.length === 0);
|
|
1463
|
+
}
|
|
1464
|
+
if (cors) {
|
|
1465
|
+
for (const byMethod of Object.values(routes)) {
|
|
1466
|
+
byMethod.OPTIONS = preflight(cors, Object.keys(byMethod));
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return routes;
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
// src/server/binding.ts
|
|
1473
|
+
class ServerBinding {
|
|
1474
|
+
#protocols;
|
|
1475
|
+
#main;
|
|
1476
|
+
#gateways;
|
|
1477
|
+
constructor(protocols) {
|
|
1478
|
+
this.#protocols = protocols;
|
|
1479
|
+
}
|
|
1480
|
+
bind(plan) {
|
|
1481
|
+
const { websocket: ws } = plan;
|
|
1482
|
+
const { http2, http1, gatewayPort } = this.#protocols;
|
|
1483
|
+
const split = ws !== undefined && gatewayPort !== undefined;
|
|
1484
|
+
const table = ws && !split ? {
|
|
1485
|
+
routes: withUpgradeRoutes(plan.routes, ws.routes),
|
|
1486
|
+
websocket: ws.websocket
|
|
1487
|
+
} : { routes: plan.routes };
|
|
1488
|
+
this.#main = Bun.serve({
|
|
1489
|
+
port: plan.port,
|
|
1490
|
+
fetch: plan.fetch,
|
|
1491
|
+
...http2 !== undefined && { http2 },
|
|
1492
|
+
...http1 !== undefined && { http1 },
|
|
1493
|
+
...table
|
|
1494
|
+
});
|
|
1495
|
+
if (split) {
|
|
1496
|
+
try {
|
|
1497
|
+
this.#gateways = Bun.serve({
|
|
1498
|
+
port: gatewayPort,
|
|
1499
|
+
fetch: plan.fetch,
|
|
1500
|
+
routes: withUpgradeRoutes({}, ws.routes),
|
|
1501
|
+
websocket: ws.websocket
|
|
1502
|
+
});
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
this.#main.stop(true);
|
|
1505
|
+
this.#main = undefined;
|
|
1506
|
+
throw error;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
const main = this.#main;
|
|
1510
|
+
const gateways = this.#gateways;
|
|
1511
|
+
return {
|
|
1512
|
+
main,
|
|
1513
|
+
sockets: gateways ?? main,
|
|
1514
|
+
gauges: {
|
|
1515
|
+
get pendingRequests() {
|
|
1516
|
+
return main.pendingRequests + (gateways?.pendingRequests ?? 0);
|
|
1517
|
+
},
|
|
1518
|
+
get pendingWebSockets() {
|
|
1519
|
+
return main.pendingWebSockets + (gateways?.pendingWebSockets ?? 0);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
get gatewayUrl() {
|
|
1525
|
+
return this.#gateways?.url.href;
|
|
1526
|
+
}
|
|
1527
|
+
async stop(force) {
|
|
1528
|
+
const main = this.#main;
|
|
1529
|
+
const gateways = this.#gateways;
|
|
1530
|
+
this.#main = undefined;
|
|
1531
|
+
this.#gateways = undefined;
|
|
1532
|
+
await Promise.all([
|
|
1533
|
+
main?.stop(gateways === undefined && force),
|
|
1534
|
+
gateways?.stop(force)
|
|
1535
|
+
]);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
Object.defineProperty(ServerBinding, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "protocols: BindingProtocols" }] });
|
|
1539
|
+
|
|
674
1540
|
// src/server/settings.ts
|
|
675
1541
|
var defaultSettings = () => ({ "trust proxy": false });
|
|
676
1542
|
|
|
677
1543
|
// src/server/application.ts
|
|
678
|
-
class HttpApplication {
|
|
1544
|
+
class HttpApplication extends ShutdownAware {
|
|
679
1545
|
warnings;
|
|
680
1546
|
#root;
|
|
681
1547
|
closed;
|
|
@@ -692,14 +1558,15 @@ class HttpApplication {
|
|
|
692
1558
|
#relayResubscribe;
|
|
693
1559
|
#notFound;
|
|
694
1560
|
#bootLogging;
|
|
1561
|
+
#binding;
|
|
1562
|
+
#split;
|
|
695
1563
|
#globalPrefix = "";
|
|
696
1564
|
#cors;
|
|
697
1565
|
#started = false;
|
|
698
|
-
#server;
|
|
699
1566
|
#resolveClosed;
|
|
700
1567
|
#shuttingDown;
|
|
701
|
-
#hooks = new ShutdownHooks;
|
|
702
1568
|
constructor(app, discovered, options, root, websocket) {
|
|
1569
|
+
super();
|
|
703
1570
|
this.#app = app;
|
|
704
1571
|
this.#root = root;
|
|
705
1572
|
this.warnings = app.warnings;
|
|
@@ -717,6 +1584,12 @@ class HttpApplication {
|
|
|
717
1584
|
this.#relayResubscribe = options.relayResubscribe;
|
|
718
1585
|
this.#notFound = options.notFound ?? "public";
|
|
719
1586
|
this.#bootLogging = options.bootLogging ?? true;
|
|
1587
|
+
this.#binding = new ServerBinding({
|
|
1588
|
+
http2: options.http2,
|
|
1589
|
+
http1: options.http1,
|
|
1590
|
+
gatewayPort: options.gatewayPort
|
|
1591
|
+
});
|
|
1592
|
+
this.#split = options.gatewayPort !== undefined && websocket !== undefined;
|
|
720
1593
|
this.gatewayPaths = websocket?.paths ?? [];
|
|
721
1594
|
this.closed = new Promise((resolve) => {
|
|
722
1595
|
this.#resolveClosed = resolve;
|
|
@@ -775,23 +1648,17 @@ class HttpApplication {
|
|
|
775
1648
|
const prefixed = this.#prefixed();
|
|
776
1649
|
const routes = buildRoutes(prefixed, middleware, this.#onError, this.#cors, (guard, from) => from === undefined ? this.#app.get(guard) : this.#app.get(guard, from));
|
|
777
1650
|
const ws = this.#websocket;
|
|
778
|
-
if (ws)
|
|
1651
|
+
if (ws && !this.#split)
|
|
779
1652
|
assertNoGatewayCollisions(prefixed, ws.paths);
|
|
780
1653
|
const fetch = buildFallback(middleware, this.#onError, this.#cors, this.#notFound);
|
|
781
|
-
const
|
|
782
|
-
port,
|
|
783
|
-
fetch,
|
|
784
|
-
routes: withUpgradeRoutes(routes, ws.routes),
|
|
785
|
-
websocket: ws.websocket
|
|
786
|
-
} : { port, fetch, routes };
|
|
787
|
-
this.#server = Bun.serve(options);
|
|
1654
|
+
const bound = this.#binding.bind({ port, routes, fetch, websocket: ws });
|
|
788
1655
|
attachAddressSource(this.#app.get(ClientAddress), {
|
|
789
|
-
server:
|
|
1656
|
+
server: bound.main,
|
|
790
1657
|
trustProxy: this.#settings["trust proxy"]
|
|
791
1658
|
});
|
|
792
|
-
this.#app.get(RequestMetrics).attach(
|
|
1659
|
+
this.#app.get(RequestMetrics).attach(bound.gauges);
|
|
793
1660
|
const pubsub = this.#app.get(PubSub);
|
|
794
|
-
pubsub.attach(
|
|
1661
|
+
pubsub.attach(bound.sockets);
|
|
795
1662
|
if (this.#relay) {
|
|
796
1663
|
const logger = this.#app.get(Logger3);
|
|
797
1664
|
await pubsub.relayThrough(this.#relay, {
|
|
@@ -807,7 +1674,10 @@ class HttpApplication {
|
|
|
807
1674
|
});
|
|
808
1675
|
}
|
|
809
1676
|
this.#logServed(prefixed, ws);
|
|
810
|
-
return
|
|
1677
|
+
return bound.main.url.href;
|
|
1678
|
+
}
|
|
1679
|
+
get gatewayUrl() {
|
|
1680
|
+
return this.#binding.gatewayUrl;
|
|
811
1681
|
}
|
|
812
1682
|
#logServed(routes, ws) {
|
|
813
1683
|
if (!this.#bootLogging)
|
|
@@ -819,6 +1689,7 @@ class HttpApplication {
|
|
|
819
1689
|
].join(" and ");
|
|
820
1690
|
this.#app.get(Logger3).info(`Serving ${subject}`, {
|
|
821
1691
|
...runtimeInfo(),
|
|
1692
|
+
...this.#binding.gatewayUrl === undefined ? {} : { gatewayUrl: this.#binding.gatewayUrl },
|
|
822
1693
|
routes: routes.map((route) => `${route.method} ${route.path}`),
|
|
823
1694
|
...gateways.length === 0 ? {} : {
|
|
824
1695
|
gateways: gateways.map((gateway) => ({
|
|
@@ -843,8 +1714,7 @@ class HttpApplication {
|
|
|
843
1714
|
}
|
|
844
1715
|
};
|
|
845
1716
|
await step(() => this.#app.drain());
|
|
846
|
-
await step(
|
|
847
|
-
this.#server = undefined;
|
|
1717
|
+
await step(() => this.#binding.stop(this.#websocket !== undefined));
|
|
848
1718
|
await step(() => this.#app.get(PubSub).close());
|
|
849
1719
|
try {
|
|
850
1720
|
await step(() => this.#app.shutdown());
|
|
@@ -856,25 +1726,21 @@ class HttpApplication {
|
|
|
856
1726
|
})();
|
|
857
1727
|
return this.#shuttingDown;
|
|
858
1728
|
}
|
|
859
|
-
enableShutdownHooks(signals = ["SIGTERM", "SIGINT"], options = {}) {
|
|
860
|
-
this.#hooks.install(() => this.shutdown(), signals, options);
|
|
861
|
-
return this;
|
|
862
|
-
}
|
|
863
1729
|
#prefixed() {
|
|
864
1730
|
if (this.#globalPrefix === "")
|
|
865
1731
|
return this.#discovered;
|
|
866
1732
|
return this.#discovered.map((route) => ({
|
|
867
1733
|
...route,
|
|
868
|
-
path:
|
|
1734
|
+
path: joinPath2(this.#globalPrefix, route.path)
|
|
869
1735
|
}));
|
|
870
1736
|
}
|
|
871
1737
|
#assertNotStarted(hook) {
|
|
872
1738
|
if (!this.#started)
|
|
873
1739
|
return;
|
|
874
|
-
throw new
|
|
1740
|
+
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.");
|
|
875
1741
|
}
|
|
876
1742
|
}
|
|
877
|
-
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" }] });
|
|
1743
|
+
Object.defineProperty(HttpApplication, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "app: App", typeOnly: "App" }, { unresolved: "discovered: readonly DiscoveredRoute[]" }, { unresolved: "options: HttpOptions", typeOnly: "HttpOptions" }, { unresolved: "root: ModuleRef", typeOnly: "ModuleRef" }, { unresolved: "websocket?: WebSocketRuntime", typeOnly: "WebSocketRuntime" }] });
|
|
878
1744
|
|
|
879
1745
|
// src/server/options-provider.ts
|
|
880
1746
|
class HttpOptionsProvider {
|
|
@@ -915,6 +1781,15 @@ class HttpOptionsProvider {
|
|
|
915
1781
|
get relayResubscribe() {
|
|
916
1782
|
return;
|
|
917
1783
|
}
|
|
1784
|
+
get http2() {
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
get http1() {
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
get gatewayPort() {
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
918
1793
|
}
|
|
919
1794
|
|
|
920
1795
|
class DefaultHttpOptions extends HttpOptionsProvider {
|
|
@@ -937,7 +1812,10 @@ function resolveHttpOptions(settings, given) {
|
|
|
937
1812
|
onError: settings.onError,
|
|
938
1813
|
websocket: settings.websocket,
|
|
939
1814
|
relay: settings.relay,
|
|
940
|
-
relayResubscribe: settings.relayResubscribe
|
|
1815
|
+
relayResubscribe: settings.relayResubscribe,
|
|
1816
|
+
http2: settings.http2,
|
|
1817
|
+
http1: settings.http1,
|
|
1818
|
+
gatewayPort: settings.gatewayPort
|
|
941
1819
|
};
|
|
942
1820
|
for (const key of Object.keys(given)) {
|
|
943
1821
|
merged[key] = given[key];
|
|
@@ -991,9 +1869,9 @@ class HttpFactory {
|
|
|
991
1869
|
for (const module of modules) {
|
|
992
1870
|
const moduleMiddleware = module.options.middleware ?? [];
|
|
993
1871
|
for (const controller of readControllers(module)) {
|
|
994
|
-
const routes =
|
|
1872
|
+
const routes = discoverRoutes2(app.get(controller, module.ref));
|
|
995
1873
|
if (routes.length === 0) {
|
|
996
|
-
throw new
|
|
1874
|
+
throw new AppError8(`${controller.name} is registered as a controller but declares no routes. ` + "Add a @Get/@Post/... method, or move it to providers.");
|
|
997
1875
|
}
|
|
998
1876
|
discovered.push(...routes.map((route) => ({
|
|
999
1877
|
...route,
|
|
@@ -1010,6 +1888,15 @@ class HttpFactory {
|
|
|
1010
1888
|
for (const warning of websocket?.warnings ?? []) {
|
|
1011
1889
|
app.get(Logger4).warn(warning);
|
|
1012
1890
|
}
|
|
1891
|
+
if (resolved.gatewayPort !== undefined && resolved.gatewayPort !== 0 && websocket === undefined) {
|
|
1892
|
+
app.get(Logger4).warn(`gatewayPort is set and no gateway is declared, so no second server ` + `binds and gatewayUrl stays undefined. Declare a @Gateway in a ` + `module's providers, or drop gatewayPort.`);
|
|
1893
|
+
}
|
|
1894
|
+
if (resolved.http1 === false && websocket !== undefined && resolved.gatewayPort === undefined) {
|
|
1895
|
+
await app.shutdown().catch(() => {
|
|
1896
|
+
return;
|
|
1897
|
+
});
|
|
1898
|
+
throw new AppError8(`http1: false refuses HTTP/1.x with a 505, and a websocket upgrade is an ` + `HTTP/1.1 request - so nothing could ever connect to ` + `${websocket.paths.join(", ")}. Set gatewayPort to serve the gateways ` + "from a second port that keeps HTTP/1.1, or drop http1: false.");
|
|
1899
|
+
}
|
|
1013
1900
|
return new HttpApplication(app, discovered, resolved, root, websocket);
|
|
1014
1901
|
}
|
|
1015
1902
|
static #socketMiddleware(app, root, options) {
|
|
@@ -1020,6 +1907,27 @@ class HttpFactory {
|
|
|
1020
1907
|
}
|
|
1021
1908
|
// src/static/files.ts
|
|
1022
1909
|
import { join, normalize, resolve } from "path";
|
|
1910
|
+
|
|
1911
|
+
// src/static/options.ts
|
|
1912
|
+
class StaticOptions {
|
|
1913
|
+
root;
|
|
1914
|
+
path;
|
|
1915
|
+
maxAge;
|
|
1916
|
+
immutable;
|
|
1917
|
+
constructor(init) {
|
|
1918
|
+
this.root = init.root;
|
|
1919
|
+
this.path = normalizePrefix(init.path ?? "/");
|
|
1920
|
+
this.maxAge = init.maxAge ?? 60;
|
|
1921
|
+
this.immutable = init.immutable ?? (() => false);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
Object.defineProperty(StaticOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: StaticOptionsInit" }] });
|
|
1925
|
+
var normalizePrefix = (path) => {
|
|
1926
|
+
const trimmed = path.split("/").filter(Boolean).join("/");
|
|
1927
|
+
return trimmed === "" ? "/" : `/${trimmed}`;
|
|
1928
|
+
};
|
|
1929
|
+
|
|
1930
|
+
// src/static/files.ts
|
|
1023
1931
|
class StaticFiles {
|
|
1024
1932
|
#options;
|
|
1025
1933
|
#root;
|
|
@@ -1116,6 +2024,99 @@ StaticModule = __decorateElement(_init, 0, "StaticModule", _dec, StaticModule);
|
|
|
1116
2024
|
__runInitializers(_init, 1, StaticModule);
|
|
1117
2025
|
__decoratorMetadata(_init, StaticModule);
|
|
1118
2026
|
let _StaticModule = StaticModule;
|
|
2027
|
+
// src/compression/negotiate.ts
|
|
2028
|
+
var quality = (params) => {
|
|
2029
|
+
for (const param of params) {
|
|
2030
|
+
const [key, value] = param.split("=");
|
|
2031
|
+
if (key?.trim().toLowerCase() !== "q")
|
|
2032
|
+
continue;
|
|
2033
|
+
const q = Number.parseFloat(value ?? "");
|
|
2034
|
+
return Number.isFinite(q) && q >= 0 && q <= 1 ? q : 1;
|
|
2035
|
+
}
|
|
2036
|
+
return 1;
|
|
2037
|
+
};
|
|
2038
|
+
var negotiate = (header, offered) => {
|
|
2039
|
+
if (header === null)
|
|
2040
|
+
return;
|
|
2041
|
+
const accepted = new Map;
|
|
2042
|
+
for (const element of header.split(",")) {
|
|
2043
|
+
const [name, ...params] = element.split(";");
|
|
2044
|
+
const token = name?.trim().toLowerCase();
|
|
2045
|
+
if (token === undefined || token === "")
|
|
2046
|
+
continue;
|
|
2047
|
+
accepted.set(token, quality(params));
|
|
2048
|
+
}
|
|
2049
|
+
const wildcard = accepted.get("*");
|
|
2050
|
+
let best;
|
|
2051
|
+
let bestQuality = 0;
|
|
2052
|
+
for (const encoding of offered) {
|
|
2053
|
+
const q = accepted.get(encoding) ?? wildcard ?? 0;
|
|
2054
|
+
if (q > bestQuality) {
|
|
2055
|
+
best = encoding;
|
|
2056
|
+
bestQuality = q;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
return best;
|
|
2060
|
+
};
|
|
2061
|
+
|
|
2062
|
+
// src/compression/options.ts
|
|
2063
|
+
var CompressionEncoding = Object.freeze({
|
|
2064
|
+
ZSTD: "zstd",
|
|
2065
|
+
GZIP: "gzip"
|
|
2066
|
+
});
|
|
2067
|
+
var COMPRESSIBLE = new Set([
|
|
2068
|
+
"application/graphql",
|
|
2069
|
+
"application/graphql-response+json",
|
|
2070
|
+
"application/javascript",
|
|
2071
|
+
"application/json",
|
|
2072
|
+
"application/manifest+json",
|
|
2073
|
+
"application/wasm",
|
|
2074
|
+
"application/x-javascript",
|
|
2075
|
+
"application/x-ndjson",
|
|
2076
|
+
"application/xml",
|
|
2077
|
+
"image/svg+xml"
|
|
2078
|
+
]);
|
|
2079
|
+
var isCompressibleType = (contentType) => {
|
|
2080
|
+
if (contentType === null)
|
|
2081
|
+
return false;
|
|
2082
|
+
const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
2083
|
+
if (type.startsWith("text/"))
|
|
2084
|
+
return true;
|
|
2085
|
+
if (type.endsWith("+json") || type.endsWith("+xml"))
|
|
2086
|
+
return true;
|
|
2087
|
+
return COMPRESSIBLE.has(type);
|
|
2088
|
+
};
|
|
2089
|
+
var encodable = (encoding) => {
|
|
2090
|
+
const sync = encoding === CompressionEncoding.ZSTD ? Bun.zstdCompressSync : Bun.gzipSync;
|
|
2091
|
+
if (typeof sync !== "function")
|
|
2092
|
+
return false;
|
|
2093
|
+
try {
|
|
2094
|
+
new CompressionStream(encoding);
|
|
2095
|
+
return true;
|
|
2096
|
+
} catch {
|
|
2097
|
+
return false;
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
|
|
2101
|
+
class CompressionOptions {
|
|
2102
|
+
encodings;
|
|
2103
|
+
threshold;
|
|
2104
|
+
filter;
|
|
2105
|
+
constructor(init = {}) {
|
|
2106
|
+
this.encodings = init.encodings ?? [
|
|
2107
|
+
CompressionEncoding.ZSTD,
|
|
2108
|
+
CompressionEncoding.GZIP
|
|
2109
|
+
];
|
|
2110
|
+
const missing = this.encodings.filter((encoding) => !encodable(encoding));
|
|
2111
|
+
if (missing.length > 0) {
|
|
2112
|
+
throw new Error(`Bun ${Bun.version} cannot encode ${missing.join(", ")}. ` + "Pass `encodings` without it, or upgrade Bun.");
|
|
2113
|
+
}
|
|
2114
|
+
this.threshold = init.threshold ?? 1024;
|
|
2115
|
+
this.filter = init.filter ?? isCompressibleType;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
Object.defineProperty(CompressionOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: CompressionOptionsInit = {}", optional: true }] });
|
|
2119
|
+
|
|
1119
2120
|
// src/compression/compression.ts
|
|
1120
2121
|
var BODYLESS = new Set([204, 205, 304]);
|
|
1121
2122
|
var BUFFER_LIMIT = 1024 * 1024;
|
|
@@ -1288,15 +2289,15 @@ __runInitializers(_init, 1, CompressionModule);
|
|
|
1288
2289
|
__decoratorMetadata(_init, CompressionModule);
|
|
1289
2290
|
let _CompressionModule = CompressionModule;
|
|
1290
2291
|
// src/throttle/decorators.ts
|
|
1291
|
-
var THROTTLE =
|
|
1292
|
-
var SKIP_THROTTLE =
|
|
1293
|
-
var Throttle = (limit) =>
|
|
1294
|
-
var SkipThrottle = () =>
|
|
2292
|
+
var THROTTLE = metaKey2("throttle");
|
|
2293
|
+
var SKIP_THROTTLE = metaKey2("skip-throttle");
|
|
2294
|
+
var Throttle = (limit) => meta2(THROTTLE, limit);
|
|
2295
|
+
var SkipThrottle = () => meta2(SKIP_THROTTLE, true);
|
|
1295
2296
|
// src/throttle/guard.ts
|
|
1296
2297
|
import { Logger as Logger5 } from "@dunx/core";
|
|
1297
2298
|
|
|
1298
2299
|
// src/throttle/options.ts
|
|
1299
|
-
import { AppError as
|
|
2300
|
+
import { AppError as AppError9 } from "@dunx/core";
|
|
1300
2301
|
|
|
1301
2302
|
class ThrottleOptions {
|
|
1302
2303
|
limit;
|
|
@@ -1307,13 +2308,13 @@ class ThrottleOptions {
|
|
|
1307
2308
|
store;
|
|
1308
2309
|
constructor(init) {
|
|
1309
2310
|
if (init.prefix.trim() === "") {
|
|
1310
|
-
throw new
|
|
2311
|
+
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' }.");
|
|
1311
2312
|
}
|
|
1312
2313
|
if (!Number.isInteger(init.limit) || init.limit < 1) {
|
|
1313
|
-
throw new
|
|
2314
|
+
throw new AppError9(`ThrottleModule needs a limit of at least 1; got ${init.limit}.`);
|
|
1314
2315
|
}
|
|
1315
2316
|
if (!Number.isInteger(init.windowSeconds) || init.windowSeconds < 1) {
|
|
1316
|
-
throw new
|
|
2317
|
+
throw new AppError9("ThrottleModule needs a windowSeconds of at least 1; got " + `${init.windowSeconds}.`);
|
|
1317
2318
|
}
|
|
1318
2319
|
this.limit = init.limit;
|
|
1319
2320
|
this.windowSeconds = init.windowSeconds;
|
|
@@ -1326,12 +2327,12 @@ class ThrottleOptions {
|
|
|
1326
2327
|
Object.defineProperty(ThrottleOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: ThrottleOptionsInit" }] });
|
|
1327
2328
|
|
|
1328
2329
|
// src/throttle/store.ts
|
|
1329
|
-
import { AppError as
|
|
2330
|
+
import { AppError as AppError10 } from "@dunx/core";
|
|
1330
2331
|
|
|
1331
2332
|
class ThrottleStore {
|
|
1332
2333
|
constructor() {
|
|
1333
2334
|
if (new.target === ThrottleStore) {
|
|
1334
|
-
throw new
|
|
2335
|
+
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.");
|
|
1335
2336
|
}
|
|
1336
2337
|
}
|
|
1337
2338
|
}
|
|
@@ -1406,7 +2407,7 @@ class ThrottleGuard {
|
|
|
1406
2407
|
this.logger = logger;
|
|
1407
2408
|
}
|
|
1408
2409
|
async handle(req, ctx, next) {
|
|
1409
|
-
if (ctx.get(
|
|
2410
|
+
if (ctx.get(UNMATCHED2) === true)
|
|
1410
2411
|
return next();
|
|
1411
2412
|
if (ctx.get(SKIP_THROTTLE) === true)
|
|
1412
2413
|
return next();
|
|
@@ -1417,7 +2418,7 @@ class ThrottleGuard {
|
|
|
1417
2418
|
return next();
|
|
1418
2419
|
if (used > limit.limit) {
|
|
1419
2420
|
const after = await this.#ttl(key) ?? limit.windowSeconds;
|
|
1420
|
-
throw new HttpError(
|
|
2421
|
+
throw new HttpError(HttpStatusCode2.TOO_MANY_REQUESTS, `Rate limit exceeded: ${limit.limit} requests per ` + `${limit.windowSeconds}s`, this.options.headers ? {
|
|
1421
2422
|
headers: {
|
|
1422
2423
|
"retry-after": String(after),
|
|
1423
2424
|
"ratelimit-limit": String(limit.limit),
|
|
@@ -1535,21 +2536,8 @@ var OnMessage = (event) => (value) => {
|
|
|
1535
2536
|
return value;
|
|
1536
2537
|
};
|
|
1537
2538
|
// src/ws/postgres-relay.ts
|
|
1538
|
-
import { AppError as AppError7 } from "@dunx/core";
|
|
1539
2539
|
var PROTOCOLS = ["postgres:", "postgresql:"];
|
|
1540
2540
|
var defaultPostgresRelayUrl = () => process.env["POSTGRES_URL"] ?? process.env["DATABASE_URL"] ?? "postgres://localhost:5432";
|
|
1541
|
-
var assertUrl = (url) => {
|
|
1542
|
-
let parsed;
|
|
1543
|
-
try {
|
|
1544
|
-
parsed = new URL(url);
|
|
1545
|
-
} catch {
|
|
1546
|
-
throw new AppError7(`${JSON.stringify(url)} is not a valid URL for the websocket relay. ` + "Expected something like postgres://localhost:5432/app.");
|
|
1547
|
-
}
|
|
1548
|
-
if (!PROTOCOLS.includes(parsed.protocol)) {
|
|
1549
|
-
throw new AppError7(`Unsupported protocol ${JSON.stringify(parsed.protocol)} in ` + `${JSON.stringify(url)}. Expected one of ${PROTOCOLS.join(", ")}.`);
|
|
1550
|
-
}
|
|
1551
|
-
return url;
|
|
1552
|
-
};
|
|
1553
2541
|
|
|
1554
2542
|
class PostgresRelay extends WsRelay {
|
|
1555
2543
|
#url;
|
|
@@ -1558,14 +2546,11 @@ class PostgresRelay extends WsRelay {
|
|
|
1558
2546
|
#subscription;
|
|
1559
2547
|
constructor(options = {}) {
|
|
1560
2548
|
super();
|
|
1561
|
-
this.#url =
|
|
2549
|
+
this.#url = assertRelayUrl(options.url ?? defaultPostgresRelayUrl(), PROTOCOLS, "postgres://localhost:5432/app");
|
|
1562
2550
|
this.#max = options.max ?? 1;
|
|
1563
2551
|
}
|
|
1564
2552
|
get url() {
|
|
1565
|
-
|
|
1566
|
-
if (parsed.password)
|
|
1567
|
-
parsed.password = "***";
|
|
1568
|
-
return parsed.toString();
|
|
2553
|
+
return redactUrl(this.#url);
|
|
1569
2554
|
}
|
|
1570
2555
|
#client() {
|
|
1571
2556
|
return this.#sql ??= new Bun.SQL({ url: this.#url, max: this.#max });
|
|
@@ -1599,6 +2584,82 @@ class PostgresRelay extends WsRelay {
|
|
|
1599
2584
|
}
|
|
1600
2585
|
}
|
|
1601
2586
|
Object.defineProperty(PostgresRelay, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "options: PostgresRelayOptions = {}", optional: true }] });
|
|
2587
|
+
// src/ws/redis-relay.ts
|
|
2588
|
+
var PROTOCOLS2 = [
|
|
2589
|
+
"redis:",
|
|
2590
|
+
"rediss:",
|
|
2591
|
+
"valkey:",
|
|
2592
|
+
"valkeys:",
|
|
2593
|
+
"redis+tls:",
|
|
2594
|
+
"redis+unix:",
|
|
2595
|
+
"redis+tls+unix:"
|
|
2596
|
+
];
|
|
2597
|
+
var defaultRelayUrl = () => process.env["VALKEY_URL"] ?? process.env["REDIS_URL"] ?? "redis://localhost:6379";
|
|
2598
|
+
|
|
2599
|
+
class RedisRelay extends WsRelay {
|
|
2600
|
+
#url;
|
|
2601
|
+
#options;
|
|
2602
|
+
#pub;
|
|
2603
|
+
#sub;
|
|
2604
|
+
#channel;
|
|
2605
|
+
constructor(options = {}) {
|
|
2606
|
+
super();
|
|
2607
|
+
this.#url = assertRelayUrl(options.url ?? defaultRelayUrl(), PROTOCOLS2, "redis://localhost:6379");
|
|
2608
|
+
this.#options = {
|
|
2609
|
+
maxRetries: options.maxRetries ?? 0,
|
|
2610
|
+
...options.connectionTimeout !== undefined && {
|
|
2611
|
+
connectionTimeout: options.connectionTimeout
|
|
2612
|
+
},
|
|
2613
|
+
...options.tls !== undefined && { tls: options.tls }
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
get url() {
|
|
2617
|
+
return redactUrl(this.#url);
|
|
2618
|
+
}
|
|
2619
|
+
async publish(channel, message) {
|
|
2620
|
+
const client = this.#pub ??= new Bun.RedisClient(this.#url, this.#options);
|
|
2621
|
+
try {
|
|
2622
|
+
return await client.publish(channel, message);
|
|
2623
|
+
} catch (error) {
|
|
2624
|
+
if (this.#pub === client) {
|
|
2625
|
+
this.#pub = undefined;
|
|
2626
|
+
client.close();
|
|
2627
|
+
}
|
|
2628
|
+
throw error;
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
async subscribe(channel, listener) {
|
|
2632
|
+
const client = this.#sub ??= new Bun.RedisClient(this.#url, this.#options);
|
|
2633
|
+
try {
|
|
2634
|
+
await client.connect();
|
|
2635
|
+
await client.subscribe(channel, listener);
|
|
2636
|
+
this.#channel = channel;
|
|
2637
|
+
} catch (error) {
|
|
2638
|
+
if (this.#sub === client) {
|
|
2639
|
+
this.#sub = undefined;
|
|
2640
|
+
client.close();
|
|
2641
|
+
}
|
|
2642
|
+
throw error;
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
async close() {
|
|
2646
|
+
const sub = this.#sub;
|
|
2647
|
+
const channel = this.#channel;
|
|
2648
|
+
this.#pub?.close();
|
|
2649
|
+
this.#pub = undefined;
|
|
2650
|
+
this.#sub = undefined;
|
|
2651
|
+
this.#channel = undefined;
|
|
2652
|
+
if (!sub)
|
|
2653
|
+
return;
|
|
2654
|
+
if (channel !== undefined) {
|
|
2655
|
+
try {
|
|
2656
|
+
await sub.unsubscribe(channel);
|
|
2657
|
+
} catch {}
|
|
2658
|
+
}
|
|
2659
|
+
sub.close();
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
Object.defineProperty(RedisRelay, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "options: RedisRelayOptions = {}", optional: true }] });
|
|
1602
2663
|
// src/ws/relay-module.ts
|
|
1603
2664
|
import {
|
|
1604
2665
|
provide as provide5
|
|
@@ -1740,6 +2801,201 @@ class PingProbe {
|
|
|
1740
2801
|
|
|
1741
2802
|
class QueryProbe {
|
|
1742
2803
|
}
|
|
2804
|
+
// src/health/controller.ts
|
|
2805
|
+
import { inject } from "@dunx/core";
|
|
2806
|
+
|
|
2807
|
+
// src/health/report-schema.ts
|
|
2808
|
+
var state = {
|
|
2809
|
+
type: "string",
|
|
2810
|
+
enum: ["up", "down", "unknown"],
|
|
2811
|
+
description: "`unknown` is not `down`: a probe that timed out has told you nothing."
|
|
2812
|
+
};
|
|
2813
|
+
var HEALTH_REPORT_SCHEMA = Object.freeze({
|
|
2814
|
+
$id: "HealthReport",
|
|
2815
|
+
type: "object",
|
|
2816
|
+
description: "What the probe found. `up` answers 200 and anything else answers 503.",
|
|
2817
|
+
properties: {
|
|
2818
|
+
status: state,
|
|
2819
|
+
draining: {
|
|
2820
|
+
type: "boolean",
|
|
2821
|
+
description: "The process is shutting down, or something holds it out."
|
|
2822
|
+
},
|
|
2823
|
+
uptimeMs: {
|
|
2824
|
+
type: "integer",
|
|
2825
|
+
description: "Measured on a monotonic clock, so it never goes backwards."
|
|
2826
|
+
},
|
|
2827
|
+
checks: {
|
|
2828
|
+
type: "array",
|
|
2829
|
+
items: {
|
|
2830
|
+
type: "object",
|
|
2831
|
+
properties: {
|
|
2832
|
+
name: { type: "string" },
|
|
2833
|
+
state,
|
|
2834
|
+
critical: {
|
|
2835
|
+
type: "boolean",
|
|
2836
|
+
description: "A failure here sheds traffic. Memory and disk do not."
|
|
2837
|
+
},
|
|
2838
|
+
ms: { type: "integer", description: "How long the check took." },
|
|
2839
|
+
detail: {
|
|
2840
|
+
type: "string",
|
|
2841
|
+
description: "A latency, a version, or a failure message."
|
|
2842
|
+
}
|
|
2843
|
+
},
|
|
2844
|
+
required: ["name", "state", "critical", "ms"]
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
},
|
|
2848
|
+
required: ["status", "draining", "uptimeMs", "checks"]
|
|
2849
|
+
});
|
|
2850
|
+
|
|
2851
|
+
// src/health/registry.ts
|
|
2852
|
+
var bounded = async (indicator, timeoutMs) => {
|
|
2853
|
+
let timer;
|
|
2854
|
+
const timeout = new Promise((resolve) => {
|
|
2855
|
+
timer = setTimeout(() => resolve({ state: "unknown", detail: `no answer in ${timeoutMs} ms` }), timeoutMs);
|
|
2856
|
+
timer.unref?.();
|
|
2857
|
+
});
|
|
2858
|
+
try {
|
|
2859
|
+
return await Promise.race([
|
|
2860
|
+
Promise.resolve().then(() => indicator.check()).catch((error) => ({
|
|
2861
|
+
state: "down",
|
|
2862
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
2863
|
+
})),
|
|
2864
|
+
timeout
|
|
2865
|
+
]);
|
|
2866
|
+
} finally {
|
|
2867
|
+
if (timer)
|
|
2868
|
+
clearTimeout(timer);
|
|
2869
|
+
}
|
|
2870
|
+
};
|
|
2871
|
+
var worst = (checks) => {
|
|
2872
|
+
const critical = checks.filter((check) => check.critical);
|
|
2873
|
+
if (critical.some((check) => check.state === "down"))
|
|
2874
|
+
return "down";
|
|
2875
|
+
if (critical.some((check) => check.state === "unknown"))
|
|
2876
|
+
return "unknown";
|
|
2877
|
+
return "up";
|
|
2878
|
+
};
|
|
2879
|
+
|
|
2880
|
+
class HealthOptions {
|
|
2881
|
+
liveness;
|
|
2882
|
+
readiness;
|
|
2883
|
+
timeoutMs;
|
|
2884
|
+
routes;
|
|
2885
|
+
documented;
|
|
2886
|
+
drainDelayMs;
|
|
2887
|
+
constructor(init = {}) {
|
|
2888
|
+
this.liveness = init.liveness ?? [];
|
|
2889
|
+
this.readiness = init.readiness ?? [];
|
|
2890
|
+
this.timeoutMs = init.timeoutMs ?? 2000;
|
|
2891
|
+
this.routes = init.routes ?? true;
|
|
2892
|
+
this.documented = init.documented ?? true;
|
|
2893
|
+
this.drainDelayMs = Math.max(0, init.drainDelayMs ?? 0);
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
Object.defineProperty(HealthOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: HealthOptionsInit = {}", optional: true }] });
|
|
2897
|
+
|
|
2898
|
+
class HealthRegistry {
|
|
2899
|
+
options;
|
|
2900
|
+
readiness_;
|
|
2901
|
+
#startedAt = performance.now();
|
|
2902
|
+
constructor(options, readiness_) {
|
|
2903
|
+
this.options = options;
|
|
2904
|
+
this.readiness_ = readiness_;
|
|
2905
|
+
}
|
|
2906
|
+
async report(indicators) {
|
|
2907
|
+
const checks = await Promise.all(indicators.map(async (indicator) => {
|
|
2908
|
+
const started = performance.now();
|
|
2909
|
+
const result = await bounded(indicator, this.options.timeoutMs);
|
|
2910
|
+
return {
|
|
2911
|
+
name: indicator.name,
|
|
2912
|
+
state: result.state,
|
|
2913
|
+
critical: indicator.critical,
|
|
2914
|
+
ms: Math.round(performance.now() - started),
|
|
2915
|
+
...result.detail === undefined ? {} : { detail: result.detail }
|
|
2916
|
+
};
|
|
2917
|
+
}));
|
|
2918
|
+
return {
|
|
2919
|
+
status: worst(checks),
|
|
2920
|
+
draining: this.readiness_.draining,
|
|
2921
|
+
uptimeMs: Math.round(performance.now() - this.#startedAt),
|
|
2922
|
+
checks
|
|
2923
|
+
};
|
|
2924
|
+
}
|
|
2925
|
+
liveness() {
|
|
2926
|
+
return this.report(this.options.liveness);
|
|
2927
|
+
}
|
|
2928
|
+
async readiness() {
|
|
2929
|
+
const report = await this.report(this.options.readiness);
|
|
2930
|
+
if (!this.readiness_.draining)
|
|
2931
|
+
return report;
|
|
2932
|
+
return {
|
|
2933
|
+
...report,
|
|
2934
|
+
status: "down",
|
|
2935
|
+
checks: [
|
|
2936
|
+
{
|
|
2937
|
+
name: "readiness",
|
|
2938
|
+
state: "down",
|
|
2939
|
+
critical: true,
|
|
2940
|
+
ms: 0,
|
|
2941
|
+
detail: this.readiness_.reason ?? "not accepting traffic"
|
|
2942
|
+
},
|
|
2943
|
+
...report.checks
|
|
2944
|
+
]
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
Object.defineProperty(HealthRegistry, Symbol.for("dunx.deps"), { value: () => [HealthOptions, { unresolved: "private readonly readiness_: Readiness", typeOnly: "Readiness" }] });
|
|
2949
|
+
|
|
2950
|
+
// src/health/controller.ts
|
|
2951
|
+
var probeResponses = {
|
|
2952
|
+
response: { 200: HEALTH_REPORT_SCHEMA, 503: HEALTH_REPORT_SCHEMA }
|
|
2953
|
+
};
|
|
2954
|
+
var answer = (report) => Response.json(report, { status: report.status === "up" ? 200 : 503 });
|
|
2955
|
+
var _dec = [
|
|
2956
|
+
Controller("health")
|
|
2957
|
+
];
|
|
2958
|
+
var _dec2 = [
|
|
2959
|
+
Public2(),
|
|
2960
|
+
Get("/live", probeResponses)
|
|
2961
|
+
];
|
|
2962
|
+
var _dec3 = [
|
|
2963
|
+
Public2(),
|
|
2964
|
+
Get("/ready", probeResponses)
|
|
2965
|
+
];
|
|
2966
|
+
var _health = new WeakMap;
|
|
2967
|
+
var _init = __decoratorStart(undefined);
|
|
2968
|
+
|
|
2969
|
+
class HealthController {
|
|
2970
|
+
constructor() {
|
|
2971
|
+
__privateAdd(this, _health, inject(HealthRegistry));
|
|
2972
|
+
__runInitializers(_init, 5, this);
|
|
2973
|
+
}
|
|
2974
|
+
async live() {
|
|
2975
|
+
return answer(await __privateGet(this, _health).liveness());
|
|
2976
|
+
}
|
|
2977
|
+
async ready() {
|
|
2978
|
+
return answer(await __privateGet(this, _health).readiness());
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
__decorateElement(_init, 1, "live", _dec2, HealthController);
|
|
2982
|
+
__decorateElement(_init, 1, "ready", _dec3, HealthController);
|
|
2983
|
+
HealthController = __decorateElement(_init, 0, "HealthController", _dec, HealthController);
|
|
2984
|
+
__runInitializers(_init, 1, HealthController);
|
|
2985
|
+
__decoratorMetadata(_init, HealthController);
|
|
2986
|
+
let _HealthController = HealthController;
|
|
2987
|
+
var _dec = [
|
|
2988
|
+
ApiHidden2()
|
|
2989
|
+
];
|
|
2990
|
+
var _base = HealthController;
|
|
2991
|
+
var _init = __decoratorStart(_base);
|
|
2992
|
+
|
|
2993
|
+
class HiddenHealthController extends _base {
|
|
2994
|
+
}
|
|
2995
|
+
HiddenHealthController = __decorateElement(_init, 0, "HiddenHealthController", _dec, HiddenHealthController);
|
|
2996
|
+
__runInitializers(_init, 1, HiddenHealthController);
|
|
2997
|
+
__decoratorMetadata(_init, HiddenHealthController);
|
|
2998
|
+
let _HiddenHealthController = HiddenHealthController;
|
|
1743
2999
|
// src/health/indicators.ts
|
|
1744
3000
|
import { statfs } from "fs/promises";
|
|
1745
3001
|
var ms = (started) => Math.round(performance.now() - started);
|
|
@@ -1926,7 +3182,7 @@ __runInitializers(_init, 1, HealthModule);
|
|
|
1926
3182
|
__decoratorMetadata(_init, HealthModule);
|
|
1927
3183
|
let _HealthModule = HealthModule;
|
|
1928
3184
|
export {
|
|
1929
|
-
ApiHidden,
|
|
3185
|
+
ApiHidden2 as ApiHidden,
|
|
1930
3186
|
ClientAddress,
|
|
1931
3187
|
Compression,
|
|
1932
3188
|
CompressionEncoding,
|
|
@@ -1943,7 +3199,7 @@ export {
|
|
|
1943
3199
|
Gateway,
|
|
1944
3200
|
Get,
|
|
1945
3201
|
HEALTH_REPORT_SCHEMA,
|
|
1946
|
-
HIDDEN,
|
|
3202
|
+
HIDDEN2 as HIDDEN,
|
|
1947
3203
|
HealthController,
|
|
1948
3204
|
HealthIndicator,
|
|
1949
3205
|
HealthModule,
|
|
@@ -1952,7 +3208,7 @@ export {
|
|
|
1952
3208
|
HttpError,
|
|
1953
3209
|
HttpFactory,
|
|
1954
3210
|
HttpOptionsProvider,
|
|
1955
|
-
HttpStatusCode,
|
|
3211
|
+
HttpStatusCode2 as HttpStatusCode,
|
|
1956
3212
|
MemoryIndicator,
|
|
1957
3213
|
MemoryOptions,
|
|
1958
3214
|
MemoryThrottleStore,
|
|
@@ -1964,17 +3220,17 @@ export {
|
|
|
1964
3220
|
OnPing,
|
|
1965
3221
|
OnPong,
|
|
1966
3222
|
OnUpgrade,
|
|
1967
|
-
PUBLIC,
|
|
3223
|
+
PUBLIC2 as PUBLIC,
|
|
1968
3224
|
Patch,
|
|
1969
3225
|
PingProbe,
|
|
1970
3226
|
Post,
|
|
1971
3227
|
PostgresRelay,
|
|
1972
3228
|
PostgresRelayConnectionOptions,
|
|
1973
3229
|
PubSub,
|
|
1974
|
-
Public,
|
|
3230
|
+
Public2 as Public,
|
|
1975
3231
|
Put,
|
|
1976
3232
|
QueryProbe,
|
|
1977
|
-
ROLES,
|
|
3233
|
+
ROLES2 as ROLES,
|
|
1978
3234
|
Readiness,
|
|
1979
3235
|
ReadinessOptions,
|
|
1980
3236
|
RedisIndicator,
|
|
@@ -1983,7 +3239,7 @@ export {
|
|
|
1983
3239
|
RelayConnectionOptions,
|
|
1984
3240
|
RequestLoggingMiddleware,
|
|
1985
3241
|
RequestMetrics,
|
|
1986
|
-
Roles,
|
|
3242
|
+
Roles2 as Roles,
|
|
1987
3243
|
SKIP_THROTTLE,
|
|
1988
3244
|
SkipThrottle,
|
|
1989
3245
|
SocketLoggingMiddleware,
|
|
@@ -1991,25 +3247,25 @@ export {
|
|
|
1991
3247
|
StaticModule,
|
|
1992
3248
|
StaticOptions,
|
|
1993
3249
|
THROTTLE,
|
|
1994
|
-
TRACEPARENT_HEADER,
|
|
1995
|
-
TRACERESPONSE_HEADER,
|
|
1996
|
-
TRACESTATE_HEADER,
|
|
3250
|
+
TRACEPARENT_HEADER2 as TRACEPARENT_HEADER,
|
|
3251
|
+
TRACERESPONSE_HEADER2 as TRACERESPONSE_HEADER,
|
|
3252
|
+
TRACESTATE_HEADER2 as TRACESTATE_HEADER,
|
|
1997
3253
|
Throttle,
|
|
1998
3254
|
ThrottleGuard,
|
|
1999
3255
|
ThrottleModule,
|
|
2000
3256
|
ThrottleOptions,
|
|
2001
3257
|
ThrottleStore,
|
|
2002
|
-
TraceContext,
|
|
2003
|
-
UNMATCHED,
|
|
3258
|
+
TraceContext2 as TraceContext,
|
|
3259
|
+
UNMATCHED2 as UNMATCHED,
|
|
2004
3260
|
UNMATCHED_ROUTE,
|
|
2005
|
-
UseGuards,
|
|
3261
|
+
UseGuards2 as UseGuards,
|
|
2006
3262
|
ValidationError,
|
|
2007
3263
|
WsRelay,
|
|
2008
3264
|
WsRelayModule,
|
|
2009
3265
|
defaultErrorMapper,
|
|
2010
3266
|
errorMapper,
|
|
2011
|
-
mergeMeta,
|
|
2012
|
-
meta,
|
|
2013
|
-
metaKey,
|
|
2014
|
-
metaOf
|
|
3267
|
+
mergeMeta2 as mergeMeta,
|
|
3268
|
+
meta2 as meta,
|
|
3269
|
+
metaKey2 as metaKey,
|
|
3270
|
+
metaOf2 as metaOf
|
|
2015
3271
|
};
|