@dunx/http 0.1.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/LICENSE +201 -0
- package/README.md +779 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +1427 -0
- package/dist/index.js.map +34 -0
- package/dist/route/decorators.d.ts +9 -0
- package/dist/route/discover.d.ts +26 -0
- package/dist/route/marker.d.ts +12 -0
- package/dist/route/metadata.d.ts +36 -0
- package/dist/route/schema.d.ts +87 -0
- package/dist/server/application.d.ts +84 -0
- package/dist/server/client-address.d.ts +15 -0
- package/dist/server/context.d.ts +21 -0
- package/dist/server/cors.d.ts +26 -0
- package/dist/server/errors.d.ts +25 -0
- package/dist/server/factory.d.ts +12 -0
- package/dist/server/input.d.ts +17 -0
- package/dist/server/middleware.d.ts +20 -0
- package/dist/server/request-logging.d.ts +55 -0
- package/dist/server/routes.d.ts +47 -0
- package/dist/server/settings.d.ts +14 -0
- package/dist/server/status.d.ts +43 -0
- package/dist/ws/adapter.d.ts +21 -0
- package/dist/ws/decorators.d.ts +17 -0
- package/dist/ws/discover.d.ts +40 -0
- package/dist/ws/envelope.d.ts +16 -0
- package/dist/ws/marker.d.ts +27 -0
- package/dist/ws/pubsub.d.ts +52 -0
- package/dist/ws/redis-relay.d.ts +52 -0
- package/dist/ws/relay.d.ts +83 -0
- package/dist/ws/runtime.d.ts +25 -0
- package/dist/ws/socket.d.ts +28 -0
- package/package.json +65 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1427 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/route/marker.ts
|
|
3
|
+
var ROUTE = Symbol.for("dunx.route");
|
|
4
|
+
var CONTROLLER = Symbol.for("dunx.controller");
|
|
5
|
+
var markRoute = (target, meta) => {
|
|
6
|
+
Object.defineProperty(target, ROUTE, { value: meta, configurable: true });
|
|
7
|
+
};
|
|
8
|
+
var routeMetaOf = (value) => typeof value === "function" ? value[ROUTE] : undefined;
|
|
9
|
+
var markController = (target, prefix) => {
|
|
10
|
+
Object.defineProperty(target, CONTROLLER, {
|
|
11
|
+
value: prefix,
|
|
12
|
+
configurable: true
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
var prefixOf = (target) => target[CONTROLLER] ?? "";
|
|
16
|
+
|
|
17
|
+
// src/route/decorators.ts
|
|
18
|
+
var Controller = (prefix = "") => (target) => {
|
|
19
|
+
markController(target, prefix);
|
|
20
|
+
return target;
|
|
21
|
+
};
|
|
22
|
+
var verb = (method) => (path = "/", options) => (value, _context) => {
|
|
23
|
+
markRoute(value, { method, path, options });
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
var Get = verb("GET");
|
|
27
|
+
var Post = verb("POST");
|
|
28
|
+
var Put = verb("PUT");
|
|
29
|
+
var Patch = verb("PATCH");
|
|
30
|
+
var Delete = verb("DELETE");
|
|
31
|
+
// src/route/metadata.ts
|
|
32
|
+
var META = Symbol.for("dunx.meta");
|
|
33
|
+
var GUARDS = Symbol.for("dunx.guards");
|
|
34
|
+
var metaKey = (name) => ({
|
|
35
|
+
name,
|
|
36
|
+
id: Symbol(name)
|
|
37
|
+
});
|
|
38
|
+
var write = (target, key, value) => {
|
|
39
|
+
const record = new Map(target[META]);
|
|
40
|
+
record.set(key.id, value);
|
|
41
|
+
Object.defineProperty(target, META, { value: record, configurable: true });
|
|
42
|
+
};
|
|
43
|
+
var meta = (key, value) => (target) => {
|
|
44
|
+
write(target, key, value);
|
|
45
|
+
return target;
|
|
46
|
+
};
|
|
47
|
+
var ROLES = metaKey("roles");
|
|
48
|
+
var PUBLIC = metaKey("public");
|
|
49
|
+
var Roles = (...roles) => meta(ROLES, roles);
|
|
50
|
+
var Public = () => meta(PUBLIC, true);
|
|
51
|
+
var UseGuards = (...guards) => (target) => {
|
|
52
|
+
const existing = target[GUARDS] ?? [];
|
|
53
|
+
const merged = Object.hasOwn(target, GUARDS) ? [...guards, ...existing] : [...existing, ...guards];
|
|
54
|
+
Object.defineProperty(target, GUARDS, {
|
|
55
|
+
value: merged,
|
|
56
|
+
configurable: true
|
|
57
|
+
});
|
|
58
|
+
return target;
|
|
59
|
+
};
|
|
60
|
+
var guardsOf = (target) => target[GUARDS] ?? [];
|
|
61
|
+
var metaOf = (target) => target[META];
|
|
62
|
+
var mergeMeta = (...targets) => {
|
|
63
|
+
const merged = new Map;
|
|
64
|
+
for (const target of targets) {
|
|
65
|
+
const record = target[META];
|
|
66
|
+
if (record)
|
|
67
|
+
for (const [id, value] of record)
|
|
68
|
+
merged.set(id, value);
|
|
69
|
+
}
|
|
70
|
+
return merged;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/route/discover.ts
|
|
74
|
+
var joinPath = (prefix, path) => {
|
|
75
|
+
const joined = `/${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
76
|
+
return joined.length > 1 ? joined.replace(/\/$/, "") : "/";
|
|
77
|
+
};
|
|
78
|
+
var discoverRoutes = (instance) => {
|
|
79
|
+
const klass = instance.constructor;
|
|
80
|
+
const prefix = prefixOf(klass);
|
|
81
|
+
const classGuards = guardsOf(klass);
|
|
82
|
+
const members = instance;
|
|
83
|
+
const routes = [];
|
|
84
|
+
const seen = new Set;
|
|
85
|
+
for (let proto = Object.getPrototypeOf(instance);proto !== null && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) {
|
|
86
|
+
for (const [name, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(proto))) {
|
|
87
|
+
if (name === "constructor" || seen.has(name))
|
|
88
|
+
continue;
|
|
89
|
+
const meta2 = routeMetaOf(descriptor.value);
|
|
90
|
+
if (!meta2)
|
|
91
|
+
continue;
|
|
92
|
+
seen.add(name);
|
|
93
|
+
const marked = descriptor.value;
|
|
94
|
+
routes.push({
|
|
95
|
+
method: meta2.method,
|
|
96
|
+
path: joinPath(prefix, meta2.path),
|
|
97
|
+
controller: klass.name,
|
|
98
|
+
handlerName: name,
|
|
99
|
+
handler: members[name].bind(instance),
|
|
100
|
+
options: meta2.options,
|
|
101
|
+
meta: mergeMeta(klass, marked),
|
|
102
|
+
guards: [...classGuards, ...guardsOf(marked)]
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return routes;
|
|
107
|
+
};
|
|
108
|
+
// src/server/client-address.ts
|
|
109
|
+
import { AppError } from "@dunx/core";
|
|
110
|
+
var sources = new WeakMap;
|
|
111
|
+
|
|
112
|
+
class ClientAddress {
|
|
113
|
+
of(req) {
|
|
114
|
+
const source = sources.get(this);
|
|
115
|
+
if (!source) {
|
|
116
|
+
throw new AppError("ClientAddress has no server yet. The address comes from the live Bun " + "server, so it is only available once listen() has run.");
|
|
117
|
+
}
|
|
118
|
+
if (source.trustProxy) {
|
|
119
|
+
const forwarded = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
|
|
120
|
+
if (forwarded)
|
|
121
|
+
return forwarded;
|
|
122
|
+
}
|
|
123
|
+
return source.server.requestIP(req)?.address;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
var attachAddressSource = (target, source) => {
|
|
127
|
+
sources.set(target, source);
|
|
128
|
+
};
|
|
129
|
+
// src/server/context.ts
|
|
130
|
+
var EMPTY = new Map;
|
|
131
|
+
var buildContext = (route) => {
|
|
132
|
+
const record = route.meta ?? EMPTY;
|
|
133
|
+
return Object.freeze({
|
|
134
|
+
controller: route.controller,
|
|
135
|
+
handler: route.handlerName,
|
|
136
|
+
method: route.method,
|
|
137
|
+
path: route.path,
|
|
138
|
+
get: (key) => record.get(key.id)
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
// src/server/status.ts
|
|
142
|
+
var HttpStatusCode = Object.freeze({
|
|
143
|
+
OK: 200,
|
|
144
|
+
CREATED: 201,
|
|
145
|
+
ACCEPTED: 202,
|
|
146
|
+
NO_CONTENT: 204,
|
|
147
|
+
MOVED_PERMANENTLY: 301,
|
|
148
|
+
FOUND: 302,
|
|
149
|
+
NOT_MODIFIED: 304,
|
|
150
|
+
TEMPORARY_REDIRECT: 307,
|
|
151
|
+
PERMANENT_REDIRECT: 308,
|
|
152
|
+
BAD_REQUEST: 400,
|
|
153
|
+
UNAUTHORIZED: 401,
|
|
154
|
+
PAYMENT_REQUIRED: 402,
|
|
155
|
+
FORBIDDEN: 403,
|
|
156
|
+
NOT_FOUND: 404,
|
|
157
|
+
METHOD_NOT_ALLOWED: 405,
|
|
158
|
+
NOT_ACCEPTABLE: 406,
|
|
159
|
+
REQUEST_TIMEOUT: 408,
|
|
160
|
+
CONFLICT: 409,
|
|
161
|
+
GONE: 410,
|
|
162
|
+
PRECONDITION_FAILED: 412,
|
|
163
|
+
PAYLOAD_TOO_LARGE: 413,
|
|
164
|
+
URI_TOO_LONG: 414,
|
|
165
|
+
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
166
|
+
IM_A_TEAPOT: 418,
|
|
167
|
+
UNPROCESSABLE_ENTITY: 422,
|
|
168
|
+
TOO_MANY_REQUESTS: 429,
|
|
169
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
170
|
+
NOT_IMPLEMENTED: 501,
|
|
171
|
+
BAD_GATEWAY: 502,
|
|
172
|
+
SERVICE_UNAVAILABLE: 503,
|
|
173
|
+
GATEWAY_TIMEOUT: 504
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// src/server/cors.ts
|
|
177
|
+
var ORIGIN = "access-control-allow-origin";
|
|
178
|
+
var allowedOrigin = (options, requested) => {
|
|
179
|
+
const origin = options.origin ?? "*";
|
|
180
|
+
if (typeof origin === "string") {
|
|
181
|
+
if (origin !== "*")
|
|
182
|
+
return origin === requested ? origin : undefined;
|
|
183
|
+
if (!options.credentials)
|
|
184
|
+
return "*";
|
|
185
|
+
return requested ?? undefined;
|
|
186
|
+
}
|
|
187
|
+
if (requested === null)
|
|
188
|
+
return;
|
|
189
|
+
const allowed = typeof origin === "function" ? origin(requested) : origin.includes(requested);
|
|
190
|
+
return allowed ? requested : undefined;
|
|
191
|
+
};
|
|
192
|
+
var applyCors = (options, req, response) => {
|
|
193
|
+
const origin = allowedOrigin(options, req.headers.get("origin"));
|
|
194
|
+
if (origin === undefined)
|
|
195
|
+
return response;
|
|
196
|
+
response.headers.set(ORIGIN, origin);
|
|
197
|
+
if (origin !== "*")
|
|
198
|
+
response.headers.append("vary", "Origin");
|
|
199
|
+
if (options.credentials) {
|
|
200
|
+
response.headers.set("access-control-allow-credentials", "true");
|
|
201
|
+
}
|
|
202
|
+
if (options.exposedHeaders?.length) {
|
|
203
|
+
response.headers.set("access-control-expose-headers", options.exposedHeaders.join(", "));
|
|
204
|
+
}
|
|
205
|
+
return response;
|
|
206
|
+
};
|
|
207
|
+
var withCors = (options, handler) => {
|
|
208
|
+
return async (req) => applyCors(options, req, await handler(req));
|
|
209
|
+
};
|
|
210
|
+
var preflight = (options, methods) => {
|
|
211
|
+
const allowMethods = (options.methods ?? methods).join(", ");
|
|
212
|
+
return async (req) => {
|
|
213
|
+
const response = applyCors(options, req, new Response(null, { status: HttpStatusCode.NO_CONTENT }));
|
|
214
|
+
if (!response.headers.has(ORIGIN))
|
|
215
|
+
return response;
|
|
216
|
+
response.headers.set("access-control-allow-methods", allowMethods);
|
|
217
|
+
const allowHeaders = options.allowedHeaders ?? (req.headers.get("access-control-request-headers") ?? "").split(",").map((header) => header.trim()).filter((header) => header.length > 0);
|
|
218
|
+
if (allowHeaders.length > 0) {
|
|
219
|
+
response.headers.set("access-control-allow-headers", allowHeaders.join(", "));
|
|
220
|
+
}
|
|
221
|
+
if (options.maxAge !== undefined) {
|
|
222
|
+
response.headers.set("access-control-max-age", String(options.maxAge));
|
|
223
|
+
}
|
|
224
|
+
return response;
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
// src/server/errors.ts
|
|
228
|
+
import { AppError as AppError2 } from "@dunx/core";
|
|
229
|
+
class HttpError extends AppError2 {
|
|
230
|
+
status;
|
|
231
|
+
name = "HttpError";
|
|
232
|
+
constructor(status, message, options) {
|
|
233
|
+
super(message, options);
|
|
234
|
+
this.status = status;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
Object.defineProperty(HttpError, Symbol.for("dunx.deps"), {
|
|
238
|
+
value: () => [{ unresolved: "readonly status: number" }, { unresolved: "message: string" }, ErrorOptions]
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
class ValidationError extends HttpError {
|
|
242
|
+
source;
|
|
243
|
+
issues;
|
|
244
|
+
name = "ValidationError";
|
|
245
|
+
constructor(source, issues) {
|
|
246
|
+
super(HttpStatusCode.BAD_REQUEST, `Invalid ${source}`);
|
|
247
|
+
this.source = source;
|
|
248
|
+
this.issues = issues;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
Object.defineProperty(ValidationError, Symbol.for("dunx.deps"), {
|
|
252
|
+
value: () => [{ unresolved: "readonly source: InputSource" }, { unresolved: "readonly issues: readonly ValidationIssue[]" }]
|
|
253
|
+
});
|
|
254
|
+
var defaultErrorMapper = (error) => {
|
|
255
|
+
if (error instanceof ValidationError) {
|
|
256
|
+
return Response.json({ error: error.message, status: error.status, issues: error.issues }, { status: error.status });
|
|
257
|
+
}
|
|
258
|
+
if (error instanceof HttpError) {
|
|
259
|
+
return Response.json({ error: error.message, status: error.status }, { status: error.status });
|
|
260
|
+
}
|
|
261
|
+
console.error(error);
|
|
262
|
+
return Response.json({
|
|
263
|
+
error: "Internal Server Error",
|
|
264
|
+
status: HttpStatusCode.INTERNAL_SERVER_ERROR
|
|
265
|
+
}, { status: HttpStatusCode.INTERNAL_SERVER_ERROR });
|
|
266
|
+
};
|
|
267
|
+
// src/server/factory.ts
|
|
268
|
+
import {
|
|
269
|
+
collectModules,
|
|
270
|
+
AppError as AppError8,
|
|
271
|
+
AppFactory,
|
|
272
|
+
Logger as Logger3,
|
|
273
|
+
provide,
|
|
274
|
+
readControllers,
|
|
275
|
+
RequestContext as RequestContext2
|
|
276
|
+
} from "@dunx/core";
|
|
277
|
+
|
|
278
|
+
// src/ws/envelope.ts
|
|
279
|
+
var encode = (event, data) => JSON.stringify({ event, data });
|
|
280
|
+
var decode = (message) => {
|
|
281
|
+
if (typeof message !== "string")
|
|
282
|
+
return;
|
|
283
|
+
let parsed;
|
|
284
|
+
try {
|
|
285
|
+
parsed = JSON.parse(message);
|
|
286
|
+
} catch {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
290
|
+
return;
|
|
291
|
+
const { event, data } = parsed;
|
|
292
|
+
return typeof event === "string" ? { event, data } : undefined;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// src/ws/runtime.ts
|
|
296
|
+
import { AppError as AppError3 } from "@dunx/core";
|
|
297
|
+
|
|
298
|
+
// src/ws/marker.ts
|
|
299
|
+
var HANDLER = Symbol.for("dunx.ws.handler");
|
|
300
|
+
var GATEWAY = Symbol.for("dunx.ws.gateway");
|
|
301
|
+
var HandlerKind = Object.freeze({
|
|
302
|
+
UPGRADE: "upgrade",
|
|
303
|
+
OPEN: "open",
|
|
304
|
+
MESSAGE: "message",
|
|
305
|
+
CLOSE: "close",
|
|
306
|
+
DRAIN: "drain",
|
|
307
|
+
PING: "ping",
|
|
308
|
+
PONG: "pong"
|
|
309
|
+
});
|
|
310
|
+
var markHandler = (target, meta2) => {
|
|
311
|
+
Object.defineProperty(target, HANDLER, { value: meta2, configurable: true });
|
|
312
|
+
};
|
|
313
|
+
var handlerMetaOf = (value) => typeof value === "function" ? value[HANDLER] : undefined;
|
|
314
|
+
var markGateway = (target, path) => {
|
|
315
|
+
Object.defineProperty(target, GATEWAY, { value: path, configurable: true });
|
|
316
|
+
};
|
|
317
|
+
var gatewayPathOf = (target) => target[GATEWAY] ?? "/";
|
|
318
|
+
var isGateway = (target) => target[GATEWAY] !== undefined;
|
|
319
|
+
|
|
320
|
+
// src/ws/runtime.ts
|
|
321
|
+
var slotOf = (handler) => handler.kind === HandlerKind.MESSAGE && handler.event !== undefined ? `message ${JSON.stringify(handler.event)}` : handler.kind;
|
|
322
|
+
var buildRuntime = (gateway) => {
|
|
323
|
+
if (gateway.handlers.length === 0) {
|
|
324
|
+
throw new AppError3(`${gateway.name} is registered as a gateway but declares no handlers. ` + "Add an @OnMessage/@OnOpen/... method, or drop the @Gateway decorator.");
|
|
325
|
+
}
|
|
326
|
+
const owners = new Map;
|
|
327
|
+
const events = new Map;
|
|
328
|
+
for (const handler of gateway.handlers) {
|
|
329
|
+
const slot = slotOf(handler);
|
|
330
|
+
const existing = owners.get(slot);
|
|
331
|
+
if (existing) {
|
|
332
|
+
throw new AppError3(`Handler collision in ${gateway.name}: ${slot} is claimed by ` + `${existing.method}() and by ${handler.method}(). One handler per event.`);
|
|
333
|
+
}
|
|
334
|
+
owners.set(slot, handler);
|
|
335
|
+
if (handler.kind === HandlerKind.MESSAGE && handler.event !== undefined) {
|
|
336
|
+
events.set(handler.event, handler.invoke);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const at = (slot) => owners.get(slot)?.invoke;
|
|
340
|
+
return {
|
|
341
|
+
name: gateway.name,
|
|
342
|
+
path: gateway.path,
|
|
343
|
+
upgrade: at(HandlerKind.UPGRADE),
|
|
344
|
+
open: at(HandlerKind.OPEN),
|
|
345
|
+
close: at(HandlerKind.CLOSE),
|
|
346
|
+
drain: at(HandlerKind.DRAIN),
|
|
347
|
+
ping: at(HandlerKind.PING),
|
|
348
|
+
pong: at(HandlerKind.PONG),
|
|
349
|
+
raw: at(HandlerKind.MESSAGE),
|
|
350
|
+
events
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
var buildGateways = (discovered) => {
|
|
354
|
+
const byPath = new Map;
|
|
355
|
+
for (const gateway of discovered) {
|
|
356
|
+
const existing = byPath.get(gateway.path);
|
|
357
|
+
if (existing) {
|
|
358
|
+
throw new AppError3(`Gateway path collision: ${gateway.path} is served by ${existing.name} ` + `and by ${gateway.name}. One gateway per path.`);
|
|
359
|
+
}
|
|
360
|
+
byPath.set(gateway.path, buildRuntime(gateway));
|
|
361
|
+
}
|
|
362
|
+
return byPath;
|
|
363
|
+
};
|
|
364
|
+
var someHandler = (gateways, pick) => {
|
|
365
|
+
for (const gateway of gateways)
|
|
366
|
+
if (pick(gateway) !== undefined)
|
|
367
|
+
return true;
|
|
368
|
+
return false;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// src/ws/adapter.ts
|
|
372
|
+
var RUNTIME = Symbol.for("dunx.ws.runtime");
|
|
373
|
+
var defaultOnError = (error, socket) => {
|
|
374
|
+
console.error(`[dunx/http] ${socket.data.path} handler failed:`, error);
|
|
375
|
+
};
|
|
376
|
+
var runtimeOf = (socket) => socket.data[RUNTIME];
|
|
377
|
+
var isBinary = (value) => value instanceof ArrayBuffer || ArrayBuffer.isView(value);
|
|
378
|
+
var replyRaw = (socket, value) => {
|
|
379
|
+
if (value === undefined)
|
|
380
|
+
return;
|
|
381
|
+
socket.send(typeof value === "string" || isBinary(value) ? value : JSON.stringify(value));
|
|
382
|
+
};
|
|
383
|
+
var settle = (result, socket, onError, then) => {
|
|
384
|
+
if (result instanceof Promise) {
|
|
385
|
+
result.then((value) => {
|
|
386
|
+
if (!then)
|
|
387
|
+
return;
|
|
388
|
+
try {
|
|
389
|
+
then(value);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
onError(error, socket);
|
|
392
|
+
}
|
|
393
|
+
}, (error) => onError(error, socket));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (then)
|
|
397
|
+
then(result);
|
|
398
|
+
};
|
|
399
|
+
var buildWebSocket = (discovered, options = {}) => {
|
|
400
|
+
const byPath = buildGateways(discovered);
|
|
401
|
+
const gateways = [...byPath.values()];
|
|
402
|
+
const onError = options.onError ?? defaultOnError;
|
|
403
|
+
const { onError: _onError, ...socketOptions } = options;
|
|
404
|
+
const run = (invoke, args, ws, then) => {
|
|
405
|
+
try {
|
|
406
|
+
settle(invoke(...args), ws, onError, then);
|
|
407
|
+
} catch (error) {
|
|
408
|
+
onError(error, ws);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
const websocket = {
|
|
412
|
+
...socketOptions,
|
|
413
|
+
message(ws, message) {
|
|
414
|
+
const gateway = runtimeOf(ws);
|
|
415
|
+
if (gateway.events.size > 0) {
|
|
416
|
+
const envelope = decode(message);
|
|
417
|
+
const handler = envelope && gateway.events.get(envelope.event);
|
|
418
|
+
if (envelope && handler) {
|
|
419
|
+
run(handler, [envelope.data, ws], ws, (value) => {
|
|
420
|
+
if (value !== undefined)
|
|
421
|
+
ws.send(encode(envelope.event, value));
|
|
422
|
+
});
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (gateway.raw) {
|
|
427
|
+
run(gateway.raw, [message, ws], ws, (value) => replyRaw(ws, value));
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
...someHandler(gateways, (g) => g.open) && {
|
|
431
|
+
open(ws) {
|
|
432
|
+
const { open } = runtimeOf(ws);
|
|
433
|
+
if (open)
|
|
434
|
+
run(open, [ws], ws, undefined);
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
...someHandler(gateways, (g) => g.close) && {
|
|
438
|
+
close(ws, code, reason) {
|
|
439
|
+
const { close } = runtimeOf(ws);
|
|
440
|
+
if (close)
|
|
441
|
+
run(close, [ws, code, reason], ws, undefined);
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
...someHandler(gateways, (g) => g.drain) && {
|
|
445
|
+
drain(ws) {
|
|
446
|
+
const { drain } = runtimeOf(ws);
|
|
447
|
+
if (drain)
|
|
448
|
+
run(drain, [ws], ws, undefined);
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
...someHandler(gateways, (g) => g.ping) && {
|
|
452
|
+
ping(ws, data) {
|
|
453
|
+
const { ping } = runtimeOf(ws);
|
|
454
|
+
if (ping)
|
|
455
|
+
run(ping, [data, ws], ws, undefined);
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
...someHandler(gateways, (g) => g.pong) && {
|
|
459
|
+
pong(ws, data) {
|
|
460
|
+
const { pong } = runtimeOf(ws);
|
|
461
|
+
if (pong)
|
|
462
|
+
run(pong, [data, ws], ws, undefined);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const accept = (req, server, gateway, context) => {
|
|
467
|
+
const data = { path: gateway.path, context, [RUNTIME]: gateway };
|
|
468
|
+
return server.upgrade(req, { data }) ? undefined : new Response("Expected a WebSocket upgrade", { status: 426 });
|
|
469
|
+
};
|
|
470
|
+
const upgradeHandler = (gateway) => (req, server) => {
|
|
471
|
+
if (!gateway.upgrade)
|
|
472
|
+
return accept(req, server, gateway, undefined);
|
|
473
|
+
const result = gateway.upgrade(req);
|
|
474
|
+
if (result instanceof Promise) {
|
|
475
|
+
return result.then((value) => value instanceof Response ? value : accept(req, server, gateway, value));
|
|
476
|
+
}
|
|
477
|
+
return result instanceof Response ? result : accept(req, server, gateway, result);
|
|
478
|
+
};
|
|
479
|
+
return {
|
|
480
|
+
websocket,
|
|
481
|
+
routes: new Map(gateways.map((gateway) => [gateway.path, upgradeHandler(gateway)])),
|
|
482
|
+
paths: [...byPath.keys()]
|
|
483
|
+
};
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// src/ws/discover.ts
|
|
487
|
+
import {
|
|
488
|
+
AppError as AppError4
|
|
489
|
+
} from "@dunx/core";
|
|
490
|
+
var normalizePath = (path) => {
|
|
491
|
+
const joined = `/${path}`.replace(/\/{2,}/g, "/");
|
|
492
|
+
return joined.length > 1 ? joined.replace(/\/$/, "") : "/";
|
|
493
|
+
};
|
|
494
|
+
var eachHandler = (start) => {
|
|
495
|
+
const found = [];
|
|
496
|
+
const seen = new Set;
|
|
497
|
+
for (let proto = start;proto !== null && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) {
|
|
498
|
+
for (const [name, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(proto))) {
|
|
499
|
+
if (name === "constructor" || seen.has(name))
|
|
500
|
+
continue;
|
|
501
|
+
const meta2 = handlerMetaOf(descriptor.value);
|
|
502
|
+
if (!meta2)
|
|
503
|
+
continue;
|
|
504
|
+
seen.add(name);
|
|
505
|
+
found.push([name, meta2]);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return found;
|
|
509
|
+
};
|
|
510
|
+
var discoverGateway = (instance) => {
|
|
511
|
+
const klass = instance.constructor;
|
|
512
|
+
const members = instance;
|
|
513
|
+
return {
|
|
514
|
+
name: klass.name,
|
|
515
|
+
path: normalizePath(gatewayPathOf(klass)),
|
|
516
|
+
handlers: eachHandler(Object.getPrototypeOf(instance)).map(([name, meta2]) => ({
|
|
517
|
+
kind: meta2.kind,
|
|
518
|
+
event: meta2.event,
|
|
519
|
+
method: name,
|
|
520
|
+
invoke: members[name].bind(instance)
|
|
521
|
+
}))
|
|
522
|
+
};
|
|
523
|
+
};
|
|
524
|
+
var findHandlerMethod = (ctor) => eachHandler(ctor.prototype)[0]?.[0];
|
|
525
|
+
var classOf = (entry) => {
|
|
526
|
+
if (typeof entry === "function")
|
|
527
|
+
return { token: entry, ctor: entry };
|
|
528
|
+
return entry.provider.kind === "class" ? { token: entry.token, ctor: entry.provider.ctor } : undefined;
|
|
529
|
+
};
|
|
530
|
+
var discoverGateways = (modules, resolve) => {
|
|
531
|
+
const discovered = [];
|
|
532
|
+
for (const module of modules) {
|
|
533
|
+
for (const entry of module.options.providers ?? []) {
|
|
534
|
+
const candidate = classOf(entry);
|
|
535
|
+
if (!candidate)
|
|
536
|
+
continue;
|
|
537
|
+
if (isGateway(candidate.ctor)) {
|
|
538
|
+
discovered.push(discoverGateway(resolve(candidate.token)));
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const orphan = findHandlerMethod(candidate.ctor);
|
|
542
|
+
if (orphan !== undefined) {
|
|
543
|
+
throw new AppError4(`${candidate.ctor.name}.${orphan}() is a websocket handler, but ` + `${candidate.ctor.name} is not a gateway. Decorate the class with ` + "@Gateway(path), or drop the handler decorator.");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return discovered;
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// src/ws/pubsub.ts
|
|
551
|
+
import { AppError as AppError5 } from "@dunx/core";
|
|
552
|
+
|
|
553
|
+
// src/ws/relay.ts
|
|
554
|
+
var DEFAULT_RELAY_CHANNEL = "dunx:ws";
|
|
555
|
+
var defaultRelayError = (error, phase) => {
|
|
556
|
+
console.warn(`[dunx/http] the websocket relay could not ${phase}. Fan-out is local to ` + "this process until it recovers:", error);
|
|
557
|
+
};
|
|
558
|
+
var toBytes = (data) => ArrayBuffer.isView(data) ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : new Uint8Array(data);
|
|
559
|
+
var encodeRelay = (origin, topic, data) => typeof data === "string" ? JSON.stringify({ o: origin, t: topic, d: data }) : JSON.stringify({
|
|
560
|
+
o: origin,
|
|
561
|
+
t: topic,
|
|
562
|
+
d: Buffer.from(toBytes(data)).toString("base64"),
|
|
563
|
+
b: 1
|
|
564
|
+
});
|
|
565
|
+
var decodeRelay = (message) => {
|
|
566
|
+
let parsed;
|
|
567
|
+
try {
|
|
568
|
+
parsed = JSON.parse(message);
|
|
569
|
+
} catch {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
573
|
+
return;
|
|
574
|
+
const { o, t, d, b } = parsed;
|
|
575
|
+
if (typeof o !== "string" || typeof t !== "string" || typeof d !== "string") {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
return { origin: o, topic: t, data: b ? Buffer.from(d, "base64") : d };
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// src/ws/pubsub.ts
|
|
582
|
+
class PubSub {
|
|
583
|
+
#origin = Bun.randomUUIDv7();
|
|
584
|
+
#server;
|
|
585
|
+
#relay;
|
|
586
|
+
#channel = DEFAULT_RELAY_CHANNEL;
|
|
587
|
+
#onRelayError = defaultRelayError;
|
|
588
|
+
#relayFailing = false;
|
|
589
|
+
#resubscribeTimer;
|
|
590
|
+
#resubscribeLeft = 0;
|
|
591
|
+
#resubscribeDelay = 0;
|
|
592
|
+
attach(server) {
|
|
593
|
+
this.#server = server;
|
|
594
|
+
}
|
|
595
|
+
get attached() {
|
|
596
|
+
return this.#server !== undefined;
|
|
597
|
+
}
|
|
598
|
+
get origin() {
|
|
599
|
+
return this.#origin;
|
|
600
|
+
}
|
|
601
|
+
get relaying() {
|
|
602
|
+
return this.#relay !== undefined;
|
|
603
|
+
}
|
|
604
|
+
async relayThrough(relay, options = {}) {
|
|
605
|
+
if (this.#relay) {
|
|
606
|
+
throw new AppError5("PubSub already relays. Two subscriptions on one channel would deliver " + "every relayed message twice \u2014 pass HttpOptions.relay or call " + "relayThrough(), not both.");
|
|
607
|
+
}
|
|
608
|
+
this.#relay = relay;
|
|
609
|
+
this.#channel = options.channel ?? DEFAULT_RELAY_CHANNEL;
|
|
610
|
+
this.#onRelayError = options.onError ?? defaultRelayError;
|
|
611
|
+
this.#resubscribeLeft = options.resubscribe?.attempts ?? 5;
|
|
612
|
+
this.#resubscribeDelay = options.resubscribe?.delayMs ?? 500;
|
|
613
|
+
await this.#trySubscribe();
|
|
614
|
+
}
|
|
615
|
+
async#trySubscribe() {
|
|
616
|
+
const relay = this.#relay;
|
|
617
|
+
if (!relay)
|
|
618
|
+
return;
|
|
619
|
+
try {
|
|
620
|
+
await relay.subscribe(this.#channel, (message) => {
|
|
621
|
+
this.#inbound(message);
|
|
622
|
+
});
|
|
623
|
+
this.#relayFailing = false;
|
|
624
|
+
this.#resubscribeLeft = 0;
|
|
625
|
+
} catch (error) {
|
|
626
|
+
this.#degrade(error, "subscribe");
|
|
627
|
+
this.#scheduleResubscribe();
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
#scheduleResubscribe() {
|
|
631
|
+
if (this.#resubscribeLeft <= 0 || this.#relay === undefined)
|
|
632
|
+
return;
|
|
633
|
+
this.#resubscribeLeft -= 1;
|
|
634
|
+
const delay = this.#resubscribeDelay;
|
|
635
|
+
this.#resubscribeDelay = Math.min(delay * 2, 30000);
|
|
636
|
+
this.#resubscribeTimer = setTimeout(() => {
|
|
637
|
+
this.#trySubscribe();
|
|
638
|
+
}, delay);
|
|
639
|
+
this.#resubscribeTimer.unref?.();
|
|
640
|
+
}
|
|
641
|
+
publish(topic, data, compress) {
|
|
642
|
+
const sent = this.#live().publish(topic, data, compress);
|
|
643
|
+
this.#outbound(topic, data);
|
|
644
|
+
return sent;
|
|
645
|
+
}
|
|
646
|
+
publishEvent(topic, event, data) {
|
|
647
|
+
return this.publish(topic, encode(event, data));
|
|
648
|
+
}
|
|
649
|
+
subscriberCount(topic) {
|
|
650
|
+
return this.#live().subscriberCount(topic);
|
|
651
|
+
}
|
|
652
|
+
async close() {
|
|
653
|
+
const relay = this.#relay;
|
|
654
|
+
this.#relay = undefined;
|
|
655
|
+
this.#resubscribeLeft = 0;
|
|
656
|
+
if (this.#resubscribeTimer !== undefined) {
|
|
657
|
+
clearTimeout(this.#resubscribeTimer);
|
|
658
|
+
this.#resubscribeTimer = undefined;
|
|
659
|
+
}
|
|
660
|
+
this.#server = undefined;
|
|
661
|
+
if (!relay?.close)
|
|
662
|
+
return;
|
|
663
|
+
try {
|
|
664
|
+
await relay.close();
|
|
665
|
+
} catch (error) {
|
|
666
|
+
this.#degrade(error, "close");
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
#outbound(topic, data) {
|
|
670
|
+
const relay = this.#relay;
|
|
671
|
+
if (!relay)
|
|
672
|
+
return;
|
|
673
|
+
try {
|
|
674
|
+
const result = relay.publish(this.#channel, encodeRelay(this.#origin, topic, data));
|
|
675
|
+
if (result instanceof Promise) {
|
|
676
|
+
result.then(() => {
|
|
677
|
+
this.#relayFailing = false;
|
|
678
|
+
}, (error) => {
|
|
679
|
+
this.#degrade(error, "publish");
|
|
680
|
+
});
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
this.#relayFailing = false;
|
|
684
|
+
} catch (error) {
|
|
685
|
+
this.#degrade(error, "publish");
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
#inbound(message) {
|
|
689
|
+
const frame = decodeRelay(message);
|
|
690
|
+
if (!frame || frame.origin === this.#origin)
|
|
691
|
+
return;
|
|
692
|
+
this.#server?.publish(frame.topic, frame.data);
|
|
693
|
+
}
|
|
694
|
+
#degrade(error, phase) {
|
|
695
|
+
if (this.#relayFailing)
|
|
696
|
+
return;
|
|
697
|
+
this.#relayFailing = true;
|
|
698
|
+
this.#onRelayError(error, phase);
|
|
699
|
+
}
|
|
700
|
+
#live() {
|
|
701
|
+
if (!this.#server) {
|
|
702
|
+
throw new AppError5("PubSub has no server yet. Publish once the server is listening: " + "HttpApp.listen() is what attaches it.");
|
|
703
|
+
}
|
|
704
|
+
return this.#server;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// src/server/application.ts
|
|
709
|
+
import {
|
|
710
|
+
AppError as AppError7,
|
|
711
|
+
Logger as Logger2
|
|
712
|
+
} from "@dunx/core";
|
|
713
|
+
|
|
714
|
+
// src/server/request-logging.ts
|
|
715
|
+
import { Logger, RequestContext } from "@dunx/core";
|
|
716
|
+
var REQUEST_ID_HEADER = "x-request-id";
|
|
717
|
+
var parse = (text, limit) => {
|
|
718
|
+
if (limit === 0)
|
|
719
|
+
return;
|
|
720
|
+
if (text.length === 0)
|
|
721
|
+
return;
|
|
722
|
+
if (text.length > limit)
|
|
723
|
+
return `[${text.length} bytes]`;
|
|
724
|
+
try {
|
|
725
|
+
return JSON.parse(text);
|
|
726
|
+
} catch {
|
|
727
|
+
return text;
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
var elapsedMs = (started) => Math.round((Bun.nanoseconds() - started) / 1e6);
|
|
731
|
+
|
|
732
|
+
class RequestLoggingMiddleware {
|
|
733
|
+
logger;
|
|
734
|
+
context;
|
|
735
|
+
#limit;
|
|
736
|
+
#requestBody;
|
|
737
|
+
#responseBody;
|
|
738
|
+
#ignore;
|
|
739
|
+
constructor(logger, context, options = {}) {
|
|
740
|
+
this.logger = logger;
|
|
741
|
+
this.context = context;
|
|
742
|
+
this.#limit = options.maxBodyLength ?? 2048;
|
|
743
|
+
this.#requestBody = options.requestBody ?? false;
|
|
744
|
+
this.#responseBody = options.responseBody ?? false;
|
|
745
|
+
this.#ignore = new Set(options.ignore ?? []);
|
|
746
|
+
}
|
|
747
|
+
handle(req, ctx, next) {
|
|
748
|
+
const url = req.url;
|
|
749
|
+
const from = url.indexOf("/", url.indexOf("://") + 3);
|
|
750
|
+
const mark = from === -1 ? -1 : url.indexOf("?", from);
|
|
751
|
+
const path = from === -1 ? "/" : mark === -1 ? url.slice(from) : url.slice(from, mark);
|
|
752
|
+
if (this.#ignore.size > 0 && this.#ignore.has(path))
|
|
753
|
+
return next();
|
|
754
|
+
const started = Bun.nanoseconds();
|
|
755
|
+
const requestId = req.headers.get(REQUEST_ID_HEADER) ?? crypto.randomUUID();
|
|
756
|
+
return this.context.runWithContext({
|
|
757
|
+
requestId,
|
|
758
|
+
method: ctx.method,
|
|
759
|
+
event: path,
|
|
760
|
+
flow: "http",
|
|
761
|
+
context: `${ctx.controller}.${ctx.handler}`
|
|
762
|
+
}, () => {
|
|
763
|
+
const request = {};
|
|
764
|
+
if (mark !== -1) {
|
|
765
|
+
request["query"] = Object.fromEntries(new URLSearchParams(url.slice(mark + 1)));
|
|
766
|
+
}
|
|
767
|
+
const body = this.#body(req);
|
|
768
|
+
if (body === undefined) {
|
|
769
|
+
request["userAgent"] = req.headers.get("user-agent");
|
|
770
|
+
return this.#dispatch(req, path, requestId, started, request, next);
|
|
771
|
+
}
|
|
772
|
+
return body.then((value) => {
|
|
773
|
+
if (value !== undefined)
|
|
774
|
+
request["body"] = value;
|
|
775
|
+
request["userAgent"] = req.headers.get("user-agent");
|
|
776
|
+
return this.#dispatch(req, path, requestId, started, request, next);
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
#dispatch(req, path, requestId, started, request, next) {
|
|
781
|
+
let settled;
|
|
782
|
+
try {
|
|
783
|
+
settled = next();
|
|
784
|
+
} catch (error) {
|
|
785
|
+
this.#failed(req, path, started, request, error);
|
|
786
|
+
throw error;
|
|
787
|
+
}
|
|
788
|
+
return settled.then((response) => this.#succeeded(req, path, requestId, started, request, response), (error) => {
|
|
789
|
+
this.#failed(req, path, started, request, error);
|
|
790
|
+
throw error;
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
#failed(req, path, started, request, error) {
|
|
794
|
+
const status = error instanceof HttpError ? error.status : HttpStatusCode.INTERNAL_SERVER_ERROR;
|
|
795
|
+
const entry = {
|
|
796
|
+
request,
|
|
797
|
+
err: error,
|
|
798
|
+
statusCode: status,
|
|
799
|
+
elapsedMs: elapsedMs(started)
|
|
800
|
+
};
|
|
801
|
+
const line = `${req.method} ${path} ${status}`;
|
|
802
|
+
if (status < HttpStatusCode.INTERNAL_SERVER_ERROR) {
|
|
803
|
+
this.logger.warn(line, entry);
|
|
804
|
+
} else {
|
|
805
|
+
this.logger.error(line, entry);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
#succeeded(req, path, requestId, started, request, response) {
|
|
809
|
+
const body = this.#responseFields(response);
|
|
810
|
+
if (body === undefined) {
|
|
811
|
+
this.logger.info(`${req.method} ${path} ${response.status}`, {
|
|
812
|
+
request,
|
|
813
|
+
statusCode: response.status,
|
|
814
|
+
elapsedMs: elapsedMs(started)
|
|
815
|
+
});
|
|
816
|
+
response.headers.set(REQUEST_ID_HEADER, requestId);
|
|
817
|
+
return response;
|
|
818
|
+
}
|
|
819
|
+
return body.then((value) => {
|
|
820
|
+
this.logger.info(`${req.method} ${path} ${response.status}`, {
|
|
821
|
+
request,
|
|
822
|
+
statusCode: response.status,
|
|
823
|
+
...value === undefined ? {} : { responseBody: value },
|
|
824
|
+
elapsedMs: elapsedMs(started)
|
|
825
|
+
});
|
|
826
|
+
response.headers.set(REQUEST_ID_HEADER, requestId);
|
|
827
|
+
return response;
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
#body(req) {
|
|
831
|
+
if (!this.#requestBody)
|
|
832
|
+
return;
|
|
833
|
+
if (req.method === "GET" || req.method === "HEAD")
|
|
834
|
+
return;
|
|
835
|
+
if (!(req.headers.get("content-type") ?? "").includes("application/json")) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
return req.clone().text().then((text) => parse(text, this.#limit));
|
|
839
|
+
}
|
|
840
|
+
#responseFields(response) {
|
|
841
|
+
if (!this.#responseBody)
|
|
842
|
+
return;
|
|
843
|
+
if (!(response.headers.get("content-type") ?? "").includes("application/json")) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
return response.clone().text().then((text) => parse(text, this.#limit));
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
Object.defineProperty(RequestLoggingMiddleware, Symbol.for("dunx.deps"), {
|
|
850
|
+
value: () => [Logger, RequestContext, { unresolved: "options: RequestLoggingOptions = {}" }]
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
// src/server/routes.ts
|
|
854
|
+
import { AppError as AppError6 } from "@dunx/core";
|
|
855
|
+
|
|
856
|
+
// src/server/input.ts
|
|
857
|
+
var grouped = (entries) => {
|
|
858
|
+
const collected = {};
|
|
859
|
+
entries.forEach((value, key) => {
|
|
860
|
+
const existing = collected[key];
|
|
861
|
+
if (existing === undefined)
|
|
862
|
+
collected[key] = value;
|
|
863
|
+
else if (Array.isArray(existing))
|
|
864
|
+
existing.push(value);
|
|
865
|
+
else
|
|
866
|
+
collected[key] = [existing, value];
|
|
867
|
+
});
|
|
868
|
+
return collected;
|
|
869
|
+
};
|
|
870
|
+
var asJson = (req) => req.json();
|
|
871
|
+
var asUrlEncoded = async (req) => grouped(new URLSearchParams(await req.text()));
|
|
872
|
+
var asMultipart = async (req) => grouped(await req.formData());
|
|
873
|
+
var asText = (req) => req.text();
|
|
874
|
+
var parserFor = (media) => {
|
|
875
|
+
if (media === "application/json" || media.endsWith("+json"))
|
|
876
|
+
return asJson;
|
|
877
|
+
if (media === "application/x-www-form-urlencoded")
|
|
878
|
+
return asUrlEncoded;
|
|
879
|
+
if (media === "multipart/form-data")
|
|
880
|
+
return asMultipart;
|
|
881
|
+
if (media.startsWith("text/"))
|
|
882
|
+
return asText;
|
|
883
|
+
return;
|
|
884
|
+
};
|
|
885
|
+
var JSON_MEDIA = "application/json";
|
|
886
|
+
var mediaTypeOf = (req) => {
|
|
887
|
+
const header = req.headers.get("content-type");
|
|
888
|
+
if (header === JSON_MEDIA || header === null)
|
|
889
|
+
return JSON_MEDIA;
|
|
890
|
+
const end = header.indexOf(";");
|
|
891
|
+
const media = (end === -1 ? header : header.slice(0, end)).trim();
|
|
892
|
+
return media === "" ? JSON_MEDIA : media.toLowerCase();
|
|
893
|
+
};
|
|
894
|
+
var flatten = (issue) => {
|
|
895
|
+
const path = issue.path?.map((segment) => String(typeof segment === "object" ? segment.key : segment)).join(".");
|
|
896
|
+
return path === undefined || path === "" ? { message: issue.message } : { message: issue.message, path };
|
|
897
|
+
};
|
|
898
|
+
var accept = (source, result) => {
|
|
899
|
+
if (result.issues !== undefined) {
|
|
900
|
+
throw new ValidationError(source, result.issues.map(flatten));
|
|
901
|
+
}
|
|
902
|
+
return result.value;
|
|
903
|
+
};
|
|
904
|
+
var fillWith = (draft, source, schema, value) => {
|
|
905
|
+
const result = schema["~standard"].validate(value);
|
|
906
|
+
if (result instanceof Promise) {
|
|
907
|
+
return result.then((settled) => {
|
|
908
|
+
draft[source] = accept(source, settled);
|
|
909
|
+
return draft;
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
draft[source] = accept(source, result);
|
|
913
|
+
return draft;
|
|
914
|
+
};
|
|
915
|
+
var bodyFill = (schema) => (draft) => {
|
|
916
|
+
const media = mediaTypeOf(draft.req);
|
|
917
|
+
const parse2 = parserFor(media);
|
|
918
|
+
if (parse2 === undefined) {
|
|
919
|
+
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/*.");
|
|
920
|
+
}
|
|
921
|
+
return parse2(draft.req).then((value) => fillWith(draft, "body", schema, value), (error) => {
|
|
922
|
+
throw new HttpError(HttpStatusCode.BAD_REQUEST, `Malformed ${media} body`, { cause: error });
|
|
923
|
+
});
|
|
924
|
+
};
|
|
925
|
+
var searchOf = (url) => {
|
|
926
|
+
const start = url.indexOf("?");
|
|
927
|
+
if (start === -1)
|
|
928
|
+
return "";
|
|
929
|
+
const end = url.indexOf("#", start + 1);
|
|
930
|
+
return end === -1 ? url.slice(start + 1) : url.slice(start + 1, end);
|
|
931
|
+
};
|
|
932
|
+
var queryFill = (schema) => (draft) => {
|
|
933
|
+
const params = new URLSearchParams(searchOf(draft.req.url));
|
|
934
|
+
return fillWith(draft, "query", schema, grouped(params));
|
|
935
|
+
};
|
|
936
|
+
var paramsFill = (schema) => (draft) => fillWith(draft, "params", schema, draft.req.params);
|
|
937
|
+
var then = (first, second) => (draft) => {
|
|
938
|
+
const started = first(draft);
|
|
939
|
+
return started instanceof Promise ? started.then(second) : second(started);
|
|
940
|
+
};
|
|
941
|
+
var buildInputReader = (options) => {
|
|
942
|
+
const fills = [];
|
|
943
|
+
if (options?.body !== undefined)
|
|
944
|
+
fills.push(bodyFill(options.body));
|
|
945
|
+
if (options?.query !== undefined)
|
|
946
|
+
fills.push(queryFill(options.query));
|
|
947
|
+
if (options?.params !== undefined)
|
|
948
|
+
fills.push(paramsFill(options.params));
|
|
949
|
+
if (fills.length === 0)
|
|
950
|
+
return (req) => ({ req });
|
|
951
|
+
const fill = fills.reduce(then);
|
|
952
|
+
return (req) => fill({ req });
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
// src/server/middleware.ts
|
|
956
|
+
var compose = (middleware, ctx, handler) => middleware.reduceRight((next, current) => (req) => current.handle(req, ctx, () => next(req)), handler);
|
|
957
|
+
|
|
958
|
+
// src/server/routes.ts
|
|
959
|
+
var construct = (guard) => new guard;
|
|
960
|
+
var toResponse = (value, status) => {
|
|
961
|
+
if (value instanceof Response)
|
|
962
|
+
return value;
|
|
963
|
+
if (value === undefined || value === null) {
|
|
964
|
+
return new Response(null, { status: HttpStatusCode.NO_CONTENT });
|
|
965
|
+
}
|
|
966
|
+
return Response.json(value, { status });
|
|
967
|
+
};
|
|
968
|
+
var statusFor = (route) => route.options?.status ?? (route.method === "POST" ? HttpStatusCode.CREATED : HttpStatusCode.OK);
|
|
969
|
+
var assertNoCollisions = (discovered) => {
|
|
970
|
+
const owners = new Map;
|
|
971
|
+
for (const route of discovered) {
|
|
972
|
+
const key = `${route.method} ${route.path}`;
|
|
973
|
+
const owner = `${route.controller}.${route.handlerName}`;
|
|
974
|
+
const existing = owners.get(key);
|
|
975
|
+
if (existing !== undefined) {
|
|
976
|
+
throw new AppError6(`Route collision: ${key} is declared by ${existing} and by ${owner}. ` + "Bun would keep only one of them.");
|
|
977
|
+
}
|
|
978
|
+
owners.set(key, owner);
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
var assertNoGatewayCollisions = (discovered, gatewayPaths) => {
|
|
982
|
+
const gateways = new Set(gatewayPaths);
|
|
983
|
+
for (const route of discovered) {
|
|
984
|
+
if (gateways.has(route.path)) {
|
|
985
|
+
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.");
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
var withUpgradeRoutes = (routes, gateways) => {
|
|
990
|
+
const merged = { ...routes };
|
|
991
|
+
for (const [path, upgrade] of gateways)
|
|
992
|
+
merged[path] = { GET: upgrade };
|
|
993
|
+
return merged;
|
|
994
|
+
};
|
|
995
|
+
var unmatchedContext = (req) => Object.freeze({
|
|
996
|
+
controller: "(unmatched)",
|
|
997
|
+
handler: "(none)",
|
|
998
|
+
method: req.method,
|
|
999
|
+
path: new URL(req.url).pathname,
|
|
1000
|
+
get: () => {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
var buildFallback = (middleware = [], onError = defaultErrorMapper, cors) => {
|
|
1005
|
+
const miss = () => {
|
|
1006
|
+
throw new HttpError(HttpStatusCode.NOT_FOUND, "NOT_FOUND");
|
|
1007
|
+
};
|
|
1008
|
+
const run = async (req) => {
|
|
1009
|
+
try {
|
|
1010
|
+
return await compose(middleware, unmatchedContext(req), miss)(req);
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
return onError(error, req);
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
return cors ? withCors(cors, run) : run;
|
|
1016
|
+
};
|
|
1017
|
+
var directOr = (guarded, route, read, status, onError, noMiddleware) => {
|
|
1018
|
+
if (!noMiddleware)
|
|
1019
|
+
return guarded;
|
|
1020
|
+
const settle2 = (value, req) => {
|
|
1021
|
+
try {
|
|
1022
|
+
return toResponse(value, status);
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
return onError(error, req);
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
const invoke = (input, req) => {
|
|
1028
|
+
try {
|
|
1029
|
+
const value = route.handler(input);
|
|
1030
|
+
return value instanceof Promise ? value.then((resolved) => settle2(resolved, req), (error) => onError(error, req)) : settle2(value, req);
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
return onError(error, req);
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
return (req) => {
|
|
1036
|
+
try {
|
|
1037
|
+
const input = read(req);
|
|
1038
|
+
return input instanceof Promise ? input.then((resolved) => invoke(resolved, req), (error) => onError(error, req)) : invoke(input, req);
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
return onError(error, req);
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
1043
|
+
};
|
|
1044
|
+
var buildRoutes = (discovered, middleware = [], onError = defaultErrorMapper, cors, resolve = construct) => {
|
|
1045
|
+
assertNoCollisions(discovered);
|
|
1046
|
+
const routes = {};
|
|
1047
|
+
const instances = new Map;
|
|
1048
|
+
const guardOf = (guard) => {
|
|
1049
|
+
const existing = instances.get(guard);
|
|
1050
|
+
if (existing)
|
|
1051
|
+
return existing;
|
|
1052
|
+
const created = resolve(guard);
|
|
1053
|
+
instances.set(guard, created);
|
|
1054
|
+
return created;
|
|
1055
|
+
};
|
|
1056
|
+
for (const route of discovered) {
|
|
1057
|
+
const read = buildInputReader(route.options);
|
|
1058
|
+
const status = statusFor(route);
|
|
1059
|
+
const chain = [...middleware, ...(route.guards ?? []).map(guardOf)];
|
|
1060
|
+
const chained = compose(chain, buildContext(route), async (req) => toResponse(await route.handler(await read(req)), status));
|
|
1061
|
+
const guarded = async (req) => {
|
|
1062
|
+
try {
|
|
1063
|
+
return await chained(req);
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
return onError(error, req);
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
const byMethod = routes[route.path] ??= {};
|
|
1069
|
+
byMethod[route.method] = cors ? withCors(cors, guarded) : directOr(guarded, route, read, status, onError, chain.length === 0);
|
|
1070
|
+
}
|
|
1071
|
+
if (cors) {
|
|
1072
|
+
for (const byMethod of Object.values(routes)) {
|
|
1073
|
+
byMethod.OPTIONS = preflight(cors, Object.keys(byMethod));
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return routes;
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
// src/server/settings.ts
|
|
1080
|
+
var defaultSettings = () => ({ "trust proxy": false });
|
|
1081
|
+
|
|
1082
|
+
// src/server/application.ts
|
|
1083
|
+
class HttpApplication {
|
|
1084
|
+
closed;
|
|
1085
|
+
gatewayPaths;
|
|
1086
|
+
#app;
|
|
1087
|
+
#discovered;
|
|
1088
|
+
#middleware;
|
|
1089
|
+
#settings = defaultSettings();
|
|
1090
|
+
#onError;
|
|
1091
|
+
#port;
|
|
1092
|
+
#websocket;
|
|
1093
|
+
#relay;
|
|
1094
|
+
#relayChannel;
|
|
1095
|
+
#globalPrefix = "";
|
|
1096
|
+
#cors;
|
|
1097
|
+
#started = false;
|
|
1098
|
+
#server;
|
|
1099
|
+
#resolveClosed;
|
|
1100
|
+
#shuttingDown;
|
|
1101
|
+
#hooked = false;
|
|
1102
|
+
constructor(app, discovered, options, websocket) {
|
|
1103
|
+
this.#app = app;
|
|
1104
|
+
this.#discovered = discovered;
|
|
1105
|
+
this.#middleware = [
|
|
1106
|
+
...options.requestLogging === false ? [] : [RequestLoggingMiddleware],
|
|
1107
|
+
...options.middleware ?? []
|
|
1108
|
+
];
|
|
1109
|
+
this.#onError = options.onError ?? defaultErrorMapper;
|
|
1110
|
+
this.#port = options.port ?? 3000;
|
|
1111
|
+
this.#websocket = websocket;
|
|
1112
|
+
this.#relay = options.relay;
|
|
1113
|
+
this.#relayChannel = options.relayChannel;
|
|
1114
|
+
this.gatewayPaths = websocket?.paths ?? [];
|
|
1115
|
+
this.closed = new Promise((resolve) => {
|
|
1116
|
+
this.#resolveClosed = resolve;
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
get(token) {
|
|
1120
|
+
return this.#app.get(token);
|
|
1121
|
+
}
|
|
1122
|
+
setGlobalPrefix(prefix) {
|
|
1123
|
+
this.#assertNotStarted("setGlobalPrefix()");
|
|
1124
|
+
this.#globalPrefix = prefix;
|
|
1125
|
+
return this;
|
|
1126
|
+
}
|
|
1127
|
+
use(...middleware) {
|
|
1128
|
+
this.#assertNotStarted("use()");
|
|
1129
|
+
this.#middleware.push(...middleware);
|
|
1130
|
+
return this;
|
|
1131
|
+
}
|
|
1132
|
+
set(key, value) {
|
|
1133
|
+
this.#assertNotStarted("set()");
|
|
1134
|
+
this.#settings[key] = value;
|
|
1135
|
+
return this;
|
|
1136
|
+
}
|
|
1137
|
+
setting(key) {
|
|
1138
|
+
return this.#settings[key];
|
|
1139
|
+
}
|
|
1140
|
+
enableCors(options = {}) {
|
|
1141
|
+
this.#assertNotStarted("enableCors()");
|
|
1142
|
+
this.#cors = options;
|
|
1143
|
+
return this;
|
|
1144
|
+
}
|
|
1145
|
+
clientIp(req) {
|
|
1146
|
+
return this.#app.get(ClientAddress).of(req);
|
|
1147
|
+
}
|
|
1148
|
+
async listen(port = this.#port) {
|
|
1149
|
+
this.#assertNotStarted("listen()");
|
|
1150
|
+
this.#started = true;
|
|
1151
|
+
const middleware = this.#middleware.map((entry) => this.#app.get(entry));
|
|
1152
|
+
const prefixed = this.#prefixed();
|
|
1153
|
+
const routes = buildRoutes(prefixed, middleware, this.#onError, this.#cors, (guard) => this.#app.get(guard));
|
|
1154
|
+
const ws = this.#websocket;
|
|
1155
|
+
if (ws)
|
|
1156
|
+
assertNoGatewayCollisions(prefixed, ws.paths);
|
|
1157
|
+
const fetch = buildFallback(middleware, this.#onError, this.#cors);
|
|
1158
|
+
const options = ws ? {
|
|
1159
|
+
port,
|
|
1160
|
+
fetch,
|
|
1161
|
+
routes: withUpgradeRoutes(routes, ws.routes),
|
|
1162
|
+
websocket: ws.websocket
|
|
1163
|
+
} : { port, fetch, routes };
|
|
1164
|
+
this.#server = Bun.serve(options);
|
|
1165
|
+
attachAddressSource(this.#app.get(ClientAddress), {
|
|
1166
|
+
server: this.#server,
|
|
1167
|
+
trustProxy: this.#settings["trust proxy"]
|
|
1168
|
+
});
|
|
1169
|
+
const pubsub = this.#app.get(PubSub);
|
|
1170
|
+
pubsub.attach(this.#server);
|
|
1171
|
+
if (this.#relay) {
|
|
1172
|
+
const logger = this.#app.get(Logger2);
|
|
1173
|
+
await pubsub.relayThrough(this.#relay, {
|
|
1174
|
+
...this.#relayChannel !== undefined && {
|
|
1175
|
+
channel: this.#relayChannel
|
|
1176
|
+
},
|
|
1177
|
+
onError: (error, phase) => {
|
|
1178
|
+
logger.warn(`the websocket relay could not ${phase}. Fan-out is local to this ` + "process until it recovers.", { error });
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
return this.#server.url.href;
|
|
1183
|
+
}
|
|
1184
|
+
async shutdown() {
|
|
1185
|
+
this.#shuttingDown ??= (async () => {
|
|
1186
|
+
await this.#server?.stop(this.#websocket !== undefined);
|
|
1187
|
+
this.#server = undefined;
|
|
1188
|
+
await this.#app.get(PubSub).close();
|
|
1189
|
+
await this.#app.shutdown();
|
|
1190
|
+
this.#resolveClosed?.();
|
|
1191
|
+
})();
|
|
1192
|
+
return this.#shuttingDown;
|
|
1193
|
+
}
|
|
1194
|
+
enableShutdownHooks(signals = ["SIGTERM", "SIGINT"]) {
|
|
1195
|
+
if (this.#hooked)
|
|
1196
|
+
return this;
|
|
1197
|
+
this.#hooked = true;
|
|
1198
|
+
for (const signal of signals) {
|
|
1199
|
+
process.once(signal, () => void this.shutdown());
|
|
1200
|
+
}
|
|
1201
|
+
return this;
|
|
1202
|
+
}
|
|
1203
|
+
#prefixed() {
|
|
1204
|
+
if (this.#globalPrefix === "")
|
|
1205
|
+
return this.#discovered;
|
|
1206
|
+
return this.#discovered.map((route) => ({
|
|
1207
|
+
...route,
|
|
1208
|
+
path: joinPath(this.#globalPrefix, route.path)
|
|
1209
|
+
}));
|
|
1210
|
+
}
|
|
1211
|
+
#assertNotStarted(hook) {
|
|
1212
|
+
if (!this.#started)
|
|
1213
|
+
return;
|
|
1214
|
+
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.");
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
Object.defineProperty(HttpApplication, Symbol.for("dunx.deps"), {
|
|
1218
|
+
value: () => [{ unresolved: "app: App" }, { unresolved: "discovered: readonly DiscoveredRoute[]" }, { unresolved: "options: HttpOptions" }, { unresolved: "websocket?: WebSocketRuntime" }]
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
// src/server/factory.ts
|
|
1222
|
+
class HttpModule {
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
class HttpFactory {
|
|
1226
|
+
static async create(root, options = {}) {
|
|
1227
|
+
const logging = provide(RequestLoggingMiddleware, {
|
|
1228
|
+
useFactory: (logger, context) => new RequestLoggingMiddleware(logger, context, typeof options.requestLogging === "object" ? options.requestLogging : {}),
|
|
1229
|
+
inject: [Logger3, RequestContext2]
|
|
1230
|
+
});
|
|
1231
|
+
const scope = {
|
|
1232
|
+
module: HttpModule,
|
|
1233
|
+
imports: [root],
|
|
1234
|
+
providers: options.requestLogging === false ? [PubSub] : [PubSub, logging]
|
|
1235
|
+
};
|
|
1236
|
+
const app = await AppFactory.create(scope, options.overrides ? { overrides: options.overrides } : {});
|
|
1237
|
+
const modules = collectModules(scope);
|
|
1238
|
+
const discovered = [];
|
|
1239
|
+
for (const module of modules) {
|
|
1240
|
+
for (const controller of readControllers(module)) {
|
|
1241
|
+
const routes = discoverRoutes(app.get(controller));
|
|
1242
|
+
if (routes.length === 0) {
|
|
1243
|
+
throw new AppError8(`${controller.name} is registered as a controller but declares no routes. ` + "Add a @Get/@Post/... method, or move it to providers.");
|
|
1244
|
+
}
|
|
1245
|
+
discovered.push(...routes);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
assertNoCollisions(discovered);
|
|
1249
|
+
const gateways = discoverGateways(modules, (token) => app.get(token));
|
|
1250
|
+
const websocket = gateways.length > 0 ? buildWebSocket(gateways, options.websocket) : undefined;
|
|
1251
|
+
return new HttpApplication(app, discovered, options, websocket);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
// src/ws/decorators.ts
|
|
1255
|
+
var Gateway = (path = "/") => (target) => {
|
|
1256
|
+
markGateway(target, path);
|
|
1257
|
+
return target;
|
|
1258
|
+
};
|
|
1259
|
+
var lifecycle = (kind) => () => (value) => {
|
|
1260
|
+
markHandler(value, { kind, event: undefined });
|
|
1261
|
+
return value;
|
|
1262
|
+
};
|
|
1263
|
+
var OnUpgrade = lifecycle(HandlerKind.UPGRADE);
|
|
1264
|
+
var OnOpen = lifecycle(HandlerKind.OPEN);
|
|
1265
|
+
var OnClose = lifecycle(HandlerKind.CLOSE);
|
|
1266
|
+
var OnDrain = lifecycle(HandlerKind.DRAIN);
|
|
1267
|
+
var OnPing = lifecycle(HandlerKind.PING);
|
|
1268
|
+
var OnPong = lifecycle(HandlerKind.PONG);
|
|
1269
|
+
var OnMessage = (event) => (value) => {
|
|
1270
|
+
markHandler(value, { kind: HandlerKind.MESSAGE, event });
|
|
1271
|
+
return value;
|
|
1272
|
+
};
|
|
1273
|
+
// src/ws/redis-relay.ts
|
|
1274
|
+
import { AppError as AppError9 } from "@dunx/core";
|
|
1275
|
+
var PROTOCOLS = [
|
|
1276
|
+
"redis:",
|
|
1277
|
+
"rediss:",
|
|
1278
|
+
"valkey:",
|
|
1279
|
+
"valkeys:",
|
|
1280
|
+
"redis+tls:",
|
|
1281
|
+
"redis+unix:",
|
|
1282
|
+
"redis+tls+unix:"
|
|
1283
|
+
];
|
|
1284
|
+
var defaultRelayUrl = () => process.env["VALKEY_URL"] ?? process.env["REDIS_URL"] ?? "redis://localhost:6379";
|
|
1285
|
+
var assertUrl = (url) => {
|
|
1286
|
+
let parsed;
|
|
1287
|
+
try {
|
|
1288
|
+
parsed = new URL(url);
|
|
1289
|
+
} catch {
|
|
1290
|
+
throw new AppError9(`${JSON.stringify(url)} is not a valid URL for the websocket relay. ` + "Expected something like redis://localhost:6379.");
|
|
1291
|
+
}
|
|
1292
|
+
if (!PROTOCOLS.includes(parsed.protocol)) {
|
|
1293
|
+
throw new AppError9(`Unsupported protocol ${JSON.stringify(parsed.protocol)} in ` + `${JSON.stringify(url)}. Expected one of ${PROTOCOLS.join(", ")}.`);
|
|
1294
|
+
}
|
|
1295
|
+
return url;
|
|
1296
|
+
};
|
|
1297
|
+
|
|
1298
|
+
class RedisRelay {
|
|
1299
|
+
#url;
|
|
1300
|
+
#options;
|
|
1301
|
+
#pub;
|
|
1302
|
+
#sub;
|
|
1303
|
+
#channel;
|
|
1304
|
+
constructor(options = {}) {
|
|
1305
|
+
this.#url = assertUrl(options.url ?? defaultRelayUrl());
|
|
1306
|
+
this.#options = {
|
|
1307
|
+
maxRetries: options.maxRetries ?? 0,
|
|
1308
|
+
...options.connectionTimeout !== undefined && {
|
|
1309
|
+
connectionTimeout: options.connectionTimeout
|
|
1310
|
+
},
|
|
1311
|
+
...options.tls !== undefined && { tls: options.tls }
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
get url() {
|
|
1315
|
+
const parsed = new URL(this.#url);
|
|
1316
|
+
if (parsed.password)
|
|
1317
|
+
parsed.password = "***";
|
|
1318
|
+
return parsed.toString();
|
|
1319
|
+
}
|
|
1320
|
+
async publish(channel, message) {
|
|
1321
|
+
const client = this.#pub ??= new Bun.RedisClient(this.#url, this.#options);
|
|
1322
|
+
try {
|
|
1323
|
+
return await client.publish(channel, message);
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
if (this.#pub === client) {
|
|
1326
|
+
this.#pub = undefined;
|
|
1327
|
+
client.close();
|
|
1328
|
+
}
|
|
1329
|
+
throw error;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
async subscribe(channel, listener) {
|
|
1333
|
+
const client = this.#sub ??= new Bun.RedisClient(this.#url, this.#options);
|
|
1334
|
+
try {
|
|
1335
|
+
await client.connect();
|
|
1336
|
+
await client.subscribe(channel, listener);
|
|
1337
|
+
this.#channel = channel;
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
if (this.#sub === client) {
|
|
1340
|
+
this.#sub = undefined;
|
|
1341
|
+
client.close();
|
|
1342
|
+
}
|
|
1343
|
+
throw error;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
async close() {
|
|
1347
|
+
const sub = this.#sub;
|
|
1348
|
+
const channel = this.#channel;
|
|
1349
|
+
this.#pub?.close();
|
|
1350
|
+
this.#pub = undefined;
|
|
1351
|
+
this.#sub = undefined;
|
|
1352
|
+
this.#channel = undefined;
|
|
1353
|
+
if (!sub)
|
|
1354
|
+
return;
|
|
1355
|
+
if (channel !== undefined) {
|
|
1356
|
+
try {
|
|
1357
|
+
await sub.unsubscribe(channel);
|
|
1358
|
+
} catch {}
|
|
1359
|
+
}
|
|
1360
|
+
sub.close();
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
Object.defineProperty(RedisRelay, Symbol.for("dunx.deps"), {
|
|
1364
|
+
value: () => [{ unresolved: "options: RedisRelayOptions = {}" }]
|
|
1365
|
+
});
|
|
1366
|
+
export {
|
|
1367
|
+
withUpgradeRoutes,
|
|
1368
|
+
withCors,
|
|
1369
|
+
preflight,
|
|
1370
|
+
normalizePath,
|
|
1371
|
+
metaOf,
|
|
1372
|
+
metaKey,
|
|
1373
|
+
meta,
|
|
1374
|
+
mergeMeta,
|
|
1375
|
+
joinPath,
|
|
1376
|
+
guardsOf,
|
|
1377
|
+
encodeRelay,
|
|
1378
|
+
encode,
|
|
1379
|
+
discoverRoutes,
|
|
1380
|
+
discoverGateways,
|
|
1381
|
+
discoverGateway,
|
|
1382
|
+
defaultRelayUrl,
|
|
1383
|
+
defaultErrorMapper,
|
|
1384
|
+
decodeRelay,
|
|
1385
|
+
decode,
|
|
1386
|
+
compose,
|
|
1387
|
+
buildWebSocket,
|
|
1388
|
+
buildRuntime,
|
|
1389
|
+
buildRoutes,
|
|
1390
|
+
buildGateways,
|
|
1391
|
+
buildContext,
|
|
1392
|
+
assertNoGatewayCollisions,
|
|
1393
|
+
assertNoCollisions,
|
|
1394
|
+
ValidationError,
|
|
1395
|
+
UseGuards,
|
|
1396
|
+
Roles,
|
|
1397
|
+
RequestLoggingMiddleware,
|
|
1398
|
+
RedisRelay,
|
|
1399
|
+
ROLES,
|
|
1400
|
+
REQUEST_ID_HEADER,
|
|
1401
|
+
Put,
|
|
1402
|
+
Public,
|
|
1403
|
+
PubSub,
|
|
1404
|
+
Post,
|
|
1405
|
+
Patch,
|
|
1406
|
+
PUBLIC,
|
|
1407
|
+
OnUpgrade,
|
|
1408
|
+
OnPong,
|
|
1409
|
+
OnPing,
|
|
1410
|
+
OnOpen,
|
|
1411
|
+
OnMessage,
|
|
1412
|
+
OnDrain,
|
|
1413
|
+
OnClose,
|
|
1414
|
+
HttpStatusCode,
|
|
1415
|
+
HttpFactory,
|
|
1416
|
+
HttpError,
|
|
1417
|
+
HandlerKind,
|
|
1418
|
+
Get,
|
|
1419
|
+
Gateway,
|
|
1420
|
+
Delete,
|
|
1421
|
+
DEFAULT_RELAY_CHANNEL,
|
|
1422
|
+
Controller,
|
|
1423
|
+
ClientAddress
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1426
|
+
//# debugId=B2F007DAD14DC76D64756E2164756E21
|
|
1427
|
+
//# sourceMappingURL=index.js.map
|