@dunx/http 3.5.1 → 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 +14 -4
- 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/json.d.ts +12 -11
- package/dist/client/options.d.ts +3 -3
- package/dist/client/retry.d.ts +17 -31
- package/dist/client/service.d.ts +21 -12
- package/dist/client/sse.d.ts +39 -0
- package/dist/client.d.ts +10 -1
- package/dist/client.js +166 -108
- 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 +9 -1
- 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,13 +62,16 @@ 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
|
-
| Metrics | Per-route counts and timings, off by default | [Metrics](../../docs/guide/
|
|
68
|
-
| Health and draining | `/health/live`, `/health/ready`, readiness during a rollout | [Health checks](../../docs/guide/
|
|
68
|
+
| Metrics | Per-route counts and timings, off by default | [Metrics](../../docs/guide/23-metrics.md) |
|
|
69
|
+
| Health and draining | `/health/live`, `/health/ready`, readiness during a rollout | [Health checks](../../docs/guide/21-health-checks.md) |
|
|
69
70
|
| Throttling | `@Throttle`, `@SkipThrottle`, memory and Redis counters | [Middleware and guards](../../docs/guide/08-middleware-and-guards.md) |
|
|
70
|
-
|
|
|
71
|
-
|
|
|
71
|
+
| Outbound resilience | `HttpRetryClassifier`: which statuses retry, and `Retry-After` | [Resilience](../../docs/guide/25-resilience.md) |
|
|
72
|
+
| Static files | `Bun.file` behind a mount, with a cache policy | [Deployment](../../docs/guide/20-deployment.md) |
|
|
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) |
|
|
72
75
|
|
|
73
76
|
## Subpaths
|
|
74
77
|
|
|
@@ -76,6 +79,7 @@ The guide is canonical for every row; this table is the index.
|
|
|
76
79
|
| ---------------------- | ----------------------------------------------------------------- |
|
|
77
80
|
| `@dunx/http` | Everything above |
|
|
78
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 |
|
|
79
83
|
| `@dunx/http/internal` | The framework's own plumbing. No stability promise |
|
|
80
84
|
|
|
81
85
|
`@dunx/http/internal` holds route-table construction, the middleware fold, the
|
|
@@ -100,6 +104,12 @@ from, and it may change in any release.
|
|
|
100
104
|
ns. W3C Trace Context is the only correlation id; there is no second one.
|
|
101
105
|
- `metrics: true` adds per-route counts and a nanosecond histogram at +35.2 ns a
|
|
102
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.
|
|
103
113
|
|
|
104
114
|
## License
|
|
105
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/json.d.ts
CHANGED
|
@@ -8,17 +8,7 @@
|
|
|
8
8
|
* the caller and should say so.
|
|
9
9
|
*/
|
|
10
10
|
export declare const safeStringify: (value: unknown) => string;
|
|
11
|
-
|
|
12
|
-
* A plain object: `{}`, `Object.create(null)`, or a JSON-parsed value. Anything
|
|
13
|
-
* with its own prototype - `Date`, `Map`, `Error`, a class instance - is not one.
|
|
14
|
-
*
|
|
15
|
-
* The prototype check rather than the reference's `typeof === 'object' && !Array
|
|
16
|
-
* && !(instanceof Error)`, which answered `true` for a `Date` and for every class
|
|
17
|
-
* instance, so "is this a plain object" did not mean what it said. Body routing
|
|
18
|
-
* does not use this - see {@link isJsonBody} - so tightening it changes no
|
|
19
|
-
* behaviour beyond making the predicate honest.
|
|
20
|
-
*/
|
|
21
|
-
export declare const isPlainObject: (value: unknown) => value is Record<string, unknown>;
|
|
11
|
+
export { isPlainObject } from '@dunx/core';
|
|
22
12
|
/**
|
|
23
13
|
* Whether a payload should be JSON-encoded, or handed to `fetch` as-is.
|
|
24
14
|
*
|
|
@@ -32,3 +22,14 @@ export declare const isPlainObject: (value: unknown) => value is Record<string,
|
|
|
32
22
|
* JSON-encodable; a `Blob` is neither.
|
|
33
23
|
*/
|
|
34
24
|
export declare const isJsonBody: (payload: unknown) => boolean;
|
|
25
|
+
/**
|
|
26
|
+
* JSON when the upstream said so or the body parses; text otherwise; undefined
|
|
27
|
+
* for empty.
|
|
28
|
+
*
|
|
29
|
+
* **A failed read rejects.** It used to be caught and reported as an empty body,
|
|
30
|
+
* so a 2xx whose body died mid-stream reached the caller as a success with no
|
|
31
|
+
* data, and a retry policy saw nothing to retry. A caller that wants the old
|
|
32
|
+
* behaviour asks for it: the two error paths in `HttpService` do, because there
|
|
33
|
+
* the status is the signal and an unreadable body should not replace it.
|
|
34
|
+
*/
|
|
35
|
+
export declare const readBody: (response: Response) => Promise<unknown>;
|
package/dist/client/options.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HttpRetryOptions } from './retry.js';
|
|
2
2
|
/**
|
|
3
3
|
* Named `HttpClientOptions`, not `HttpOptions`: the server half already exports
|
|
4
4
|
* that from `@dunx/http` for `HttpFactory.create`, and two things called
|
|
@@ -15,7 +15,7 @@ export interface HttpClientOptionsInit {
|
|
|
15
15
|
readonly timeoutMs?: number;
|
|
16
16
|
/** Sent on every request, under anything a call sets itself. */
|
|
17
17
|
readonly headers?: Readonly<Record<string, string>>;
|
|
18
|
-
readonly retry?:
|
|
18
|
+
readonly retry?: HttpRetryOptions;
|
|
19
19
|
/**
|
|
20
20
|
* Forward W3C Trace Context upstream as `traceparent`, so the callee's spans
|
|
21
21
|
* join this request's trace and one trace spans both services.
|
|
@@ -71,7 +71,7 @@ export declare class HttpClientOptions {
|
|
|
71
71
|
readonly baseUrl: string | undefined;
|
|
72
72
|
readonly timeoutMs: number;
|
|
73
73
|
readonly headers: Readonly<Record<string, string>>;
|
|
74
|
-
readonly retry:
|
|
74
|
+
readonly retry: HttpRetryOptions;
|
|
75
75
|
readonly propagateTrace: boolean;
|
|
76
76
|
readonly name: string | undefined;
|
|
77
77
|
readonly fetchOptions: Readonly<Record<string, unknown>>;
|
package/dist/client/retry.d.ts
CHANGED
|
@@ -1,49 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
/** Base delay, doubled each attempt. */
|
|
3
|
-
readonly baseMs: number;
|
|
4
|
-
/** @default 2 */
|
|
5
|
-
readonly power?: number;
|
|
6
|
-
/** Upper bound of the random component added to each delay. @default 1000 */
|
|
7
|
-
readonly jitterMs?: number;
|
|
8
|
-
/** @default 30000 */
|
|
9
|
-
readonly maxMs?: number;
|
|
10
|
-
}
|
|
11
|
-
/** `base * power^attempt + jitter`, capped. `attempt` is 0 for the first retry. */
|
|
12
|
-
export declare const backoffDelay: (attempt: number, { baseMs, power, jitterMs, maxMs }: BackoffOptions) => number;
|
|
1
|
+
import { RetryClassifier, type RetryOptions, type RetryVerdict } from '@dunx/core';
|
|
13
2
|
/**
|
|
14
3
|
* The wait an upstream asked for, in ms, or undefined.
|
|
15
4
|
*
|
|
16
5
|
* RFC 9110 allows either a delay in seconds or an HTTP date, and both appear in
|
|
17
|
-
* the wild
|
|
18
|
-
*
|
|
19
|
-
* told you exactly how long to wait.
|
|
6
|
+
* the wild: GitHub sends seconds, some CDNs send a date. Ignoring the header means
|
|
7
|
+
* retrying straight back into a rate limit that had just said how long to wait.
|
|
20
8
|
*/
|
|
21
9
|
export declare const retryAfterMs: (headers: Headers, now?: number) => number | undefined;
|
|
22
10
|
/**
|
|
23
11
|
* Statuses worth trying again: a server that failed, one that is overloaded, and
|
|
24
|
-
* one that timed out.
|
|
25
|
-
*
|
|
26
|
-
* it again unchanged gets the same answer.
|
|
12
|
+
* one that timed out. Narrower than 409 and 422, which are the server rejecting
|
|
13
|
+
* the *request* - sending it again unchanged gets the same answer.
|
|
27
14
|
*/
|
|
28
15
|
export declare const isRetryableStatus: (status: number) => boolean;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
readonly maxRetries?: number;
|
|
32
|
-
/** @default 1000 */
|
|
33
|
-
readonly retryDelayMs?: number;
|
|
34
|
-
readonly backoff?: Omit<BackoffOptions, 'baseMs'>;
|
|
16
|
+
/** Core's generic retry knobs plus the two an HTTP failure carries. */
|
|
17
|
+
export interface HttpRetryOptions<T = unknown> extends RetryOptions<T> {
|
|
35
18
|
/** @default isRetryableStatus */
|
|
36
19
|
readonly shouldRetryOnStatus?: (status: number) => boolean;
|
|
37
20
|
/** Honour a `Retry-After` header over the computed backoff. @default true */
|
|
38
21
|
readonly respectRetryAfter?: boolean;
|
|
39
|
-
readonly onAttempt?: (attempt: number, isRetry: boolean) => void;
|
|
40
|
-
readonly onError?: (error: unknown, attempt: number, willRetry: boolean) => void;
|
|
41
|
-
readonly onSuccess?: (result: T, attempt: number) => void;
|
|
42
22
|
}
|
|
43
23
|
/**
|
|
44
|
-
*
|
|
24
|
+
* The HTTP half of the retry decision, and the only place in dunx's resilience
|
|
25
|
+
* path that knows what a status is.
|
|
45
26
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
27
|
+
* An abort is never retried: the caller's signal fired or the timeout expired, and
|
|
28
|
+
* both mean the budget for this call is spent. A transport failure is retried,
|
|
29
|
+
* because a refused connection is the case retrying exists for.
|
|
48
30
|
*/
|
|
49
|
-
export declare
|
|
31
|
+
export declare class HttpRetryClassifier extends RetryClassifier {
|
|
32
|
+
private readonly options;
|
|
33
|
+
constructor(options?: HttpRetryOptions);
|
|
34
|
+
classify(error: unknown): RetryVerdict;
|
|
35
|
+
}
|
package/dist/client/service.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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
|
-
import { type
|
|
6
|
+
import { type HttpRetryOptions } from './retry.js';
|
|
6
7
|
/** The client speaks two more verbs than a route can declare. */
|
|
7
8
|
export type RequestMethod = HttpMethod | 'HEAD' | 'OPTIONS';
|
|
8
9
|
/**
|
|
@@ -36,11 +37,15 @@ export interface RequestConfig<TRequest = unknown, TResponse = unknown> {
|
|
|
36
37
|
readonly headerFactory?: HeaderFactory;
|
|
37
38
|
/** Merged into the async context for this call, so its logs carry it. */
|
|
38
39
|
readonly flow?: string;
|
|
39
|
-
readonly retry?:
|
|
40
|
+
readonly retry?: HttpRetryOptions<TResponse>;
|
|
40
41
|
/** Cancels the call. Combined with the timeout, whichever fires first. */
|
|
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`.
|
|
@@ -86,6 +89,12 @@ export declare class HttpService extends UrlHelper {
|
|
|
86
89
|
* regex, so it cannot disagree with `new URL`.
|
|
87
90
|
*/
|
|
88
91
|
private urlFor;
|
|
92
|
+
/**
|
|
93
|
+
* One policy per call, because the budget and the caller's signal are. Core owns
|
|
94
|
+
* the timeout, the loop and the backoff; `HttpRetryClassifier` is the only part
|
|
95
|
+
* of it that knows what a status is.
|
|
96
|
+
*/
|
|
97
|
+
private policyFor;
|
|
89
98
|
/** `serialised` is what a `headerFactory` signs, and is `''` for no body. */
|
|
90
99
|
private bodyFor;
|
|
91
100
|
private send;
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
}
|
|
12
|
+
/**
|
|
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.
|
|
15
|
+
*
|
|
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.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sseData(body: ReadableStream<Uint8Array>): AsyncGenerator<string>;
|
|
26
|
+
/**
|
|
27
|
+
* A deadline on the connect alone.
|
|
28
|
+
*
|
|
29
|
+
* An `AbortSignal.timeout` handed to `fetch` keeps aborting after the headers
|
|
30
|
+
* arrive, which cut a 600 ms stream at 200 ms when the policy supplied one. This
|
|
31
|
+
* holds its timer so the caller can drop it the moment `fetch` resolves, leaving
|
|
32
|
+
* the body cancellable only by the caller's own signal.
|
|
33
|
+
*/
|
|
34
|
+
export declare class ConnectDeadline {
|
|
35
|
+
#private;
|
|
36
|
+
constructor(ms: number, target: string);
|
|
37
|
+
get signal(): AbortSignal;
|
|
38
|
+
clear(): void;
|
|
39
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -7,6 +7,15 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export { FetchError, FetchTransportError } from './client/errors.js';
|
|
9
9
|
export { HttpClientOptions, type HttpClientOptionsInit, } from './client/options.js';
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Retry, backoff and jitter are `@dunx/core`'s, re-exported so an import of this
|
|
12
|
+
* subpath still names them. What stays here is the HTTP half of the decision:
|
|
13
|
+
* `HttpRetryClassifier` reads a status and a `Retry-After`, which is the seam that
|
|
14
|
+
* keeps both out of core.
|
|
15
|
+
*/
|
|
16
|
+
export type { BackoffOptions, RetryOptions } from '@dunx/core';
|
|
17
|
+
export { HttpRetryClassifier, type HttpRetryOptions } from './client/retry.js';
|
|
11
18
|
export { httpClient, HttpModule, type ClientTarget } from './client/module.js';
|
|
12
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';
|