@dunx/http 3.6.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/chunk-08k9vq31.js +94 -0
- package/dist/{chunk-p9hdmkm6.js → chunk-1jt27yka.js} +8 -83
- package/dist/chunk-3eecdh6d.js +160 -0
- package/dist/client/service.d.ts +13 -10
- package/dist/client/sse.d.ts +21 -8
- package/dist/client.d.ts +2 -0
- package/dist/client.js +52 -17
- package/dist/connect/middleware.d.ts +25 -0
- package/dist/connect/module.d.ts +29 -0
- package/dist/connect/options.d.ts +64 -0
- package/dist/connect/registry.d.ts +47 -0
- package/dist/connect.d.ts +12 -0
- package/dist/connect.js +208 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +253 -184
- package/dist/internal.d.ts +8 -10
- package/dist/internal.js +7 -3
- package/dist/route/claims.d.ts +11 -0
- package/dist/route/metadata.d.ts +11 -0
- package/dist/route/prefix.d.ts +6 -0
- package/dist/server/application.d.ts +1 -1
- package/dist/server/binding.d.ts +4 -2
- package/dist/server/claimed-routes.d.ts +15 -0
- package/dist/server/cors.d.ts +2 -2
- package/dist/server/middleware.d.ts +14 -2
- package/dist/server/options-provider.d.ts +3 -0
- package/dist/server/options.d.ts +8 -0
- package/dist/server/routes.d.ts +7 -2
- package/dist/sse/decorators.d.ts +28 -0
- package/dist/sse/event.d.ts +19 -0
- package/dist/sse/stream.d.ts +35 -0
- package/dist/static/files.d.ts +7 -0
- package/dist/static/options.d.ts +2 -2
- package/dist/throttle/guard.d.ts +3 -1
- package/package.json +19 -2
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ The guide is canonical for every row; this table is the index.
|
|
|
62
62
|
| Typed input | `body`, `query`, `params` over Standard Schema | [Validation](../../docs/guide/06-validation.md) |
|
|
63
63
|
| Middleware and guards | One extension point, `@UseGuards`, `@Roles`, `@Public` | [Middleware and guards](../../docs/guide/08-middleware-and-guards.md) |
|
|
64
64
|
| WebSocket gateways | `@Gateway`, handlers, `PubSub`, multi-node relay | [WebSockets](../../docs/guide/09-websockets.md) |
|
|
65
|
+
| Server-sent events | `@Sse`, `SseStream`, framing, heartbeats, `Last-Event-ID` | [Controllers](../../docs/guide/05-controllers.md) |
|
|
65
66
|
| Request logging | One structured entry per request, on by default | [Logging](../../docs/guide/13-logging.md) |
|
|
66
67
|
| Trace context | W3C `traceparent` adopted and propagated, on by default | [Logging](../../docs/guide/13-logging.md) |
|
|
67
68
|
| Metrics | Per-route counts and timings, off by default | [Metrics](../../docs/guide/23-metrics.md) |
|
|
@@ -70,6 +71,7 @@ The guide is canonical for every row; this table is the index.
|
|
|
70
71
|
| Outbound resilience | `HttpRetryClassifier`: which statuses retry, and `Retry-After` | [Resilience](../../docs/guide/25-resilience.md) |
|
|
71
72
|
| Static files | `Bun.file` behind a mount, with a cache policy | [Deployment](../../docs/guide/20-deployment.md) |
|
|
72
73
|
| Compression | zstd and gzip on Bun's own compressors | [Deployment](../../docs/guide/20-deployment.md) |
|
|
74
|
+
| RPC | protobuf over Connect and gRPC-Web, as middleware | [RPC](../../docs/guide/27-rpc.md) |
|
|
73
75
|
|
|
74
76
|
## Subpaths
|
|
75
77
|
|
|
@@ -77,6 +79,7 @@ The guide is canonical for every row; this table is the index.
|
|
|
77
79
|
| ---------------------- | ----------------------------------------------------------------- |
|
|
78
80
|
| `@dunx/http` | Everything above |
|
|
79
81
|
| `@dunx/http/client` | The outbound half: `HttpService`, retry with backoff, `HttpModule` |
|
|
82
|
+
| `@dunx/http/connect` | protobuf services over Connect and gRPC-Web, mounted as middleware |
|
|
80
83
|
| `@dunx/http/internal` | The framework's own plumbing. No stability promise |
|
|
81
84
|
|
|
82
85
|
`@dunx/http/internal` holds route-table construction, the middleware fold, the
|
|
@@ -101,6 +104,12 @@ from, and it may change in any release.
|
|
|
101
104
|
ns. W3C Trace Context is the only correlation id; there is no second one.
|
|
102
105
|
- `metrics: true` adds per-route counts and a nanosecond histogram at +35.2 ns a
|
|
103
106
|
request, folded into the `.then` request logging already allocates.
|
|
107
|
+
- `@dunx/http/connect` serves Connect and gRPC-Web, not native gRPC. gRPC carries
|
|
108
|
+
`grpc-status` in an HTTP trailer and `Bun.serve` sends no trailers, so a request
|
|
109
|
+
with `content-type: application/grpc` gets a 415 that says so.
|
|
110
|
+
`@connectrpc/connect` and `@bufbuild/protobuf` are optional peers, and the
|
|
111
|
+
`.proto` toolchain stays yours. `ThrottleGuard` does not cover an RPC: it skips
|
|
112
|
+
every unmatched path, and an RPC path is in no route table.
|
|
104
113
|
|
|
105
114
|
## License
|
|
106
115
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __name = (target, name) => {
|
|
6
|
+
Object.defineProperty(target, "name", {
|
|
7
|
+
value: name,
|
|
8
|
+
enumerable: false,
|
|
9
|
+
configurable: true
|
|
10
|
+
});
|
|
11
|
+
return target;
|
|
12
|
+
};
|
|
13
|
+
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
|
|
14
|
+
var __typeError = (msg) => {
|
|
15
|
+
throw TypeError(msg);
|
|
16
|
+
};
|
|
17
|
+
var __defNormalProp = (obj, key, value) => (key in obj) ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
18
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
19
|
+
var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
|
|
20
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
21
|
+
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);
|
|
22
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
23
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
24
|
+
var __decoratorStart = (base) => [, , , __create(base?.[__knownSymbol("metadata")] ?? null)];
|
|
25
|
+
var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
|
|
26
|
+
var __expectFn = (fn) => fn !== undefined && typeof fn !== "function" ? __typeError("Function expected") : fn;
|
|
27
|
+
var __decoratorContext = (kind, name, done, metadata, fns) => ({
|
|
28
|
+
kind: __decoratorStrings[kind],
|
|
29
|
+
name,
|
|
30
|
+
metadata,
|
|
31
|
+
addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null))
|
|
32
|
+
});
|
|
33
|
+
var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
|
|
34
|
+
var __runInitializers = (array, flags, self, value) => {
|
|
35
|
+
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length;i < n; i++)
|
|
36
|
+
flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
|
|
37
|
+
return value;
|
|
38
|
+
};
|
|
39
|
+
var __decorateElement = (array, flags, name, decorators, target, extra) => {
|
|
40
|
+
var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
|
|
41
|
+
var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
|
|
42
|
+
var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
|
|
43
|
+
var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : {
|
|
44
|
+
get [name]() {
|
|
45
|
+
return __privateGet(this, extra);
|
|
46
|
+
},
|
|
47
|
+
set [name](x) {
|
|
48
|
+
__privateSet(this, extra, x);
|
|
49
|
+
}
|
|
50
|
+
}, name));
|
|
51
|
+
k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
|
|
52
|
+
for (var i = decorators.length - 1;i >= 0; i--) {
|
|
53
|
+
ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
|
|
54
|
+
if (k) {
|
|
55
|
+
ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => (name in x) };
|
|
56
|
+
if (k ^ 3)
|
|
57
|
+
access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
|
|
58
|
+
if (k > 2)
|
|
59
|
+
access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
|
|
60
|
+
}
|
|
61
|
+
it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? undefined : { get: desc.get, set: desc.set } : target, ctx);
|
|
62
|
+
done._ = 1;
|
|
63
|
+
if (k ^ 4 || it === undefined)
|
|
64
|
+
__expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
|
|
65
|
+
else if (typeof it !== "object" || it === null)
|
|
66
|
+
__typeError("Object expected");
|
|
67
|
+
else
|
|
68
|
+
__expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
|
|
69
|
+
}
|
|
70
|
+
return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/route/claims.ts
|
|
74
|
+
import { AppError } from "@dunx/core";
|
|
75
|
+
|
|
76
|
+
class PathClaims {
|
|
77
|
+
#owners = new Map;
|
|
78
|
+
#noun;
|
|
79
|
+
#remedy;
|
|
80
|
+
constructor(noun, remedy) {
|
|
81
|
+
this.#noun = noun;
|
|
82
|
+
this.#remedy = remedy;
|
|
83
|
+
}
|
|
84
|
+
claim(key, owner) {
|
|
85
|
+
const existing = this.#owners.get(key);
|
|
86
|
+
if (existing !== undefined) {
|
|
87
|
+
throw new AppError(`${this.#noun} collision: ${key} is declared by ${existing} and by ` + `${owner}. ${this.#remedy}`);
|
|
88
|
+
}
|
|
89
|
+
this.#owners.set(key, owner);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
Object.defineProperty(PathClaims, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "noun: string" }, { unresolved: "remedy: string" }] });
|
|
93
|
+
|
|
94
|
+
export { __privateGet, __privateAdd, __decoratorStart, __decoratorMetadata, __runInitializers, __decorateElement, PathClaims };
|
|
@@ -40,6 +40,8 @@ var ROLES2 = metaKey2("roles");
|
|
|
40
40
|
var PUBLIC2 = metaKey2("public");
|
|
41
41
|
var HIDDEN2 = metaKey2("hidden");
|
|
42
42
|
var UNMATCHED2 = metaKey2("unmatched");
|
|
43
|
+
var STREAMS2 = metaKey2("streams");
|
|
44
|
+
var REQUEST_SERVER = metaKey2("request-server");
|
|
43
45
|
var Roles2 = (...roles) => meta2(ROLES2, roles);
|
|
44
46
|
var Public2 = () => meta2(PUBLIC2, true);
|
|
45
47
|
var ApiHidden2 = () => meta2(HIDDEN2, true);
|
|
@@ -90,6 +92,11 @@ var discoverRoutes2 = (instance) => {
|
|
|
90
92
|
};
|
|
91
93
|
|
|
92
94
|
// src/route/prefix.ts
|
|
95
|
+
var normalizePrefix = (path) => {
|
|
96
|
+
const trimmed = path.split("/").filter(Boolean).join("/");
|
|
97
|
+
return trimmed === "" ? "/" : `/${trimmed}`;
|
|
98
|
+
};
|
|
99
|
+
|
|
93
100
|
class RoutePrefix2 {
|
|
94
101
|
#value = "";
|
|
95
102
|
get value() {
|
|
@@ -103,86 +110,4 @@ class RoutePrefix2 {
|
|
|
103
110
|
}
|
|
104
111
|
}
|
|
105
112
|
|
|
106
|
-
|
|
107
|
-
var HANDLER = Symbol.for("dunx.ws.handler");
|
|
108
|
-
var GATEWAY = Symbol.for("dunx.ws.gateway");
|
|
109
|
-
var HandlerKind = Object.freeze({
|
|
110
|
-
UPGRADE: "upgrade",
|
|
111
|
-
OPEN: "open",
|
|
112
|
-
MESSAGE: "message",
|
|
113
|
-
CLOSE: "close",
|
|
114
|
-
DRAIN: "drain",
|
|
115
|
-
PING: "ping",
|
|
116
|
-
PONG: "pong"
|
|
117
|
-
});
|
|
118
|
-
var markHandler = (target, meta) => {
|
|
119
|
-
Object.defineProperty(target, HANDLER, { value: meta, configurable: true });
|
|
120
|
-
};
|
|
121
|
-
var handlerMetaOf = (value) => typeof value === "function" ? value[HANDLER] : undefined;
|
|
122
|
-
var markGateway = (target, path) => {
|
|
123
|
-
Object.defineProperty(target, GATEWAY, { value: path, configurable: true });
|
|
124
|
-
};
|
|
125
|
-
var gatewayPathOf = (target) => target[GATEWAY] ?? "/";
|
|
126
|
-
var isGateway2 = (target) => target[GATEWAY] !== undefined;
|
|
127
|
-
|
|
128
|
-
// src/server/context.ts
|
|
129
|
-
var EMPTY = new Map;
|
|
130
|
-
var buildContext2 = (route) => {
|
|
131
|
-
const record = route.meta ?? EMPTY;
|
|
132
|
-
return Object.freeze({
|
|
133
|
-
controller: route.controller,
|
|
134
|
-
handler: route.handlerName,
|
|
135
|
-
method: route.method,
|
|
136
|
-
path: route.path,
|
|
137
|
-
parsesBody: route.options?.body !== undefined,
|
|
138
|
-
get: (key) => record.get(key.id)
|
|
139
|
-
});
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
// src/ws/discover.ts
|
|
143
|
-
import {
|
|
144
|
-
AppError,
|
|
145
|
-
classOf,
|
|
146
|
-
markedMethods as markedMethods2
|
|
147
|
-
} from "@dunx/core";
|
|
148
|
-
var normalizePath = (path) => {
|
|
149
|
-
const joined = `/${path}`.replace(/\/{2,}/g, "/");
|
|
150
|
-
return joined.length > 1 ? joined.replace(/\/$/, "") : "/";
|
|
151
|
-
};
|
|
152
|
-
var eachHandler = (start) => markedMethods2(start, handlerMetaOf);
|
|
153
|
-
var discoverGateway = (instance) => {
|
|
154
|
-
const klass = instance.constructor;
|
|
155
|
-
const members = instance;
|
|
156
|
-
return {
|
|
157
|
-
name: klass.name,
|
|
158
|
-
path: normalizePath(gatewayPathOf(klass)),
|
|
159
|
-
handlers: eachHandler(Object.getPrototypeOf(instance)).map(({ name, meta }) => ({
|
|
160
|
-
kind: meta.kind,
|
|
161
|
-
event: meta.event,
|
|
162
|
-
method: name,
|
|
163
|
-
invoke: members[name].bind(instance)
|
|
164
|
-
}))
|
|
165
|
-
};
|
|
166
|
-
};
|
|
167
|
-
var findHandlerMethod = (ctor) => eachHandler(ctor.prototype)[0]?.name;
|
|
168
|
-
var discoverGateways = (modules, resolve) => {
|
|
169
|
-
const discovered = [];
|
|
170
|
-
for (const module of modules) {
|
|
171
|
-
for (const entry of module.options.providers ?? []) {
|
|
172
|
-
const candidate = classOf(entry);
|
|
173
|
-
if (!candidate)
|
|
174
|
-
continue;
|
|
175
|
-
if (isGateway2(candidate.ctor)) {
|
|
176
|
-
discovered.push(discoverGateway(resolve(candidate.token)));
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
const orphan = findHandlerMethod(candidate.ctor);
|
|
180
|
-
if (orphan !== undefined) {
|
|
181
|
-
throw new AppError(`${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.");
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return discovered;
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
export { defaultStatusFor2, markRoute, markController, metaKey2, meta2, ROLES2, PUBLIC2, HIDDEN2, UNMATCHED2, Roles2, Public2, ApiHidden2, UseGuards2, metaOf2, mergeMeta2, joinPath2, discoverRoutes2, RoutePrefix2, HandlerKind, markHandler, markGateway, isGateway2, discoverGateway, discoverGateways, buildContext2 };
|
|
113
|
+
export { defaultStatusFor2, markRoute, markController, metaKey2, meta2, ROLES2, PUBLIC2, HIDDEN2, UNMATCHED2, STREAMS2, REQUEST_SERVER, Roles2, Public2, ApiHidden2, UseGuards2, metaOf2, mergeMeta2, joinPath2, discoverRoutes2, normalizePrefix, RoutePrefix2 };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
normalizePrefix
|
|
4
|
+
} from "./chunk-1jt27yka.js";
|
|
5
|
+
|
|
6
|
+
// src/ws/marker.ts
|
|
7
|
+
var HANDLER = Symbol.for("dunx.ws.handler");
|
|
8
|
+
var GATEWAY = Symbol.for("dunx.ws.gateway");
|
|
9
|
+
var HandlerKind = Object.freeze({
|
|
10
|
+
UPGRADE: "upgrade",
|
|
11
|
+
OPEN: "open",
|
|
12
|
+
MESSAGE: "message",
|
|
13
|
+
CLOSE: "close",
|
|
14
|
+
DRAIN: "drain",
|
|
15
|
+
PING: "ping",
|
|
16
|
+
PONG: "pong"
|
|
17
|
+
});
|
|
18
|
+
var markHandler = (target, meta) => {
|
|
19
|
+
Object.defineProperty(target, HANDLER, { value: meta, configurable: true });
|
|
20
|
+
};
|
|
21
|
+
var handlerMetaOf = (value) => typeof value === "function" ? value[HANDLER] : undefined;
|
|
22
|
+
var markGateway = (target, path) => {
|
|
23
|
+
Object.defineProperty(target, GATEWAY, { value: path, configurable: true });
|
|
24
|
+
};
|
|
25
|
+
var gatewayPathOf = (target) => target[GATEWAY] ?? "/";
|
|
26
|
+
var isGateway2 = (target) => target[GATEWAY] !== undefined;
|
|
27
|
+
|
|
28
|
+
// src/server/context.ts
|
|
29
|
+
var EMPTY = new Map;
|
|
30
|
+
var buildContext2 = (route) => {
|
|
31
|
+
const record = route.meta ?? EMPTY;
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
controller: route.controller,
|
|
34
|
+
handler: route.handlerName,
|
|
35
|
+
method: route.method,
|
|
36
|
+
path: route.path,
|
|
37
|
+
parsesBody: route.options?.body !== undefined,
|
|
38
|
+
get: (key) => record.get(key.id)
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/static/options.ts
|
|
43
|
+
class StaticOptions2 {
|
|
44
|
+
root;
|
|
45
|
+
path;
|
|
46
|
+
maxAge;
|
|
47
|
+
immutable;
|
|
48
|
+
constructor(init) {
|
|
49
|
+
this.root = init.root;
|
|
50
|
+
this.path = normalizePrefix(init.path ?? "/");
|
|
51
|
+
this.maxAge = init.maxAge ?? 60;
|
|
52
|
+
this.immutable = init.immutable ?? (() => false);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
Object.defineProperty(StaticOptions2, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: StaticOptionsInit" }] });
|
|
56
|
+
|
|
57
|
+
// src/static/files.ts
|
|
58
|
+
import { join, normalize, resolve } from "path";
|
|
59
|
+
var IMMUTABLE_CACHE_CONTROL2 = "public, max-age=31536000, immutable";
|
|
60
|
+
|
|
61
|
+
class StaticFiles2 {
|
|
62
|
+
#options;
|
|
63
|
+
#root;
|
|
64
|
+
#prefix;
|
|
65
|
+
constructor(options) {
|
|
66
|
+
this.#options = options;
|
|
67
|
+
this.#root = resolve(options.root);
|
|
68
|
+
this.#prefix = options.path === "/" ? "/" : `${options.path}/`;
|
|
69
|
+
}
|
|
70
|
+
resolvePath(pathname) {
|
|
71
|
+
const relative = pathname.startsWith(this.#prefix) ? pathname.slice(this.#prefix.length) : pathname.slice(this.#options.path.length);
|
|
72
|
+
let decoded;
|
|
73
|
+
try {
|
|
74
|
+
decoded = decodeURIComponent(relative);
|
|
75
|
+
} catch {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (decoded.includes("\x00"))
|
|
79
|
+
return;
|
|
80
|
+
const candidate = resolve(join(this.#root, normalize(decoded)));
|
|
81
|
+
if (candidate !== this.#root && !candidate.startsWith(`${this.#root}/`)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
return candidate;
|
|
85
|
+
}
|
|
86
|
+
#cacheControl(pathname) {
|
|
87
|
+
const { immutable, maxAge } = this.#options;
|
|
88
|
+
return immutable(pathname) ? IMMUTABLE_CACHE_CONTROL2 : `public, max-age=${maxAge}`;
|
|
89
|
+
}
|
|
90
|
+
async handle(req, _ctx, next) {
|
|
91
|
+
const { pathname } = new URL(req.url);
|
|
92
|
+
if (pathname !== this.#options.path && !pathname.startsWith(this.#prefix)) {
|
|
93
|
+
return next();
|
|
94
|
+
}
|
|
95
|
+
if (req.method !== "GET" && req.method !== "HEAD")
|
|
96
|
+
return next();
|
|
97
|
+
const path = this.resolvePath(pathname);
|
|
98
|
+
if (path === undefined)
|
|
99
|
+
return next();
|
|
100
|
+
const file = Bun.file(path);
|
|
101
|
+
if (!await file.exists())
|
|
102
|
+
return next();
|
|
103
|
+
return new Response(file, {
|
|
104
|
+
headers: {
|
|
105
|
+
"cache-control": this.#cacheControl(pathname),
|
|
106
|
+
...file.type === "" ? { "content-type": "application/octet-stream" } : {},
|
|
107
|
+
"x-content-type-options": "nosniff"
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
Object.defineProperty(StaticFiles2, Symbol.for("dunx.deps"), { value: () => [StaticOptions2] });
|
|
113
|
+
|
|
114
|
+
// src/ws/discover.ts
|
|
115
|
+
import {
|
|
116
|
+
AppError,
|
|
117
|
+
classOf,
|
|
118
|
+
markedMethods
|
|
119
|
+
} from "@dunx/core";
|
|
120
|
+
var normalizePath = (path) => {
|
|
121
|
+
const joined = `/${path}`.replace(/\/{2,}/g, "/");
|
|
122
|
+
return joined.length > 1 ? joined.replace(/\/$/, "") : "/";
|
|
123
|
+
};
|
|
124
|
+
var eachHandler = (start) => markedMethods(start, handlerMetaOf);
|
|
125
|
+
var discoverGateway = (instance) => {
|
|
126
|
+
const klass = instance.constructor;
|
|
127
|
+
const members = instance;
|
|
128
|
+
return {
|
|
129
|
+
name: klass.name,
|
|
130
|
+
path: normalizePath(gatewayPathOf(klass)),
|
|
131
|
+
handlers: eachHandler(Object.getPrototypeOf(instance)).map(({ name, meta }) => ({
|
|
132
|
+
kind: meta.kind,
|
|
133
|
+
event: meta.event,
|
|
134
|
+
method: name,
|
|
135
|
+
invoke: members[name].bind(instance)
|
|
136
|
+
}))
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
var findHandlerMethod = (ctor) => eachHandler(ctor.prototype)[0]?.name;
|
|
140
|
+
var discoverGateways = (modules, resolve) => {
|
|
141
|
+
const discovered = [];
|
|
142
|
+
for (const module of modules) {
|
|
143
|
+
for (const entry of module.options.providers ?? []) {
|
|
144
|
+
const candidate = classOf(entry);
|
|
145
|
+
if (!candidate)
|
|
146
|
+
continue;
|
|
147
|
+
if (isGateway2(candidate.ctor)) {
|
|
148
|
+
discovered.push(discoverGateway(resolve(candidate.token)));
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const orphan = findHandlerMethod(candidate.ctor);
|
|
152
|
+
if (orphan !== undefined) {
|
|
153
|
+
throw new AppError(`${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.");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return discovered;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export { HandlerKind, markHandler, markGateway, isGateway2, discoverGateway, discoverGateways, buildContext2, StaticOptions2, IMMUTABLE_CACHE_CONTROL2, StaticFiles2 };
|
package/dist/client/service.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Logger, RequestContext } from '@dunx/core';
|
|
2
2
|
import { UrlHelper, type ParamsType } from '@arkv/shared';
|
|
3
3
|
import type { HttpMethod } from '../route/marker.js';
|
|
4
|
+
import { type SseMessage } from './sse.js';
|
|
4
5
|
import { HttpClientOptions } from './options.js';
|
|
5
6
|
import { type HttpRetryOptions } from './retry.js';
|
|
6
7
|
/** The client speaks two more verbs than a route can declare. */
|
|
@@ -41,6 +42,10 @@ export interface RequestConfig<TRequest = unknown, TResponse = unknown> {
|
|
|
41
42
|
readonly signal?: AbortSignal;
|
|
42
43
|
}
|
|
43
44
|
type BaseOptions<TRequest, TResponse> = Omit<RequestConfig<TRequest, TResponse>, 'method' | 'url' | 'payload'>;
|
|
45
|
+
/** What both SSE readers take: no retry, and only the verbs a stream uses. */
|
|
46
|
+
type SseConfig<TRequest> = Omit<RequestConfig<TRequest>, 'method' | 'retry'> & {
|
|
47
|
+
readonly method?: 'GET' | 'POST';
|
|
48
|
+
};
|
|
44
49
|
/**
|
|
45
50
|
* A `fetch` client with a per-request timeout, retry with backoff, W3C Trace
|
|
46
51
|
* Context propagation and one log line per call. `fetch` and nothing else, so there is no
|
|
@@ -61,21 +66,19 @@ export declare class HttpService extends UrlHelper {
|
|
|
61
66
|
put<TRequest = unknown, TResponse = unknown>(url?: string | URL, payload?: TRequest, options?: BaseOptions<TRequest, TResponse>): Promise<TResponse>;
|
|
62
67
|
patch<TRequest = unknown, TResponse = unknown>(url?: string | URL, payload?: TRequest, options?: BaseOptions<TRequest, TResponse>): Promise<TResponse>;
|
|
63
68
|
delete<TResponse = unknown>(url?: string | URL, options?: BaseOptions<never, TResponse>): Promise<TResponse>;
|
|
69
|
+
/** Each `data:` payload of an event stream, `[DONE]` consumed rather than
|
|
70
|
+
* yielded. {@link streamSseEvents} keeps the envelope too. */
|
|
71
|
+
streamSse<TRequest = unknown>(config: SseConfig<TRequest>): AsyncGenerator<string>;
|
|
64
72
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
73
|
+
* The same response, each event whole: the joined `data` plus whatever `event`,
|
|
74
|
+
* `id` and `retry` it carried. `id` is what a reconnect sends as `Last-Event-ID`.
|
|
67
75
|
*
|
|
68
76
|
* **No retry**, deliberately: a partially consumed stream cannot be replayed, so
|
|
69
77
|
* retrying would re-deliver events the caller has already seen. The timeout
|
|
70
|
-
* covers the connect only -
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* Hand-rolled rather than delegated: Bun exposes no `EventSource` global and no
|
|
74
|
-
* SSE parser, which was measured rather than assumed.
|
|
78
|
+
* covers the connect only - dropped once headers arrive, or a long-lived stream
|
|
79
|
+
* would be cut off mid-flight.
|
|
75
80
|
*/
|
|
76
|
-
|
|
77
|
-
readonly method?: 'GET' | 'POST';
|
|
78
|
-
}): AsyncGenerator<string>;
|
|
81
|
+
streamSseEvents<TRequest = unknown>(config: SseConfig<TRequest>): AsyncGenerator<SseMessage>;
|
|
79
82
|
/**
|
|
80
83
|
* Resolves the target: an absolute url, a path relative to `baseUrl`, or
|
|
81
84
|
* `baseUrl` plus an explicit `path`.
|
package/dist/client/sse.d.ts
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
|
+
/** One dispatched event as it arrived. Not `SseEvent`, the write side, where
|
|
2
|
+
* `data` is any value rather than the text off the wire. */
|
|
3
|
+
export interface SseMessage {
|
|
4
|
+
/** The `data:` lines of one event, joined with `\n` as the spec requires. */
|
|
5
|
+
readonly data: string;
|
|
6
|
+
/** The `event:` name, absent for the default `message`. */
|
|
7
|
+
readonly event?: string;
|
|
8
|
+
readonly id?: string;
|
|
9
|
+
/** The reconnection delay the server asked for, in milliseconds. */
|
|
10
|
+
readonly retry?: number;
|
|
11
|
+
}
|
|
1
12
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Async iteration rather than `getReader()`: it acquires the reader and releases
|
|
6
|
-
* it on completion, on a `break` in the consumer, and on the `[DONE]` return,
|
|
7
|
-
* which is the case the manual form needed a `releaseLock()` in a `finally` for.
|
|
13
|
+
* Every event of a server-sent-events body, in order, ending with the stream or
|
|
14
|
+
* with `[DONE]`. One still being read when the body ends is dropped, per spec.
|
|
8
15
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
16
|
+
* Async iteration rather than `getReader()`, which releases the reader on
|
|
17
|
+
* completion, on a consumer `break` and on the `[DONE]` return. Hand-rolled:
|
|
18
|
+
* Bun exposes no `EventSource` global and no SSE parser, measured not assumed.
|
|
19
|
+
*/
|
|
20
|
+
export declare function sseMessages(body: ReadableStream<Uint8Array>): AsyncGenerator<SseMessage>;
|
|
21
|
+
/**
|
|
22
|
+
* The `data:` payloads alone, one string per event rather than per line, so a
|
|
23
|
+
* multi-line payload arrives as it was sent.
|
|
11
24
|
*/
|
|
12
25
|
export declare function sseData(body: ReadableStream<Uint8Array>): AsyncGenerator<string>;
|
|
13
26
|
/**
|
package/dist/client.d.ts
CHANGED
|
@@ -17,3 +17,5 @@ export type { BackoffOptions, RetryOptions } from '@dunx/core';
|
|
|
17
17
|
export { HttpRetryClassifier, type HttpRetryOptions } from './client/retry.js';
|
|
18
18
|
export { httpClient, HttpModule, type ClientTarget } from './client/module.js';
|
|
19
19
|
export { HttpService, type HeaderFactory, type RequestConfig, type RequestMethod, } from './client/service.js';
|
|
20
|
+
/** What `streamSseEvents` yields: one dispatched event, envelope included. */
|
|
21
|
+
export type { SseMessage } from './client/sse.js';
|
package/dist/client.js
CHANGED
|
@@ -159,28 +159,61 @@ var readBody = async (response) => {
|
|
|
159
159
|
};
|
|
160
160
|
|
|
161
161
|
// src/client/sse.ts
|
|
162
|
-
|
|
162
|
+
var LINE = /\r\n|\r|\n/;
|
|
163
|
+
var split = (line) => {
|
|
164
|
+
const colon = line.indexOf(":");
|
|
165
|
+
if (colon === -1)
|
|
166
|
+
return [line, ""];
|
|
167
|
+
const value = line.slice(colon + 1);
|
|
168
|
+
return [line.slice(0, colon), value.startsWith(" ") ? value.slice(1) : value];
|
|
169
|
+
};
|
|
170
|
+
async function* sseMessages(body) {
|
|
163
171
|
const decoder = new TextDecoder;
|
|
164
172
|
let buffer = "";
|
|
173
|
+
let data = [];
|
|
174
|
+
let event;
|
|
175
|
+
let id;
|
|
176
|
+
let retry;
|
|
165
177
|
for await (const chunk of body) {
|
|
166
178
|
buffer += decoder.decode(chunk, { stream: true });
|
|
167
|
-
let
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
179
|
+
let end = LINE.exec(buffer);
|
|
180
|
+
while (end !== null) {
|
|
181
|
+
const line = buffer.slice(0, end.index);
|
|
182
|
+
buffer = buffer.slice(end.index + end[0].length);
|
|
183
|
+
end = LINE.exec(buffer);
|
|
184
|
+
if (line === "") {
|
|
185
|
+
const payload = data.join(`
|
|
173
186
|
`);
|
|
174
|
-
|
|
187
|
+
data = [];
|
|
188
|
+
if (payload === "") {
|
|
189
|
+
event = undefined;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (payload === "[DONE]")
|
|
193
|
+
return;
|
|
194
|
+
yield {
|
|
195
|
+
data: payload,
|
|
196
|
+
...event === undefined ? {} : { event },
|
|
197
|
+
...id === undefined ? {} : { id },
|
|
198
|
+
...retry === undefined ? {} : { retry }
|
|
199
|
+
};
|
|
200
|
+
event = undefined;
|
|
175
201
|
continue;
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
202
|
+
}
|
|
203
|
+
if (line.startsWith(":"))
|
|
204
|
+
continue;
|
|
205
|
+
const [field, value] = split(line);
|
|
206
|
+
if (field === "data")
|
|
207
|
+
data.push(value);
|
|
208
|
+
else if (field === "event")
|
|
209
|
+
event = value;
|
|
210
|
+
else if (field === "id" && !value.includes("\x00"))
|
|
211
|
+
id = value;
|
|
212
|
+
else if (field === "retry" && /^\d+$/.test(value))
|
|
213
|
+
retry = Number(value);
|
|
180
214
|
}
|
|
181
215
|
}
|
|
182
216
|
}
|
|
183
|
-
|
|
184
217
|
class ConnectDeadline {
|
|
185
218
|
#controller = new AbortController;
|
|
186
219
|
#timer;
|
|
@@ -297,6 +330,10 @@ class HttpService extends UrlHelper {
|
|
|
297
330
|
});
|
|
298
331
|
}
|
|
299
332
|
async* streamSse(config) {
|
|
333
|
+
for await (const message of this.streamSseEvents(config))
|
|
334
|
+
yield message.data;
|
|
335
|
+
}
|
|
336
|
+
async* streamSseEvents(config) {
|
|
300
337
|
const url = this.urlFor(config);
|
|
301
338
|
const method = config.method ?? "POST";
|
|
302
339
|
const startedAt = Date.now();
|
|
@@ -304,9 +341,7 @@ class HttpService extends UrlHelper {
|
|
|
304
341
|
const deadline = new ConnectDeadline(config.timeoutMs ?? this.options.timeoutMs, url.href);
|
|
305
342
|
let response;
|
|
306
343
|
try {
|
|
307
|
-
const policy = this.policyFor({ ...config, timeoutMs: 0 }, {
|
|
308
|
-
maxRetries: 0
|
|
309
|
-
});
|
|
344
|
+
const policy = this.policyFor({ ...config, timeoutMs: 0 }, { maxRetries: 0 });
|
|
310
345
|
response = await policy.run((signal) => this.send({ ...config, method }, url, body, serialised, AbortSignal.any([signal, deadline.signal]), "text/event-stream"));
|
|
311
346
|
} finally {
|
|
312
347
|
deadline.clear();
|
|
@@ -317,7 +352,7 @@ class HttpService extends UrlHelper {
|
|
|
317
352
|
}), { method, url: url.href, headers: response.headers });
|
|
318
353
|
}
|
|
319
354
|
try {
|
|
320
|
-
yield*
|
|
355
|
+
yield* sseMessages(response.body);
|
|
321
356
|
} finally {
|
|
322
357
|
this.logger.debug(`SSE ${method} ${url.href} closed`, {
|
|
323
358
|
elapsedMs: Date.now() - startedAt
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BunRequest } from 'bun';
|
|
2
|
+
import type { RouteContext } from '../server/context.js';
|
|
3
|
+
import type { ClaimsPaths, Middleware, Next } from '../server/middleware.js';
|
|
4
|
+
import { ConnectRegistry } from './registry.js';
|
|
5
|
+
/**
|
|
6
|
+
* Serves every registered RPC as ordinary middleware, so request logging, CORS,
|
|
7
|
+
* a guard and the dashboard apply to a call as they apply to a route. Register
|
|
8
|
+
* it with `app.use`, since position in the chain decides what covers it.
|
|
9
|
+
*
|
|
10
|
+
* RPC paths are in no route table, so they reach the `fetch` fallback, where
|
|
11
|
+
* `ctx.get(UNMATCHED)` is true and `ctx.path` is already parsed. Reading it
|
|
12
|
+
* first is what leaves a matched route paying nothing. Anything outside the
|
|
13
|
+
* registered paths falls through untouched.
|
|
14
|
+
*
|
|
15
|
+
* `ThrottleGuard` is the one that does **not** cover an RPC: it returns early on
|
|
16
|
+
* every unmatched path so a burst of 404s cannot spend a caller's budget, and an
|
|
17
|
+
* RPC is unmatched. See docs/guide/27-rpc.md.
|
|
18
|
+
*/
|
|
19
|
+
export declare class ConnectMiddleware implements Middleware, ClaimsPaths {
|
|
20
|
+
#private;
|
|
21
|
+
constructor(registry: ConnectRegistry);
|
|
22
|
+
/** Every mounted RPC path, so a controller cannot shadow one unnoticed. */
|
|
23
|
+
claimedPaths(): readonly string[];
|
|
24
|
+
handle(req: BunRequest, ctx: RouteContext, next: Next): Promise<Response>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type AsyncModuleConfig, type Deps, type DynamicModule } from '@dunx/core';
|
|
2
|
+
import { type ConnectOptionsInit, type ConnectServiceRegistration } from './options.js';
|
|
3
|
+
/** Everything `forRoot` takes except the services, which `forRootAsync` needs
|
|
4
|
+
* synchronously, and `imports`, which `AsyncModuleConfig` already carries. */
|
|
5
|
+
export type ConnectSettings = Omit<ConnectOptionsInit, 'services' | 'imports'>;
|
|
6
|
+
/**
|
|
7
|
+
* Serves protobuf services over Connect and gRPC-Web on the port `Bun.serve`
|
|
8
|
+
* already has. See `docs/guide/27-rpc.md`.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* ConnectModule.forRoot({
|
|
12
|
+
* services: [connectService(GreetService, GreetRpc)],
|
|
13
|
+
* // What GreetRpc injects: this module is its own scope.
|
|
14
|
+
* imports: [GreetingsModule],
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* It binds `ConnectMiddleware` and does not register it - position in the chain
|
|
19
|
+
* decides which guards cover an RPC, so the app calls `app.use`.
|
|
20
|
+
*/
|
|
21
|
+
export declare class ConnectModule {
|
|
22
|
+
static forRoot(init: ConnectOptionsInit): DynamicModule;
|
|
23
|
+
/**
|
|
24
|
+
* `forRoot` with everything but the services behind a factory, so the prefix
|
|
25
|
+
* or the read limits can come off `ConfigService`. The services are positional
|
|
26
|
+
* because their classes have to be providers before any factory runs.
|
|
27
|
+
*/
|
|
28
|
+
static forRootAsync<const D extends Deps>(services: readonly ConnectServiceRegistration[], config: AsyncModuleConfig<ConnectSettings, D>): DynamicModule;
|
|
29
|
+
}
|