@lovrozagar/honey 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/README.md +51 -0
- package/docs/api-ref/go.md +399 -0
- package/docs/api-ref/python.md +399 -0
- package/docs/api-ref/rust.md +399 -0
- package/docs/api-ref/typescript.md +399 -0
- package/docs/invalidate-spec.md +303 -0
- package/docs/sdk.md +722 -0
- package/package.json +120 -0
- package/src/accepts.ts +78 -0
- package/src/body-limit.ts +100 -0
- package/src/build/index.ts +174 -0
- package/src/cli-go/auth.go +15 -0
- package/src/cli-go/config.go +165 -0
- package/src/cli-go/errors.go +57 -0
- package/src/cli-go/multipart.go +55 -0
- package/src/cli-go/output.go +109 -0
- package/src/cli-go/root.go +30 -0
- package/src/cli-go/stream.go +88 -0
- package/src/cli-go/version.go +5 -0
- package/src/cli.ts +204 -0
- package/src/client/error.ts +27 -0
- package/src/client/http.ts +502 -0
- package/src/client/index.ts +127 -0
- package/src/client/sdk.ts +265 -0
- package/src/client/sse.ts +102 -0
- package/src/client/types.ts +211 -0
- package/src/client/ws.ts +115 -0
- package/src/client-go/errors.go +207 -0
- package/src/client-go/invalidation.go +373 -0
- package/src/client-go/realtime.go +317 -0
- package/src/client-go/result.go +20 -0
- package/src/client-go/runtime.go +413 -0
- package/src/client-go/sse.go +149 -0
- package/src/client-go/transport.go +452 -0
- package/src/client-go/ws.go +104 -0
- package/src/client-mcp/README.md +10 -0
- package/src/client-mcp/server.ts +94 -0
- package/src/client-mcp/types.ts +19 -0
- package/src/client-python/_errors.py +115 -0
- package/src/client-python/_invalidation.py +578 -0
- package/src/client-python/_realtime.py +156 -0
- package/src/client-python/_runtime.py +469 -0
- package/src/client-python/_sse.py +99 -0
- package/src/client-python/_transport.py +335 -0
- package/src/client-python/_ws.py +131 -0
- package/src/client-python/py.typed +0 -0
- package/src/client-rust/errors.rs +200 -0
- package/src/client-rust/invalidation.rs +441 -0
- package/src/client-rust/invalidation_sync.rs +306 -0
- package/src/client-rust/realtime.rs +695 -0
- package/src/client-rust/result.rs +22 -0
- package/src/client-rust/runtime.rs +547 -0
- package/src/client-rust/runtime_sync.rs +452 -0
- package/src/client-rust/sse.rs +167 -0
- package/src/client-rust/ws.rs +143 -0
- package/src/cloudflare/realtime.ts +236 -0
- package/src/codegen-go-cli.ts +1180 -0
- package/src/codegen-go.ts +1010 -0
- package/src/codegen-hash.ts +8 -0
- package/src/codegen-ir.ts +517 -0
- package/src/codegen-loaders.ts +53 -0
- package/src/codegen-mcp.ts +364 -0
- package/src/codegen-python.ts +1028 -0
- package/src/codegen-rust.ts +1652 -0
- package/src/codegen-sanitize.ts +67 -0
- package/src/codegen-schema-naming.ts +146 -0
- package/src/codegen.ts +5015 -0
- package/src/context.ts +210 -0
- package/src/cookie-sign.ts +58 -0
- package/src/cookie.ts +89 -0
- package/src/cors.ts +96 -0
- package/src/crypto.ts +19 -0
- package/src/csrf.ts +54 -0
- package/src/curl-logger.ts +235 -0
- package/src/detect-runtime.ts +19 -0
- package/src/error.ts +52 -0
- package/src/errors.ts +87 -0
- package/src/etag.ts +74 -0
- package/src/feature-load.ts +23 -0
- package/src/go-type-emitter.ts +404 -0
- package/src/honey-response.ts +354 -0
- package/src/i18n-slot.ts +20 -0
- package/src/i18n.ts +234 -0
- package/src/index.ts +3843 -0
- package/src/init.ts +149 -0
- package/src/input.ts +8 -0
- package/src/ip-restrict.ts +192 -0
- package/src/logger.ts +149 -0
- package/src/middleware.ts +199 -0
- package/src/node-request.ts +240 -0
- package/src/node.ts +212 -0
- package/src/openapi/register.ts +19 -0
- package/src/openapi/scalar.ts +33 -0
- package/src/openapi/spec-factory.ts +46 -0
- package/src/openapi/spec.ts +31 -0
- package/src/openapi/swagger.ts +34 -0
- package/src/plugin.ts +771 -0
- package/src/powered-by.ts +17 -0
- package/src/pretty-json.ts +39 -0
- package/src/proxy.ts +155 -0
- package/src/python-type-emitter.ts +144 -0
- package/src/realtime/buffer.ts +101 -0
- package/src/realtime/bus.ts +93 -0
- package/src/realtime/longpoll.ts +148 -0
- package/src/realtime/protocol.ts +128 -0
- package/src/realtime/route.ts +104 -0
- package/src/realtime/runtime.ts +430 -0
- package/src/request-id.ts +23 -0
- package/src/request-to-curl.ts +34 -0
- package/src/response.ts +414 -0
- package/src/rust-type-emitter.ts +412 -0
- package/src/secure-headers.ts +69 -0
- package/src/serve-register.ts +8 -0
- package/src/serve-slot.ts +20 -0
- package/src/serve.ts +141 -0
- package/src/server-timing.ts +66 -0
- package/src/static.ts +60 -0
- package/src/telemetry/otel.ts +139 -0
- package/src/testing.ts +114 -0
- package/src/timeout.ts +36 -0
- package/src/tree.ts +464 -0
- package/src/ts-type-emitter.ts +106 -0
- package/src/type-emitter.ts +524 -0
- package/src/type-extractor.ts +749 -0
- package/src/types.ts +638 -0
- package/src/validation.ts +300 -0
- package/src/ws/bun.ts +89 -0
- package/src/ws/cloudflare.ts +133 -0
- package/src/ws/deno.ts +109 -0
- package/src/ws/node.ts +161 -0
- package/src/yaml.ts +80 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,3843 @@
|
|
|
1
|
+
import { HoneyContext } from "./context.ts";
|
|
2
|
+
import { HoneyError } from "./error.ts";
|
|
3
|
+
import { ERROR_META } from "./errors.ts";
|
|
4
|
+
import type { MiddlewareFn, RuntimeMiddleware } from "./middleware.ts";
|
|
5
|
+
import { compileChain, executeChain } from "./middleware.ts";
|
|
6
|
+
import type { ProxyConfig } from "./proxy.ts";
|
|
7
|
+
import { createProxyHandler } from "./proxy.ts";
|
|
8
|
+
import type {
|
|
9
|
+
CustomErrorFormatter,
|
|
10
|
+
ErrorFormatter,
|
|
11
|
+
ResponseOptions,
|
|
12
|
+
TypedResponse,
|
|
13
|
+
} from "./response.ts";
|
|
14
|
+
import { createErrorResponse, type HoneyRes } from "./response.ts";
|
|
15
|
+
import type {
|
|
16
|
+
OutputValidator,
|
|
17
|
+
RouteHandler,
|
|
18
|
+
RouteTree,
|
|
19
|
+
TreeNode,
|
|
20
|
+
WSRouteHandler,
|
|
21
|
+
} from "./tree.ts";
|
|
22
|
+
import {
|
|
23
|
+
createNode,
|
|
24
|
+
insertRoute,
|
|
25
|
+
insertWsRoute,
|
|
26
|
+
matchRoute,
|
|
27
|
+
matchWsRoute,
|
|
28
|
+
mergeInto,
|
|
29
|
+
} from "./tree.ts";
|
|
30
|
+
import { createBus } from "./realtime/bus.ts";
|
|
31
|
+
import type { RealtimeBus } from "./realtime/bus.ts";
|
|
32
|
+
import { createConnContext } from "./realtime/route.ts";
|
|
33
|
+
import type { RealtimeRouteOpts } from "./realtime/route.ts";
|
|
34
|
+
import type {
|
|
35
|
+
ComputeErrorsByStatus,
|
|
36
|
+
DefaultMeta,
|
|
37
|
+
ExtractSchemas,
|
|
38
|
+
HttpMethod,
|
|
39
|
+
InferInputMap,
|
|
40
|
+
InferOutput,
|
|
41
|
+
InputSchemasDef,
|
|
42
|
+
MergePath,
|
|
43
|
+
MergeRoute,
|
|
44
|
+
OutputSchemaDef,
|
|
45
|
+
ParamsFromPath,
|
|
46
|
+
StandardSchemaLike,
|
|
47
|
+
StatusKey,
|
|
48
|
+
TapContext,
|
|
49
|
+
} from "./types.ts";
|
|
50
|
+
import { codeToStatusKey, EK, EMPTY_OBJ, SK } from "./types.ts";
|
|
51
|
+
import { validateInput, validateOutput } from "./validation.ts";
|
|
52
|
+
import type { WSAdapter, WSContext, WSHandler } from "./ws/cloudflare.ts";
|
|
53
|
+
import { loadHoneyFeature } from "./feature-load.ts";
|
|
54
|
+
import { getI18nRuntime } from "./i18n-slot.ts";
|
|
55
|
+
import { getOpenApiRuntime } from "./openapi/spec-factory.ts";
|
|
56
|
+
import { getServeRuntime } from "./serve-slot.ts";
|
|
57
|
+
import type { HoneyServeOptions, ServeHandle } from "./serve.ts";
|
|
58
|
+
|
|
59
|
+
export { HoneyContext } from "./context.ts";
|
|
60
|
+
/** HoneyContext without internal backing fields — use this for consumer-facing types */
|
|
61
|
+
export type HoneyCtx<TEnv = Record<string, unknown>> = Omit<
|
|
62
|
+
import("./context.ts").HoneyContext<TEnv>,
|
|
63
|
+
| "_errorToResponse"
|
|
64
|
+
| "_isErrorResponse"
|
|
65
|
+
| "_lzCookies"
|
|
66
|
+
| "_lzHeaders"
|
|
67
|
+
| "_lzSearch"
|
|
68
|
+
| "_lzSearchAll"
|
|
69
|
+
| "_lzUrlFn"
|
|
70
|
+
| "_setErrors"
|
|
71
|
+
>;
|
|
72
|
+
export { HoneyError } from "./error.ts";
|
|
73
|
+
export { defineErrors, ERROR_META } from "./errors.ts";
|
|
74
|
+
export type { ErrorMetaEntry } from "./errors.ts";
|
|
75
|
+
export type { MiddlewareFn } from "./middleware.ts";
|
|
76
|
+
export { createMiddleware, defineMiddleware } from "./middleware.ts";
|
|
77
|
+
export type { ProxyConfig } from "./proxy.ts";
|
|
78
|
+
export type { PendingTap, TapContext, TapHandler } from "./types.ts";
|
|
79
|
+
export type {
|
|
80
|
+
CookieOptions,
|
|
81
|
+
CustomErrorFormatter,
|
|
82
|
+
ErrorFormatter,
|
|
83
|
+
ResponseOptions,
|
|
84
|
+
SSEEvent,
|
|
85
|
+
SSEOptions,
|
|
86
|
+
SSEStream,
|
|
87
|
+
TypedResponse,
|
|
88
|
+
} from "./response.ts";
|
|
89
|
+
export { createErrorResponse, HoneyRes } from "./response.ts";
|
|
90
|
+
export { mergeTree } from "./tree.ts";
|
|
91
|
+
export type {
|
|
92
|
+
DefaultMeta,
|
|
93
|
+
HoneyCodegen,
|
|
94
|
+
HoneyMeta,
|
|
95
|
+
InferBasePath,
|
|
96
|
+
InferCtx,
|
|
97
|
+
InferEnv,
|
|
98
|
+
InferErrorFactory,
|
|
99
|
+
InferInputMap,
|
|
100
|
+
InferMeta,
|
|
101
|
+
InferMethods,
|
|
102
|
+
InferOutput,
|
|
103
|
+
InferRouteCtx,
|
|
104
|
+
InferRouteErrors,
|
|
105
|
+
InferRouteInput,
|
|
106
|
+
InferRouteMeta,
|
|
107
|
+
InferRouteMethods,
|
|
108
|
+
InferRouteOutput,
|
|
109
|
+
InferRoutePaths,
|
|
110
|
+
InferRoutes,
|
|
111
|
+
InputSchemasDef,
|
|
112
|
+
MergePath,
|
|
113
|
+
OpenApiMeta,
|
|
114
|
+
OutputSchemaDef,
|
|
115
|
+
Overwrite,
|
|
116
|
+
ComputeErrorsByStatus,
|
|
117
|
+
RouteRecord,
|
|
118
|
+
StandardSchemaLike,
|
|
119
|
+
StatusKey,
|
|
120
|
+
SuccessStatusKey,
|
|
121
|
+
} from "./types.ts";
|
|
122
|
+
export type { WSAdapter, WSContext, WSHandler, WSPreUpgrade } from "./ws/cloudflare.ts";
|
|
123
|
+
export type { ConnContext, RealtimeRouteOpts } from "./realtime/route.ts";
|
|
124
|
+
export type { RealtimeBus } from "./realtime/bus.ts";
|
|
125
|
+
export type { HoneyServeOptions, ServeHandle } from "./serve.ts";
|
|
126
|
+
export type { ServeRuntime } from "./detect-runtime.ts";
|
|
127
|
+
export { detectRuntime } from "./detect-runtime.ts";
|
|
128
|
+
|
|
129
|
+
/** Type predicate for incoming wire-protocol msg frames from the realtime client. */
|
|
130
|
+
function isMsgFrame(value: unknown): value is { data: unknown; t: "msg" } {
|
|
131
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
132
|
+
if (!("t" in value) || !("data" in value)) return false;
|
|
133
|
+
return value.t === "msg";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type TelemetryAdapter = {
|
|
137
|
+
onError?(ctx: {
|
|
138
|
+
duration: number;
|
|
139
|
+
error: HoneyError;
|
|
140
|
+
method: string;
|
|
141
|
+
path: string;
|
|
142
|
+
}): void;
|
|
143
|
+
onHandler?(ctx: {
|
|
144
|
+
duration: number;
|
|
145
|
+
method: string;
|
|
146
|
+
path: string;
|
|
147
|
+
status: number;
|
|
148
|
+
}): void;
|
|
149
|
+
onMethodNotAllowed?(ctx: {
|
|
150
|
+
allowed: string[];
|
|
151
|
+
method: string;
|
|
152
|
+
path: string;
|
|
153
|
+
req: Request;
|
|
154
|
+
}): void;
|
|
155
|
+
onMiddleware?(ctx: { duration: number; error?: unknown; name: string }): void;
|
|
156
|
+
onNotFound?(ctx: { method: string; path: string; req: Request }): void;
|
|
157
|
+
onRequest?(ctx: { env: unknown; req: Request }): void;
|
|
158
|
+
onResponse?(ctx: { duration: number; req: Request; status: number }): void;
|
|
159
|
+
onRoute?(ctx: {
|
|
160
|
+
method: string;
|
|
161
|
+
params: Record<string, string>;
|
|
162
|
+
path: string;
|
|
163
|
+
req: Request;
|
|
164
|
+
}): void;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
type ErrorI18nConfig<TEnv> = {
|
|
168
|
+
errors?: Record<string, Record<string, string>>;
|
|
169
|
+
fieldNames?: Record<string, Record<string, string>>;
|
|
170
|
+
resolveLocale: (ctx: {
|
|
171
|
+
cookies: Record<string, string>;
|
|
172
|
+
env: TEnv;
|
|
173
|
+
headers: Record<string, string>;
|
|
174
|
+
params: Record<string, string>;
|
|
175
|
+
req: Request;
|
|
176
|
+
search: Record<string, string>;
|
|
177
|
+
}) => string | Promise<string>;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
type Logger = {
|
|
181
|
+
warn?(msg: string, ...args: unknown[]): void;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
function mergePath(base: string, path: string): string {
|
|
185
|
+
if (base === "/") return path;
|
|
186
|
+
if (path === "/") return base;
|
|
187
|
+
return `${base}${path}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Normalize a scope prefix: ensure leading slash, strip trailing slash (except "/"). */
|
|
191
|
+
function normalizeScopePath(raw: string): string {
|
|
192
|
+
let p = raw;
|
|
193
|
+
if (p.length === 0) return "/";
|
|
194
|
+
if (p.charCodeAt(0) !== 47) p = `/${p}`;
|
|
195
|
+
if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
|
|
196
|
+
return p;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Check whether fullPath falls under scope prefix — exact match or next char is '/'. */
|
|
200
|
+
function scopeMatches(prefix: string, fullPath: string): boolean {
|
|
201
|
+
if (prefix === "/") return true;
|
|
202
|
+
if (fullPath === prefix) return true;
|
|
203
|
+
if (fullPath.length <= prefix.length) return false;
|
|
204
|
+
if (fullPath.charCodeAt(prefix.length) !== 47) return false;
|
|
205
|
+
return fullPath.startsWith(prefix);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
type HoneyGraph = {
|
|
209
|
+
handlerMap: Record<string, RouteHandler> | null
|
|
210
|
+
hasRouteTree: boolean
|
|
211
|
+
realtimeBus: RealtimeBus | null
|
|
212
|
+
root: TreeNode
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function walkTreeHandlers(
|
|
216
|
+
root: TreeNode,
|
|
217
|
+
cb: (h: RouteHandler) => void,
|
|
218
|
+
wsCb?: (h: WSRouteHandler) => void,
|
|
219
|
+
): void {
|
|
220
|
+
if (root.m) {
|
|
221
|
+
for (const h of Object.values(root.m) as RouteHandler[]) cb(h);
|
|
222
|
+
}
|
|
223
|
+
if (root.w) {
|
|
224
|
+
for (const h of Object.values(root.w.m) as RouteHandler[]) cb(h);
|
|
225
|
+
}
|
|
226
|
+
if (root.ws && wsCb) wsCb(root.ws);
|
|
227
|
+
for (const child of Object.values(root.s)) walkTreeHandlers(child, cb, wsCb);
|
|
228
|
+
if (root.d) walkTreeHandlers(root.d.c, cb, wsCb);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const EMPTY_PARAMS = EMPTY_OBJ as Record<string, string>
|
|
232
|
+
const EMPTY_MW: RuntimeMiddleware[] = []
|
|
233
|
+
|
|
234
|
+
/** Find first '?' or '#' in url starting from pos */
|
|
235
|
+
function findSearchOrHash(url: string, pos: number): number {
|
|
236
|
+
for (let i = pos; i < url.length; i++) {
|
|
237
|
+
const c = url.charCodeAt(i);
|
|
238
|
+
if (c === 63 || c === 35) return i;
|
|
239
|
+
}
|
|
240
|
+
return -1;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function safeFire(fn: (() => unknown) | undefined, logger?: Logger): void {
|
|
244
|
+
if (fn === undefined) return;
|
|
245
|
+
try {
|
|
246
|
+
const result = fn();
|
|
247
|
+
if (result && typeof result === "object" && "catch" in result) {
|
|
248
|
+
(result as Promise<unknown>).catch((e: unknown) => {
|
|
249
|
+
logger?.warn?.("telemetry callback failed", e);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
} catch (e) {
|
|
253
|
+
logger?.warn?.("telemetry callback failed", e);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Internal context shared across extracted fetch sub-methods */
|
|
258
|
+
type FetchCtx<TEnv> = {
|
|
259
|
+
env: TEnv;
|
|
260
|
+
executionCtx: { waitUntil?: (p: Promise<unknown>) => void } | undefined;
|
|
261
|
+
log: Logger | undefined;
|
|
262
|
+
request: Request;
|
|
263
|
+
startTime: number;
|
|
264
|
+
url: () => URL;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
function defaultErrorFormatter(
|
|
268
|
+
_error: HoneyError,
|
|
269
|
+
defaultShape: Record<string, unknown>,
|
|
270
|
+
): Record<string, unknown> {
|
|
271
|
+
return defaultShape;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
type ErrorFormatterFn = ErrorFormatter;
|
|
275
|
+
|
|
276
|
+
const STATIC_CTX_RESERVED = new Set([
|
|
277
|
+
"background",
|
|
278
|
+
"cookies",
|
|
279
|
+
"env",
|
|
280
|
+
"headers",
|
|
281
|
+
"params",
|
|
282
|
+
"req",
|
|
283
|
+
"res",
|
|
284
|
+
"search",
|
|
285
|
+
]);
|
|
286
|
+
|
|
287
|
+
/* errorKeys the framework throws on its own behalf — input/output validation, content negotiation,
|
|
288
|
+
* routing. Always passes the boundary check; users never declare these via .errors(). */
|
|
289
|
+
const FRAMEWORK_EKS = new Set<(typeof EK)[keyof typeof EK]>([
|
|
290
|
+
EK.validation_failed,
|
|
291
|
+
EK.output_validation_failed,
|
|
292
|
+
EK.output_content_type_mismatch,
|
|
293
|
+
EK.unsupported_media_type,
|
|
294
|
+
EK.content_too_large,
|
|
295
|
+
EK.method_not_allowed,
|
|
296
|
+
EK.not_found,
|
|
297
|
+
EK.too_many_requests,
|
|
298
|
+
EK.request_timeout,
|
|
299
|
+
EK.gateway_timeout,
|
|
300
|
+
EK.bad_gateway,
|
|
301
|
+
]);
|
|
302
|
+
|
|
303
|
+
export class Honey<
|
|
304
|
+
TEnv,
|
|
305
|
+
TCtx = HoneyContext<TEnv>,
|
|
306
|
+
TRoutes = {},
|
|
307
|
+
TMeta = never,
|
|
308
|
+
TErrorFactory = never,
|
|
309
|
+
TDefaultErrors extends string = never,
|
|
310
|
+
TBasePath extends string = "/",
|
|
311
|
+
TTaps extends Record<string, unknown> = {},
|
|
312
|
+
TScopedMw extends readonly ScopedMwEntry[] = [],
|
|
313
|
+
> {
|
|
314
|
+
declare readonly $basePath: TBasePath;
|
|
315
|
+
declare readonly $ctx: TCtx;
|
|
316
|
+
declare readonly $env: TEnv;
|
|
317
|
+
declare readonly $errorFactory: TErrorFactory;
|
|
318
|
+
declare readonly $meta: TMeta;
|
|
319
|
+
declare readonly $routes: TRoutes;
|
|
320
|
+
declare readonly $taps: TTaps;
|
|
321
|
+
private _basePath: string;
|
|
322
|
+
private _defaultBoundaryKey: string | null;
|
|
323
|
+
private _defaultErrorKeys: Set<string>;
|
|
324
|
+
private _errorFactory: unknown;
|
|
325
|
+
private _errorSchema: StandardSchemaLike | null;
|
|
326
|
+
private _customErrorFormatter: CustomErrorFormatter | null;
|
|
327
|
+
private _customErrorSchema: StandardSchemaLike | null;
|
|
328
|
+
private _chainMeta: Record<string, unknown> | null;
|
|
329
|
+
private _chainMiddlewares: RuntimeMiddleware[];
|
|
330
|
+
private _scopedMiddlewares: ScopedEntry[];
|
|
331
|
+
private _contextValues: Record<string, unknown> | null;
|
|
332
|
+
private _errorFormatter: ErrorFormatterFn;
|
|
333
|
+
private _errorI18n: ErrorI18nConfig<TEnv> | null;
|
|
334
|
+
private _globalMiddlewares: RuntimeMiddleware[];
|
|
335
|
+
private _graph: HoneyGraph;
|
|
336
|
+
private _hasWsRoutes: boolean;
|
|
337
|
+
private _logger: Logger | null;
|
|
338
|
+
private _outputValidation: "always" | "dev" | "off";
|
|
339
|
+
private _staticRoutes: { map: Record<string, RouteHandler> | null };
|
|
340
|
+
private _stripPrefix: string | null;
|
|
341
|
+
private _trailingSlash: "enforce" | "ignore" | "strip";
|
|
342
|
+
private _wsAdapter: WSAdapter | null;
|
|
343
|
+
private _openApiCache: { epoch: number; value: Promise<unknown> } | null;
|
|
344
|
+
private _openApiYamlCache: { epoch: number; value: Promise<string> } | null;
|
|
345
|
+
private _manifestCache: { epoch: number; value: Promise<unknown> } | null;
|
|
346
|
+
private _onError:
|
|
347
|
+
| ((
|
|
348
|
+
error: unknown,
|
|
349
|
+
ctx: {
|
|
350
|
+
env: TEnv;
|
|
351
|
+
jsonFromError: (err: HoneyError) => Response;
|
|
352
|
+
req: Request;
|
|
353
|
+
},
|
|
354
|
+
) => HoneyError | Response | Promise<HoneyError | Response | undefined | void> | undefined | void)
|
|
355
|
+
| null;
|
|
356
|
+
private _onMethodNotAllowed:
|
|
357
|
+
| ((ctx: {
|
|
358
|
+
allowed: string[];
|
|
359
|
+
env: TEnv;
|
|
360
|
+
jsonFromError: (err: HoneyError) => Response;
|
|
361
|
+
req: Request;
|
|
362
|
+
}) => Response | Promise<Response>)
|
|
363
|
+
| null;
|
|
364
|
+
private _onNotFound:
|
|
365
|
+
| ((ctx: {
|
|
366
|
+
env: TEnv;
|
|
367
|
+
jsonFromError: (err: HoneyError) => Response;
|
|
368
|
+
req: Request;
|
|
369
|
+
}) => Response | Promise<Response>)
|
|
370
|
+
| null;
|
|
371
|
+
private _taps: Map<
|
|
372
|
+
string,
|
|
373
|
+
(ctx: TapContext<TEnv>, payload: unknown) => void | Promise<void>
|
|
374
|
+
> | null;
|
|
375
|
+
private _telemetry: TelemetryAdapter | null;
|
|
376
|
+
private _realtimeRoutes: Map<string, { handler: RealtimeRouteOpts["handler"]; middlewares?: RealtimeRouteOpts["use"]; reconnectBuffer?: number }>;
|
|
377
|
+
|
|
378
|
+
constructor(opts?: {
|
|
379
|
+
chainMiddlewares?: RuntimeMiddleware[];
|
|
380
|
+
defaultErrorKeys?: Set<string>;
|
|
381
|
+
globalMiddlewares?: RuntimeMiddleware[];
|
|
382
|
+
graph?: HoneyGraph;
|
|
383
|
+
handlerMap?: Record<string, RouteHandler> | null;
|
|
384
|
+
root?: TreeNode;
|
|
385
|
+
scopedMiddlewares?: ScopedEntry[];
|
|
386
|
+
}) {
|
|
387
|
+
this._basePath = "/";
|
|
388
|
+
this._graph = opts?.graph ?? {
|
|
389
|
+
handlerMap: opts?.handlerMap ?? null,
|
|
390
|
+
hasRouteTree: false,
|
|
391
|
+
realtimeBus: null,
|
|
392
|
+
root: opts?.root ?? createNode(),
|
|
393
|
+
};
|
|
394
|
+
this._globalMiddlewares = opts?.globalMiddlewares ?? [];
|
|
395
|
+
this._scopedMiddlewares = opts?.scopedMiddlewares ?? [];
|
|
396
|
+
this._chainMeta = null;
|
|
397
|
+
this._chainMiddlewares = opts?.chainMiddlewares ?? [];
|
|
398
|
+
this._contextValues = null;
|
|
399
|
+
this._defaultBoundaryKey = null;
|
|
400
|
+
this._defaultErrorKeys = opts?.defaultErrorKeys ?? new Set();
|
|
401
|
+
this._errorFactory = null;
|
|
402
|
+
this._errorSchema = null;
|
|
403
|
+
this._customErrorFormatter = null;
|
|
404
|
+
this._customErrorSchema = null;
|
|
405
|
+
this._errorFormatter = defaultErrorFormatter;
|
|
406
|
+
this._errorI18n = null;
|
|
407
|
+
this._hasWsRoutes = false;
|
|
408
|
+
this._logger = null;
|
|
409
|
+
this._outputValidation = "off";
|
|
410
|
+
this._staticRoutes = { map: null };
|
|
411
|
+
this._stripPrefix = null;
|
|
412
|
+
this._trailingSlash = "ignore";
|
|
413
|
+
this._wsAdapter = null;
|
|
414
|
+
this._openApiCache = null;
|
|
415
|
+
this._openApiYamlCache = null;
|
|
416
|
+
this._manifestCache = null;
|
|
417
|
+
this._onError = null;
|
|
418
|
+
this._onNotFound = null;
|
|
419
|
+
this._onMethodNotAllowed = null;
|
|
420
|
+
this._taps = null;
|
|
421
|
+
this._telemetry = null;
|
|
422
|
+
this._realtimeRoutes = new Map();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private get _handlerMap(): Record<string, RouteHandler> | null {
|
|
426
|
+
return this._graph.handlerMap;
|
|
427
|
+
}
|
|
428
|
+
private set _handlerMap(value: Record<string, RouteHandler> | null) {
|
|
429
|
+
this._graph.handlerMap = value;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private get _hasRouteTree(): boolean {
|
|
433
|
+
return this._graph.hasRouteTree;
|
|
434
|
+
}
|
|
435
|
+
private set _hasRouteTree(value: boolean) {
|
|
436
|
+
this._graph.hasRouteTree = value;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private get _root(): TreeNode {
|
|
440
|
+
return this._graph.root;
|
|
441
|
+
}
|
|
442
|
+
private set _root(value: TreeNode) {
|
|
443
|
+
this._graph.root = value;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** @internal — used by codegen */
|
|
447
|
+
private get _tree(): TreeNode {
|
|
448
|
+
return this._root;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private get _realtimeBus(): RealtimeBus | null {
|
|
452
|
+
return this._graph.realtimeBus;
|
|
453
|
+
}
|
|
454
|
+
private set _realtimeBus(value: RealtimeBus | null) {
|
|
455
|
+
this._graph.realtimeBus = value;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** @internal — mark that this app has WS routes */
|
|
459
|
+
_markWsRoutes(): void {
|
|
460
|
+
this._hasWsRoutes = true;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** @internal — register a static route for O(1) lookup */
|
|
464
|
+
_registerStatic(key: string, handler: RouteHandler): void {
|
|
465
|
+
if (this._staticRoutes.map === null) {
|
|
466
|
+
this._staticRoutes.map = Object.create(null) as Record<string, RouteHandler>;
|
|
467
|
+
}
|
|
468
|
+
this._staticRoutes.map[key] = handler;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** @internal — used by RouteBuilder for pre-filtered error factory */
|
|
472
|
+
get _factory(): unknown {
|
|
473
|
+
return this._errorFactory;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** @internal — used by runtime error boundary */
|
|
477
|
+
get _boundaryKey(): string | null {
|
|
478
|
+
return this._defaultBoundaryKey;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Convert unknown thrown value to error Response — used in WS, 404, 405 catch blocks */
|
|
482
|
+
private _toErrorResponse(thrown: unknown): Response {
|
|
483
|
+
const error =
|
|
484
|
+
thrown instanceof HoneyError
|
|
485
|
+
? thrown
|
|
486
|
+
: new HoneyError({
|
|
487
|
+
cause: thrown,
|
|
488
|
+
errorKey: EK.internal_server_error,
|
|
489
|
+
status: SK.internal_server_error,
|
|
490
|
+
});
|
|
491
|
+
return createErrorResponse(error, this._errorFormatter, this._customErrorFormatter);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private _createBoundaryError(errorKey: string, cause: unknown): HoneyError {
|
|
495
|
+
const factory = this._errorFactory as Record<
|
|
496
|
+
string,
|
|
497
|
+
((opts?: { cause?: unknown }) => HoneyError) | undefined
|
|
498
|
+
> | null;
|
|
499
|
+
const factoryFn = factory?.[errorKey];
|
|
500
|
+
if (factoryFn) {
|
|
501
|
+
/* check if this is a custom schema error via ERROR_META — boundary must use standard errors only */
|
|
502
|
+
const meta = (factory as Record<symbol, Record<string, { schema: unknown }>>)?.[ERROR_META];
|
|
503
|
+
if (meta?.[errorKey]?.schema) {
|
|
504
|
+
/* custom schema error — cannot be used as boundary, fall through to manual construction */
|
|
505
|
+
} else {
|
|
506
|
+
return factoryFn({ cause });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return new HoneyError({
|
|
510
|
+
cause,
|
|
511
|
+
errorKey,
|
|
512
|
+
status: SK.internal_server_error,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private _createError(errorKey: string, statusKey: StatusKey): HoneyError {
|
|
517
|
+
const factory = this._errorFactory as Record<
|
|
518
|
+
string,
|
|
519
|
+
(() => HoneyError) | undefined
|
|
520
|
+
> | null;
|
|
521
|
+
const factoryFn = factory?.[errorKey];
|
|
522
|
+
if (factoryFn) {
|
|
523
|
+
return factoryFn();
|
|
524
|
+
}
|
|
525
|
+
return new HoneyError({ errorKey, status: statusKey });
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Apply error keys from a single scoped entry to every matching handler currently in the tree */
|
|
529
|
+
private _applyScopedEntryErrors(entry: ScopedEntry): void {
|
|
530
|
+
const errors = entry.errors;
|
|
531
|
+
if (!errors || errors.length === 0) return;
|
|
532
|
+
walkTreeHandlers(
|
|
533
|
+
this._root,
|
|
534
|
+
(h) => {
|
|
535
|
+
if (scopeMatches(entry.prefix, h.rp)) {
|
|
536
|
+
for (const k of errors) h.ek.add(k);
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
(wh) => {
|
|
540
|
+
if (scopeMatches(entry.prefix, wh.rp)) {
|
|
541
|
+
for (const k of errors) wh.ek.add(k);
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Apply error keys from every scoped entry on this chain to every matching handler in the tree */
|
|
548
|
+
private _applyAllScopedErrors(): void {
|
|
549
|
+
for (const entry of this._scopedMiddlewares) {
|
|
550
|
+
this._applyScopedEntryErrors(entry);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Return scoped middleware functions that match the given route path */
|
|
555
|
+
private _filterScopedForPath(routePath: string): RuntimeMiddleware[] {
|
|
556
|
+
if (this._scopedMiddlewares.length === 0) return EMPTY_MW;
|
|
557
|
+
const out: RuntimeMiddleware[] = [];
|
|
558
|
+
for (const s of this._scopedMiddlewares) {
|
|
559
|
+
if (scopeMatches(s.prefix, routePath)) out.push(s.mw);
|
|
560
|
+
}
|
|
561
|
+
return out;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** Mutates `honeyError.message` in-place with the i18n-resolved template for its errorKey.
|
|
565
|
+
* No-op when i18n is not configured or when no template matches. */
|
|
566
|
+
private async _resolveI18n(
|
|
567
|
+
honeyError: HoneyError,
|
|
568
|
+
ctx: HoneyContext<TEnv>,
|
|
569
|
+
env: TEnv,
|
|
570
|
+
request: Request,
|
|
571
|
+
log?: Logger,
|
|
572
|
+
): Promise<void> {
|
|
573
|
+
if (!this._errorI18n) return;
|
|
574
|
+
try {
|
|
575
|
+
const locale = await this._errorI18n.resolveLocale({
|
|
576
|
+
cookies: ctx.cookies,
|
|
577
|
+
env,
|
|
578
|
+
headers: ctx.headers,
|
|
579
|
+
params: ctx.params,
|
|
580
|
+
req: request,
|
|
581
|
+
search: ctx.search,
|
|
582
|
+
});
|
|
583
|
+
const translations = this._errorI18n.errors?.[locale];
|
|
584
|
+
if (translations) {
|
|
585
|
+
const template = translations[honeyError.errorKey];
|
|
586
|
+
if (template) {
|
|
587
|
+
await loadHoneyFeature("i18n")
|
|
588
|
+
honeyError.message = getI18nRuntime().interpolate(
|
|
589
|
+
template,
|
|
590
|
+
honeyError.vars ?? {},
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const fieldTranslations = this._errorI18n.fieldNames?.[locale];
|
|
596
|
+
if (fieldTranslations && Object.keys(honeyError.fields).length > 0) {
|
|
597
|
+
for (const fieldErrors of Object.values(honeyError.fields)) {
|
|
598
|
+
for (const fe of fieldErrors) {
|
|
599
|
+
let candidate = fe.path;
|
|
600
|
+
while (candidate) {
|
|
601
|
+
const translated = fieldTranslations[candidate];
|
|
602
|
+
if (translated) {
|
|
603
|
+
fe.path = translated;
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
const dotIdx = candidate.indexOf(".");
|
|
607
|
+
if (dotIdx === -1) break;
|
|
608
|
+
candidate = candidate.slice(dotIdx + 1);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
} catch (e) {
|
|
614
|
+
log?.warn?.("i18n resolution failed", e);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Convert thrown value into an error Response — resolves boundary wrapping,
|
|
620
|
+
* i18n translation, onError callback, and telemetry.
|
|
621
|
+
* Called from the handler wrapper so errors flow back through middleware.
|
|
622
|
+
*/
|
|
623
|
+
private async _resolveErrorResponse(
|
|
624
|
+
thrown: unknown,
|
|
625
|
+
handler: RouteHandler,
|
|
626
|
+
fc: FetchCtx<TEnv>,
|
|
627
|
+
method: string,
|
|
628
|
+
path: string,
|
|
629
|
+
ctx: HoneyContext<TEnv>,
|
|
630
|
+
): Promise<Response> {
|
|
631
|
+
const { env, log, request, startTime } = fc;
|
|
632
|
+
let honeyError: HoneyError;
|
|
633
|
+
const boundaryKey = handler.bek ?? this._defaultBoundaryKey;
|
|
634
|
+
|
|
635
|
+
if (thrown instanceof HoneyError) {
|
|
636
|
+
/* framework-managed errorKeys (input/output validation, content negotiation, etc.) are always allowed
|
|
637
|
+
* regardless of handler.ek — users never declare them, the framework owns them. */
|
|
638
|
+
const isFrameworkEk = FRAMEWORK_EKS.has(thrown.errorKey as (typeof EK)[keyof typeof EK]);
|
|
639
|
+
if (!isFrameworkEk && handler.ek.size > 0 && !handler.ek.has(thrown.errorKey)) {
|
|
640
|
+
if (boundaryKey) {
|
|
641
|
+
honeyError = this._createBoundaryError(boundaryKey, thrown);
|
|
642
|
+
} else {
|
|
643
|
+
honeyError = new HoneyError({
|
|
644
|
+
cause: thrown,
|
|
645
|
+
errorKey: EK.internal_server_error,
|
|
646
|
+
status: SK.internal_server_error,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
} else {
|
|
650
|
+
honeyError = thrown;
|
|
651
|
+
}
|
|
652
|
+
} else {
|
|
653
|
+
if (boundaryKey) {
|
|
654
|
+
honeyError = this._createBoundaryError(boundaryKey, thrown);
|
|
655
|
+
} else {
|
|
656
|
+
honeyError = new HoneyError({
|
|
657
|
+
cause: thrown,
|
|
658
|
+
errorKey: EK.internal_server_error,
|
|
659
|
+
status: SK.internal_server_error,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
await this._resolveI18n(honeyError, ctx, env, request, log);
|
|
665
|
+
|
|
666
|
+
/* onError handler */
|
|
667
|
+
if (this._onError) {
|
|
668
|
+
try {
|
|
669
|
+
const customResult = await this._onError(thrown, this._makeErrorCtx(fc));
|
|
670
|
+
if (customResult instanceof HoneyError) {
|
|
671
|
+
/* user-mapped boundary error — re-run i18n against new errorKey,
|
|
672
|
+
* then fall through to default response path so telemetry +
|
|
673
|
+
* jsonFromError run exactly once. */
|
|
674
|
+
honeyError = customResult;
|
|
675
|
+
await this._resolveI18n(honeyError, ctx, env, request, log);
|
|
676
|
+
} else if (customResult) {
|
|
677
|
+
safeFire(
|
|
678
|
+
() =>
|
|
679
|
+
this._telemetry?.onError?.({
|
|
680
|
+
duration: performance.now() - startTime,
|
|
681
|
+
error: honeyError,
|
|
682
|
+
method,
|
|
683
|
+
path,
|
|
684
|
+
}),
|
|
685
|
+
log,
|
|
686
|
+
);
|
|
687
|
+
safeFire(
|
|
688
|
+
() =>
|
|
689
|
+
this._telemetry?.onResponse?.({
|
|
690
|
+
duration: performance.now() - startTime,
|
|
691
|
+
req: request,
|
|
692
|
+
status: customResult.status,
|
|
693
|
+
}),
|
|
694
|
+
log,
|
|
695
|
+
);
|
|
696
|
+
ctx._isErrorResponse = true;
|
|
697
|
+
return customResult;
|
|
698
|
+
}
|
|
699
|
+
/* customResult === undefined | void → fall through to default path */
|
|
700
|
+
} catch {
|
|
701
|
+
/* swallow onError errors */
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/* default error response */
|
|
706
|
+
safeFire(
|
|
707
|
+
() =>
|
|
708
|
+
this._telemetry?.onError?.({
|
|
709
|
+
duration: performance.now() - startTime,
|
|
710
|
+
error: honeyError,
|
|
711
|
+
method,
|
|
712
|
+
path,
|
|
713
|
+
}),
|
|
714
|
+
log,
|
|
715
|
+
);
|
|
716
|
+
const res = this._makeErrorCtx(fc).jsonFromError(honeyError);
|
|
717
|
+
safeFire(
|
|
718
|
+
() =>
|
|
719
|
+
this._telemetry?.onResponse?.({
|
|
720
|
+
duration: performance.now() - startTime,
|
|
721
|
+
req: request,
|
|
722
|
+
status: res.status,
|
|
723
|
+
}),
|
|
724
|
+
log,
|
|
725
|
+
);
|
|
726
|
+
ctx._isErrorResponse = true;
|
|
727
|
+
return res;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
basePath<P extends string>(
|
|
731
|
+
prefix: P,
|
|
732
|
+
): Honey<
|
|
733
|
+
TEnv,
|
|
734
|
+
TCtx,
|
|
735
|
+
TRoutes,
|
|
736
|
+
TMeta,
|
|
737
|
+
TErrorFactory,
|
|
738
|
+
TDefaultErrors,
|
|
739
|
+
MergePath<TBasePath, P>,
|
|
740
|
+
TTaps,
|
|
741
|
+
TScopedMw
|
|
742
|
+
> {
|
|
743
|
+
const next = new Honey<
|
|
744
|
+
TEnv,
|
|
745
|
+
TCtx,
|
|
746
|
+
TRoutes,
|
|
747
|
+
TMeta,
|
|
748
|
+
TErrorFactory,
|
|
749
|
+
TDefaultErrors,
|
|
750
|
+
MergePath<TBasePath, P>,
|
|
751
|
+
TTaps,
|
|
752
|
+
TScopedMw
|
|
753
|
+
>({
|
|
754
|
+
chainMiddlewares: this._chainMiddlewares,
|
|
755
|
+
defaultErrorKeys: this._defaultErrorKeys,
|
|
756
|
+
globalMiddlewares: this._globalMiddlewares,
|
|
757
|
+
graph: this._graph,
|
|
758
|
+
scopedMiddlewares: this._scopedMiddlewares,
|
|
759
|
+
});
|
|
760
|
+
next._basePath = mergePath(this._basePath, prefix);
|
|
761
|
+
next._defaultBoundaryKey = this._defaultBoundaryKey;
|
|
762
|
+
next._errorFactory = this._errorFactory;
|
|
763
|
+
next._errorSchema = this._errorSchema;
|
|
764
|
+
next._customErrorFormatter = this._customErrorFormatter;
|
|
765
|
+
next._customErrorSchema = this._customErrorSchema;
|
|
766
|
+
next._errorFormatter = this._errorFormatter;
|
|
767
|
+
next._errorI18n = this._errorI18n;
|
|
768
|
+
next._logger = this._logger;
|
|
769
|
+
next._outputValidation = this._outputValidation;
|
|
770
|
+
next._stripPrefix = this._stripPrefix;
|
|
771
|
+
next._trailingSlash = this._trailingSlash;
|
|
772
|
+
next._wsAdapter = this._wsAdapter;
|
|
773
|
+
next._onError = this._onError;
|
|
774
|
+
next._onNotFound = this._onNotFound;
|
|
775
|
+
next._onMethodNotAllowed = this._onMethodNotAllowed;
|
|
776
|
+
next._chainMeta = this._chainMeta;
|
|
777
|
+
next._contextValues = this._contextValues;
|
|
778
|
+
next._taps = this._taps;
|
|
779
|
+
next._telemetry = this._telemetry;
|
|
780
|
+
next._realtimeBus = this._realtimeBus;
|
|
781
|
+
next._realtimeRoutes = this._realtimeRoutes;
|
|
782
|
+
next._staticRoutes = this._staticRoutes;
|
|
783
|
+
next._hasRouteTree = this._hasRouteTree;
|
|
784
|
+
next._hasWsRoutes = this._hasWsRoutes;
|
|
785
|
+
return next as unknown as Honey<
|
|
786
|
+
TEnv,
|
|
787
|
+
TCtx,
|
|
788
|
+
TRoutes,
|
|
789
|
+
TMeta,
|
|
790
|
+
TErrorFactory,
|
|
791
|
+
TDefaultErrors,
|
|
792
|
+
MergePath<TBasePath, P>,
|
|
793
|
+
TTaps,
|
|
794
|
+
TScopedMw
|
|
795
|
+
>;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
context<TAdds extends Record<string, unknown>>(
|
|
799
|
+
values: TAdds,
|
|
800
|
+
): Honey<
|
|
801
|
+
TEnv,
|
|
802
|
+
TCtx & Readonly<TAdds>,
|
|
803
|
+
TRoutes,
|
|
804
|
+
TMeta,
|
|
805
|
+
TErrorFactory,
|
|
806
|
+
TDefaultErrors,
|
|
807
|
+
TBasePath,
|
|
808
|
+
TTaps,
|
|
809
|
+
TScopedMw
|
|
810
|
+
> {
|
|
811
|
+
for (const key in values) {
|
|
812
|
+
if (STATIC_CTX_RESERVED.has(key)) {
|
|
813
|
+
throw new Error(`context() cannot set reserved key "${key}"`)
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const next = new Honey<
|
|
817
|
+
TEnv,
|
|
818
|
+
TCtx & Readonly<TAdds>,
|
|
819
|
+
TRoutes,
|
|
820
|
+
TMeta,
|
|
821
|
+
TErrorFactory,
|
|
822
|
+
TDefaultErrors,
|
|
823
|
+
TBasePath,
|
|
824
|
+
TTaps,
|
|
825
|
+
TScopedMw
|
|
826
|
+
>({
|
|
827
|
+
chainMiddlewares: this._chainMiddlewares,
|
|
828
|
+
defaultErrorKeys: this._defaultErrorKeys,
|
|
829
|
+
globalMiddlewares: this._globalMiddlewares,
|
|
830
|
+
graph: this._graph,
|
|
831
|
+
scopedMiddlewares: this._scopedMiddlewares,
|
|
832
|
+
});
|
|
833
|
+
next._basePath = this._basePath;
|
|
834
|
+
next._chainMeta = this._chainMeta;
|
|
835
|
+
next._contextValues = this._contextValues
|
|
836
|
+
? { ...this._contextValues, ...values }
|
|
837
|
+
: { ...values };
|
|
838
|
+
next._defaultBoundaryKey = this._defaultBoundaryKey;
|
|
839
|
+
next._errorFactory = this._errorFactory;
|
|
840
|
+
next._errorSchema = this._errorSchema;
|
|
841
|
+
next._customErrorFormatter = this._customErrorFormatter;
|
|
842
|
+
next._customErrorSchema = this._customErrorSchema;
|
|
843
|
+
next._errorFormatter = this._errorFormatter;
|
|
844
|
+
next._errorI18n = this._errorI18n;
|
|
845
|
+
next._logger = this._logger;
|
|
846
|
+
next._outputValidation = this._outputValidation;
|
|
847
|
+
next._stripPrefix = this._stripPrefix;
|
|
848
|
+
next._trailingSlash = this._trailingSlash;
|
|
849
|
+
next._wsAdapter = this._wsAdapter;
|
|
850
|
+
next._onError = this._onError;
|
|
851
|
+
next._onNotFound = this._onNotFound;
|
|
852
|
+
next._onMethodNotAllowed = this._onMethodNotAllowed;
|
|
853
|
+
next._taps = this._taps;
|
|
854
|
+
next._telemetry = this._telemetry;
|
|
855
|
+
next._realtimeBus = this._realtimeBus;
|
|
856
|
+
next._realtimeRoutes = this._realtimeRoutes;
|
|
857
|
+
next._staticRoutes = this._staticRoutes;
|
|
858
|
+
next._hasRouteTree = this._hasRouteTree;
|
|
859
|
+
next._hasWsRoutes = this._hasWsRoutes;
|
|
860
|
+
return next as unknown as Honey<TEnv, TCtx & Readonly<TAdds>, TRoutes, TMeta, TErrorFactory, TDefaultErrors, TBasePath, TTaps, TScopedMw>;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
logger(logger: Logger): this {
|
|
864
|
+
this._logger = logger;
|
|
865
|
+
return this;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
outputValidation(mode: "always" | "dev" | "off"): this {
|
|
869
|
+
this._outputValidation = mode;
|
|
870
|
+
return this;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
trailingSlash(mode: "enforce" | "ignore" | "strip"): this {
|
|
874
|
+
this._trailingSlash = mode;
|
|
875
|
+
return this;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/** Strip a URL path prefix before route matching — boundary-safe (won't strip partial segments), paths without the prefix pass through unchanged */
|
|
879
|
+
stripPrefix(prefix: string): this {
|
|
880
|
+
let normalized = prefix.replace(/\/+$/, "");
|
|
881
|
+
if (normalized.length > 0 && normalized.charCodeAt(0) !== 47) {
|
|
882
|
+
normalized = `/${normalized}`;
|
|
883
|
+
}
|
|
884
|
+
this._stripPrefix = normalized === "" || normalized === "/" ? null : normalized;
|
|
885
|
+
return this;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
wsAdapter(adapter: WSAdapter): this {
|
|
889
|
+
this._wsAdapter = adapter;
|
|
890
|
+
return this;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
openapi(options: {
|
|
894
|
+
description?: string
|
|
895
|
+
docs?: "scalar" | "swagger"
|
|
896
|
+
docsPath?: string
|
|
897
|
+
filterRoutes?: (route: { meta: unknown; method: string; path: string }) => boolean
|
|
898
|
+
path?: string
|
|
899
|
+
securitySchemes?: Record<string, unknown>
|
|
900
|
+
title: string
|
|
901
|
+
version: string
|
|
902
|
+
}): this {
|
|
903
|
+
const stem = options.path ?? "/openapi"
|
|
904
|
+
const yamlHeaders = { headers: { "content-type": "application/yaml; charset=utf-8" } }
|
|
905
|
+
const loadJson = (): Promise<unknown> => {
|
|
906
|
+
const epoch = this._root.g ?? 0
|
|
907
|
+
if (this._openApiCache && this._openApiCache.epoch === epoch) {
|
|
908
|
+
return this._openApiCache.value
|
|
909
|
+
}
|
|
910
|
+
let pending: Promise<unknown>
|
|
911
|
+
pending = loadHoneyFeature("openapi")
|
|
912
|
+
.then(() =>
|
|
913
|
+
getOpenApiRuntime().generateOpenApi(this, {
|
|
914
|
+
filterRoutes: options.filterRoutes,
|
|
915
|
+
info: {
|
|
916
|
+
description: options.description,
|
|
917
|
+
title: options.title,
|
|
918
|
+
version: options.version,
|
|
919
|
+
},
|
|
920
|
+
securitySchemes: options.securitySchemes,
|
|
921
|
+
}),
|
|
922
|
+
)
|
|
923
|
+
.catch((err: unknown) => {
|
|
924
|
+
if (this._openApiCache?.value === pending) this._openApiCache = null
|
|
925
|
+
throw err
|
|
926
|
+
})
|
|
927
|
+
this._openApiCache = { epoch, value: pending }
|
|
928
|
+
return pending
|
|
929
|
+
}
|
|
930
|
+
const loadYaml = (): Promise<string> => {
|
|
931
|
+
const epoch = this._root.g ?? 0
|
|
932
|
+
if (this._openApiYamlCache && this._openApiYamlCache.epoch === epoch) {
|
|
933
|
+
return this._openApiYamlCache.value
|
|
934
|
+
}
|
|
935
|
+
let pending: Promise<string>
|
|
936
|
+
pending = Promise.resolve()
|
|
937
|
+
.then(async () => getOpenApiRuntime().toYaml(await loadJson()))
|
|
938
|
+
.catch((err: unknown) => {
|
|
939
|
+
if (this._openApiYamlCache?.value === pending) this._openApiYamlCache = null
|
|
940
|
+
throw err
|
|
941
|
+
})
|
|
942
|
+
this._openApiYamlCache = { epoch, value: pending }
|
|
943
|
+
return pending
|
|
944
|
+
}
|
|
945
|
+
this._mountInternalGet(`${stem}.json`, async (ctx) => ctx.res.json("ok", await loadJson()))
|
|
946
|
+
this._mountInternalGet(`${stem}.yml`, async (ctx) =>
|
|
947
|
+
ctx.res.text("ok", await loadYaml(), yamlHeaders),
|
|
948
|
+
)
|
|
949
|
+
this._mountInternalGet(`${stem}.yaml`, async (ctx) =>
|
|
950
|
+
ctx.res.text("ok", await loadYaml(), yamlHeaders),
|
|
951
|
+
)
|
|
952
|
+
if (options.docs) {
|
|
953
|
+
const specUrl = mergePath(this._basePath, `${stem}.json`)
|
|
954
|
+
const docs = options.docs
|
|
955
|
+
const ui = async (ctx: { res: HoneyRes }) => {
|
|
956
|
+
await loadHoneyFeature("openapi")
|
|
957
|
+
return getOpenApiRuntime().docsUi(docs, specUrl)(ctx)
|
|
958
|
+
}
|
|
959
|
+
const preferred = options.docsPath ?? "/docs"
|
|
960
|
+
const candidates = options.docsPath ? [preferred] : [preferred, "/reference"]
|
|
961
|
+
let mounted = false
|
|
962
|
+
for (const p of candidates) {
|
|
963
|
+
if (this._mountInternalGet(p, ui)) {
|
|
964
|
+
mounted = true
|
|
965
|
+
break
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if (!mounted) {
|
|
969
|
+
throw new Error(
|
|
970
|
+
`Honey.openapi({ docs }) cannot mount at ${preferred}: already registered. Pass docsPath.`,
|
|
971
|
+
)
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return this
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
manifest(options?: { path?: string }): this {
|
|
978
|
+
const path = options?.path ?? "/manifest.json"
|
|
979
|
+
const load = (): Promise<unknown> => {
|
|
980
|
+
const epoch = this._root.g ?? 0
|
|
981
|
+
if (this._manifestCache && this._manifestCache.epoch === epoch) {
|
|
982
|
+
return this._manifestCache.value
|
|
983
|
+
}
|
|
984
|
+
let pending: Promise<unknown>
|
|
985
|
+
pending = loadHoneyFeature("openapi")
|
|
986
|
+
.then(() => getOpenApiRuntime().generateManifest(this))
|
|
987
|
+
.catch((err: unknown) => {
|
|
988
|
+
if (this._manifestCache?.value === pending) this._manifestCache = null
|
|
989
|
+
throw err
|
|
990
|
+
})
|
|
991
|
+
this._manifestCache = { epoch, value: pending }
|
|
992
|
+
return pending
|
|
993
|
+
}
|
|
994
|
+
this._mountInternalGet(path, async (ctx) => ctx.res.json("ok", await load()))
|
|
995
|
+
return this
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
async serve(options?: HoneyServeOptions): Promise<ServeHandle> {
|
|
999
|
+
await loadHoneyFeature("serve")
|
|
1000
|
+
return getServeRuntime()(this, options)
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
private _lookupGet(fullPath: string): RouteHandler | null {
|
|
1004
|
+
const fromStatic = this._staticRoutes.map?.[`GET ${fullPath}`]
|
|
1005
|
+
if (fromStatic) return fromStatic
|
|
1006
|
+
const hit = matchRoute(this._root, "GET", fullPath)
|
|
1007
|
+
if (hit !== null && hit.matched) return hit.handler
|
|
1008
|
+
return null
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/** Mount an internal GET. Returns false when a user route already owns the path. */
|
|
1012
|
+
private _mountInternalGet(
|
|
1013
|
+
path: string,
|
|
1014
|
+
fn: (ctx: { res: HoneyRes }) => Response | Promise<Response>,
|
|
1015
|
+
): boolean {
|
|
1016
|
+
const fullPath = mergePath(this._basePath, path)
|
|
1017
|
+
const existing = this._lookupGet(fullPath)
|
|
1018
|
+
if (existing) return existing._skip === true
|
|
1019
|
+
const routeHandler: RouteHandler = {
|
|
1020
|
+
_skip: true,
|
|
1021
|
+
bek: this._defaultBoundaryKey,
|
|
1022
|
+
ef: null,
|
|
1023
|
+
ek: this._defaultErrorKeys,
|
|
1024
|
+
fn: (ctx) => fn(ctx as { res: HoneyRes }),
|
|
1025
|
+
iv: null,
|
|
1026
|
+
mt: null,
|
|
1027
|
+
mw: [...this._chainMiddlewares],
|
|
1028
|
+
os: null,
|
|
1029
|
+
ov: null,
|
|
1030
|
+
rp: fullPath,
|
|
1031
|
+
}
|
|
1032
|
+
insertRoute(this._root, "GET", fullPath, routeHandler)
|
|
1033
|
+
if (!fullPath.includes(":") && !fullPath.includes("*")) {
|
|
1034
|
+
this._registerStatic(`GET ${fullPath}`, routeHandler)
|
|
1035
|
+
}
|
|
1036
|
+
return true
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
defaultErrorFormatter<TSchema extends StandardSchemaLike>(
|
|
1040
|
+
schema: TSchema,
|
|
1041
|
+
fn: (error: HoneyError) => InferOutput<TSchema>,
|
|
1042
|
+
): this;
|
|
1043
|
+
defaultErrorFormatter(fn: ErrorFormatterFn): this;
|
|
1044
|
+
defaultErrorFormatter(
|
|
1045
|
+
schemaOrFn: ErrorFormatterFn | StandardSchemaLike,
|
|
1046
|
+
maybeFn?: (error: HoneyError) => unknown,
|
|
1047
|
+
): this {
|
|
1048
|
+
if (typeof schemaOrFn === "function") {
|
|
1049
|
+
this._errorSchema = null;
|
|
1050
|
+
this._errorFormatter = schemaOrFn;
|
|
1051
|
+
} else {
|
|
1052
|
+
this._errorSchema = schemaOrFn;
|
|
1053
|
+
const mapper = maybeFn as (error: HoneyError) => Record<string, unknown>;
|
|
1054
|
+
this._errorFormatter = (error) => mapper(error);
|
|
1055
|
+
}
|
|
1056
|
+
return this;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
customErrorFormatter<TSchema extends StandardSchemaLike>(
|
|
1060
|
+
schema: TSchema,
|
|
1061
|
+
fn: (error: HoneyError, data: Record<string, unknown>) => InferOutput<TSchema>,
|
|
1062
|
+
): this;
|
|
1063
|
+
customErrorFormatter(fn: CustomErrorFormatter): this;
|
|
1064
|
+
customErrorFormatter(
|
|
1065
|
+
schemaOrFn: CustomErrorFormatter | StandardSchemaLike,
|
|
1066
|
+
maybeFn?: (error: HoneyError, data: Record<string, unknown>) => unknown,
|
|
1067
|
+
): this {
|
|
1068
|
+
if (typeof schemaOrFn === "function") {
|
|
1069
|
+
this._customErrorSchema = null;
|
|
1070
|
+
this._customErrorFormatter = schemaOrFn;
|
|
1071
|
+
} else {
|
|
1072
|
+
this._customErrorSchema = schemaOrFn;
|
|
1073
|
+
const mapper = maybeFn as (error: HoneyError, data: Record<string, unknown>) => Record<string, unknown>;
|
|
1074
|
+
this._customErrorFormatter = (error, data) => mapper(error, data);
|
|
1075
|
+
}
|
|
1076
|
+
return this;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
errorI18n(config: ErrorI18nConfig<TEnv>): this {
|
|
1080
|
+
this._errorI18n = config;
|
|
1081
|
+
return this;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
onError(
|
|
1085
|
+
handler: (
|
|
1086
|
+
error: unknown,
|
|
1087
|
+
ctx: {
|
|
1088
|
+
env: TEnv;
|
|
1089
|
+
jsonFromError: (err: HoneyError) => Response;
|
|
1090
|
+
req: Request;
|
|
1091
|
+
},
|
|
1092
|
+
) => HoneyError | Response | Promise<HoneyError | Response | undefined | void> | undefined | void,
|
|
1093
|
+
): this {
|
|
1094
|
+
this._onError = handler;
|
|
1095
|
+
return this;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
onMethodNotAllowed(
|
|
1099
|
+
handler: (ctx: {
|
|
1100
|
+
allowed: string[];
|
|
1101
|
+
env: TEnv;
|
|
1102
|
+
jsonFromError: (err: HoneyError) => Response;
|
|
1103
|
+
req: Request;
|
|
1104
|
+
}) => Response | Promise<Response>,
|
|
1105
|
+
): this {
|
|
1106
|
+
this._onMethodNotAllowed = handler;
|
|
1107
|
+
return this;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
onNotFound(
|
|
1111
|
+
handler: (ctx: {
|
|
1112
|
+
env: TEnv;
|
|
1113
|
+
jsonFromError: (err: HoneyError) => Response;
|
|
1114
|
+
req: Request;
|
|
1115
|
+
}) => Response | Promise<Response>,
|
|
1116
|
+
): this {
|
|
1117
|
+
this._onNotFound = handler;
|
|
1118
|
+
return this;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/** Register a tap handler keyed by name — fires after successful handler response */
|
|
1122
|
+
tap<K extends string>(
|
|
1123
|
+
key: K,
|
|
1124
|
+
handler: (
|
|
1125
|
+
ctx: TapContext<TEnv>,
|
|
1126
|
+
payload: K extends keyof TTaps ? TTaps[K] : unknown,
|
|
1127
|
+
) => void | Promise<void>,
|
|
1128
|
+
): this {
|
|
1129
|
+
if (this._taps === null) {
|
|
1130
|
+
this._taps = new Map();
|
|
1131
|
+
}
|
|
1132
|
+
this._taps.set(key, handler as (ctx: TapContext<TEnv>, payload: unknown) => void | Promise<void>);
|
|
1133
|
+
return this;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
telemetry(adapter: TelemetryAdapter): this {
|
|
1137
|
+
this._telemetry = adapter;
|
|
1138
|
+
return this;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
routeTree(tree: RouteTree): this {
|
|
1142
|
+
this._root = tree.root;
|
|
1143
|
+
this._handlerMap = tree.handlers ?? null;
|
|
1144
|
+
this._hasRouteTree = true;
|
|
1145
|
+
return this;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
toRouteTree(): RouteTree {
|
|
1149
|
+
return { meta: {}, root: this._root };
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
errorFactory<TFactory extends Record<string, (...args: never[]) => unknown>>(
|
|
1153
|
+
factory: TFactory,
|
|
1154
|
+
): Honey<TEnv, TCtx, TRoutes, TMeta, TFactory, TDefaultErrors, TBasePath, TTaps, TScopedMw> {
|
|
1155
|
+
const next = this as unknown as Honey<
|
|
1156
|
+
TEnv,
|
|
1157
|
+
TCtx,
|
|
1158
|
+
TRoutes,
|
|
1159
|
+
TMeta,
|
|
1160
|
+
TFactory,
|
|
1161
|
+
TDefaultErrors,
|
|
1162
|
+
TBasePath,
|
|
1163
|
+
TTaps,
|
|
1164
|
+
TScopedMw
|
|
1165
|
+
>;
|
|
1166
|
+
next._errorFactory = factory;
|
|
1167
|
+
return next;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
defaultErrors<
|
|
1171
|
+
TKeys extends [TErrorFactory] extends [never]
|
|
1172
|
+
? never
|
|
1173
|
+
: keyof TErrorFactory & string,
|
|
1174
|
+
>(
|
|
1175
|
+
...keys: TKeys[]
|
|
1176
|
+
): Honey<
|
|
1177
|
+
TEnv,
|
|
1178
|
+
TCtx,
|
|
1179
|
+
TRoutes,
|
|
1180
|
+
TMeta,
|
|
1181
|
+
TErrorFactory,
|
|
1182
|
+
TDefaultErrors | TKeys,
|
|
1183
|
+
TBasePath,
|
|
1184
|
+
TTaps,
|
|
1185
|
+
TScopedMw
|
|
1186
|
+
> {
|
|
1187
|
+
const next = this as unknown as Honey<
|
|
1188
|
+
TEnv,
|
|
1189
|
+
TCtx,
|
|
1190
|
+
TRoutes,
|
|
1191
|
+
TMeta,
|
|
1192
|
+
TErrorFactory,
|
|
1193
|
+
TDefaultErrors | TKeys,
|
|
1194
|
+
TBasePath,
|
|
1195
|
+
TTaps,
|
|
1196
|
+
TScopedMw
|
|
1197
|
+
>;
|
|
1198
|
+
for (const k of keys) {
|
|
1199
|
+
next._defaultErrorKeys.add(k);
|
|
1200
|
+
}
|
|
1201
|
+
return next;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
defaultBoundary<
|
|
1205
|
+
TKey extends [TErrorFactory] extends [never]
|
|
1206
|
+
? never
|
|
1207
|
+
: keyof TErrorFactory & string,
|
|
1208
|
+
>(key: TKey): Honey<
|
|
1209
|
+
TEnv,
|
|
1210
|
+
TCtx,
|
|
1211
|
+
TRoutes,
|
|
1212
|
+
TMeta,
|
|
1213
|
+
TErrorFactory,
|
|
1214
|
+
TDefaultErrors | TKey,
|
|
1215
|
+
TBasePath,
|
|
1216
|
+
TTaps,
|
|
1217
|
+
TScopedMw
|
|
1218
|
+
> {
|
|
1219
|
+
const next = this as unknown as Honey<
|
|
1220
|
+
TEnv,
|
|
1221
|
+
TCtx,
|
|
1222
|
+
TRoutes,
|
|
1223
|
+
TMeta,
|
|
1224
|
+
TErrorFactory,
|
|
1225
|
+
TDefaultErrors | TKey,
|
|
1226
|
+
TBasePath,
|
|
1227
|
+
TTaps,
|
|
1228
|
+
TScopedMw
|
|
1229
|
+
>;
|
|
1230
|
+
next._defaultBoundaryKey = key;
|
|
1231
|
+
next._defaultErrorKeys.add(key);
|
|
1232
|
+
return next;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/** Phantom overload — constrains what route-level .meta() accepts */
|
|
1236
|
+
meta<TNewMeta extends Record<string, unknown>>(): Honey<
|
|
1237
|
+
TEnv,
|
|
1238
|
+
TCtx,
|
|
1239
|
+
TRoutes,
|
|
1240
|
+
TNewMeta,
|
|
1241
|
+
TErrorFactory,
|
|
1242
|
+
TDefaultErrors,
|
|
1243
|
+
TBasePath,
|
|
1244
|
+
TTaps,
|
|
1245
|
+
TScopedMw
|
|
1246
|
+
>;
|
|
1247
|
+
/** Typed chain-level default meta — constrains route meta and sets runtime defaults in one call */
|
|
1248
|
+
meta<
|
|
1249
|
+
TNewMeta extends Record<string, unknown>,
|
|
1250
|
+
TValues extends Partial<DefaultMeta> & Partial<TNewMeta>,
|
|
1251
|
+
>(
|
|
1252
|
+
values: TValues,
|
|
1253
|
+
): Honey<
|
|
1254
|
+
TEnv,
|
|
1255
|
+
TCtx,
|
|
1256
|
+
TRoutes,
|
|
1257
|
+
TNewMeta,
|
|
1258
|
+
TErrorFactory,
|
|
1259
|
+
TDefaultErrors,
|
|
1260
|
+
TBasePath,
|
|
1261
|
+
TTaps,
|
|
1262
|
+
TScopedMw
|
|
1263
|
+
>;
|
|
1264
|
+
/** Chain-level default meta — merged into every route registered on this chain */
|
|
1265
|
+
meta<
|
|
1266
|
+
TValues extends Partial<DefaultMeta> &
|
|
1267
|
+
([TMeta] extends [never] ? {} : Partial<TMeta>),
|
|
1268
|
+
>(
|
|
1269
|
+
values: TValues,
|
|
1270
|
+
): Honey<
|
|
1271
|
+
TEnv,
|
|
1272
|
+
TCtx,
|
|
1273
|
+
TRoutes,
|
|
1274
|
+
TMeta,
|
|
1275
|
+
TErrorFactory,
|
|
1276
|
+
TDefaultErrors,
|
|
1277
|
+
TBasePath,
|
|
1278
|
+
TTaps,
|
|
1279
|
+
TScopedMw
|
|
1280
|
+
>;
|
|
1281
|
+
meta(values?: Record<string, unknown>): Honey<
|
|
1282
|
+
TEnv,
|
|
1283
|
+
TCtx,
|
|
1284
|
+
TRoutes,
|
|
1285
|
+
unknown,
|
|
1286
|
+
TErrorFactory,
|
|
1287
|
+
TDefaultErrors,
|
|
1288
|
+
TBasePath,
|
|
1289
|
+
TTaps,
|
|
1290
|
+
TScopedMw
|
|
1291
|
+
> {
|
|
1292
|
+
if (values === undefined) {
|
|
1293
|
+
/* phantom overload — type-only, no runtime effect */
|
|
1294
|
+
return this as Honey<TEnv, TCtx, TRoutes, unknown, TErrorFactory, TDefaultErrors, TBasePath, TTaps, TScopedMw>;
|
|
1295
|
+
}
|
|
1296
|
+
/* chain-level meta — copy-on-write */
|
|
1297
|
+
const next = new Honey<
|
|
1298
|
+
TEnv,
|
|
1299
|
+
TCtx,
|
|
1300
|
+
TRoutes,
|
|
1301
|
+
unknown,
|
|
1302
|
+
TErrorFactory,
|
|
1303
|
+
TDefaultErrors,
|
|
1304
|
+
TBasePath,
|
|
1305
|
+
TTaps,
|
|
1306
|
+
TScopedMw
|
|
1307
|
+
>({
|
|
1308
|
+
chainMiddlewares: this._chainMiddlewares,
|
|
1309
|
+
defaultErrorKeys: this._defaultErrorKeys,
|
|
1310
|
+
globalMiddlewares: this._globalMiddlewares,
|
|
1311
|
+
graph: this._graph,
|
|
1312
|
+
scopedMiddlewares: this._scopedMiddlewares,
|
|
1313
|
+
});
|
|
1314
|
+
next._basePath = this._basePath;
|
|
1315
|
+
next._chainMeta = this._chainMeta
|
|
1316
|
+
? { ...this._chainMeta, ...values }
|
|
1317
|
+
: { ...values };
|
|
1318
|
+
next._contextValues = this._contextValues;
|
|
1319
|
+
next._defaultBoundaryKey = this._defaultBoundaryKey;
|
|
1320
|
+
next._errorFactory = this._errorFactory;
|
|
1321
|
+
next._errorSchema = this._errorSchema;
|
|
1322
|
+
next._customErrorFormatter = this._customErrorFormatter;
|
|
1323
|
+
next._customErrorSchema = this._customErrorSchema;
|
|
1324
|
+
next._errorFormatter = this._errorFormatter;
|
|
1325
|
+
next._errorI18n = this._errorI18n;
|
|
1326
|
+
next._logger = this._logger;
|
|
1327
|
+
next._outputValidation = this._outputValidation;
|
|
1328
|
+
next._stripPrefix = this._stripPrefix;
|
|
1329
|
+
next._trailingSlash = this._trailingSlash;
|
|
1330
|
+
next._wsAdapter = this._wsAdapter;
|
|
1331
|
+
next._onError = this._onError;
|
|
1332
|
+
next._onNotFound = this._onNotFound;
|
|
1333
|
+
next._onMethodNotAllowed = this._onMethodNotAllowed;
|
|
1334
|
+
next._taps = this._taps;
|
|
1335
|
+
next._telemetry = this._telemetry;
|
|
1336
|
+
next._realtimeBus = this._realtimeBus;
|
|
1337
|
+
next._realtimeRoutes = this._realtimeRoutes;
|
|
1338
|
+
next._staticRoutes = this._staticRoutes;
|
|
1339
|
+
next._hasRouteTree = this._hasRouteTree;
|
|
1340
|
+
next._hasWsRoutes = this._hasWsRoutes;
|
|
1341
|
+
return next;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/** Declare tap payload types — auto-extends meta with Partial<T> for meta-driven taps */
|
|
1345
|
+
taps<TNewTaps extends Record<string, unknown>>(): Honey<
|
|
1346
|
+
TEnv,
|
|
1347
|
+
Omit<TCtx, "tap"> & { tap<K extends keyof TNewTaps>(key: K, payload: TNewTaps[K]): void },
|
|
1348
|
+
TRoutes,
|
|
1349
|
+
[TMeta] extends [never] ? Partial<TNewTaps> : TMeta & Partial<TNewTaps>,
|
|
1350
|
+
TErrorFactory,
|
|
1351
|
+
TDefaultErrors,
|
|
1352
|
+
TBasePath,
|
|
1353
|
+
TNewTaps,
|
|
1354
|
+
TScopedMw
|
|
1355
|
+
> {
|
|
1356
|
+
return this as unknown as Honey<
|
|
1357
|
+
TEnv,
|
|
1358
|
+
Omit<TCtx, "tap"> & { tap<K extends keyof TNewTaps>(key: K, payload: TNewTaps[K]): void },
|
|
1359
|
+
TRoutes,
|
|
1360
|
+
[TMeta] extends [never] ? Partial<TNewTaps> : TMeta & Partial<TNewTaps>,
|
|
1361
|
+
TErrorFactory,
|
|
1362
|
+
TDefaultErrors,
|
|
1363
|
+
TBasePath,
|
|
1364
|
+
TNewTaps,
|
|
1365
|
+
TScopedMw
|
|
1366
|
+
>;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
use<TAdds>(
|
|
1370
|
+
mw: MiddlewareFn<TCtx, TAdds>,
|
|
1371
|
+
): Honey<TEnv, TCtx & TAdds, TRoutes, TMeta, TErrorFactory, TDefaultErrors, TBasePath, TTaps, TScopedMw>;
|
|
1372
|
+
|
|
1373
|
+
use<const TPath extends string, TAdds>(
|
|
1374
|
+
path: TPath,
|
|
1375
|
+
mw: MiddlewareFn<TCtx, TAdds>,
|
|
1376
|
+
): Honey<
|
|
1377
|
+
TEnv,
|
|
1378
|
+
TCtx,
|
|
1379
|
+
TRoutes,
|
|
1380
|
+
TMeta,
|
|
1381
|
+
TErrorFactory,
|
|
1382
|
+
TDefaultErrors,
|
|
1383
|
+
TBasePath,
|
|
1384
|
+
TTaps,
|
|
1385
|
+
readonly [...TScopedMw, { readonly path: MergePath<TBasePath, TPath>; readonly adds: TAdds }]
|
|
1386
|
+
>;
|
|
1387
|
+
|
|
1388
|
+
/* oxlint-disable-next-line typescript/no-explicit-any -- overload impl requires erased types */
|
|
1389
|
+
use(pathOrMw: string | MiddlewareFn<any, any>, maybeMw?: MiddlewareFn<any, any>): any {
|
|
1390
|
+
if (typeof pathOrMw !== "string") {
|
|
1391
|
+
const mw = pathOrMw as RuntimeMiddleware;
|
|
1392
|
+
const newChain = new Honey<TEnv>({
|
|
1393
|
+
chainMiddlewares: [...this._chainMiddlewares, mw],
|
|
1394
|
+
defaultErrorKeys: this._defaultErrorKeys,
|
|
1395
|
+
globalMiddlewares: this._globalMiddlewares,
|
|
1396
|
+
graph: this._graph,
|
|
1397
|
+
scopedMiddlewares: this._scopedMiddlewares,
|
|
1398
|
+
});
|
|
1399
|
+
newChain._basePath = this._basePath;
|
|
1400
|
+
newChain._chainMeta = this._chainMeta;
|
|
1401
|
+
newChain._contextValues = this._contextValues;
|
|
1402
|
+
newChain._defaultBoundaryKey = this._defaultBoundaryKey;
|
|
1403
|
+
newChain._errorFactory = this._errorFactory;
|
|
1404
|
+
newChain._errorSchema = this._errorSchema;
|
|
1405
|
+
newChain._customErrorFormatter = this._customErrorFormatter;
|
|
1406
|
+
newChain._customErrorSchema = this._customErrorSchema;
|
|
1407
|
+
newChain._errorFormatter = this._errorFormatter;
|
|
1408
|
+
newChain._errorI18n = this._errorI18n;
|
|
1409
|
+
newChain._logger = this._logger;
|
|
1410
|
+
newChain._outputValidation = this._outputValidation;
|
|
1411
|
+
newChain._stripPrefix = this._stripPrefix;
|
|
1412
|
+
newChain._trailingSlash = this._trailingSlash;
|
|
1413
|
+
newChain._wsAdapter = this._wsAdapter;
|
|
1414
|
+
newChain._onError = this._onError;
|
|
1415
|
+
newChain._onNotFound = this._onNotFound;
|
|
1416
|
+
newChain._onMethodNotAllowed = this._onMethodNotAllowed;
|
|
1417
|
+
newChain._taps = this._taps;
|
|
1418
|
+
newChain._telemetry = this._telemetry;
|
|
1419
|
+
newChain._realtimeBus = this._realtimeBus;
|
|
1420
|
+
newChain._realtimeRoutes = this._realtimeRoutes;
|
|
1421
|
+
newChain._staticRoutes = this._staticRoutes;
|
|
1422
|
+
newChain._hasRouteTree = this._hasRouteTree;
|
|
1423
|
+
newChain._hasWsRoutes = this._hasWsRoutes;
|
|
1424
|
+
return newChain;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/* scoped path */
|
|
1428
|
+
const normalizedPrefix = normalizeScopePath(pathOrMw);
|
|
1429
|
+
const fullPrefix = mergePath(this._basePath, normalizedPrefix);
|
|
1430
|
+
const mw = maybeMw as RuntimeMiddleware;
|
|
1431
|
+
const mwWithErrors = maybeMw as MiddlewareFn<unknown, unknown>;
|
|
1432
|
+
const entry: ScopedEntry = {
|
|
1433
|
+
errors: mwWithErrors.errors ? [...mwWithErrors.errors] : undefined,
|
|
1434
|
+
mw,
|
|
1435
|
+
prefix: fullPrefix,
|
|
1436
|
+
};
|
|
1437
|
+
this._scopedMiddlewares.push(entry);
|
|
1438
|
+
const newChain = new Honey<TEnv>({
|
|
1439
|
+
chainMiddlewares: this._chainMiddlewares,
|
|
1440
|
+
defaultErrorKeys: this._defaultErrorKeys,
|
|
1441
|
+
globalMiddlewares: this._globalMiddlewares,
|
|
1442
|
+
graph: this._graph,
|
|
1443
|
+
scopedMiddlewares: this._scopedMiddlewares,
|
|
1444
|
+
});
|
|
1445
|
+
newChain._basePath = this._basePath;
|
|
1446
|
+
newChain._chainMeta = this._chainMeta;
|
|
1447
|
+
newChain._contextValues = this._contextValues;
|
|
1448
|
+
newChain._defaultBoundaryKey = this._defaultBoundaryKey;
|
|
1449
|
+
newChain._errorFactory = this._errorFactory;
|
|
1450
|
+
newChain._errorSchema = this._errorSchema;
|
|
1451
|
+
newChain._customErrorFormatter = this._customErrorFormatter;
|
|
1452
|
+
newChain._customErrorSchema = this._customErrorSchema;
|
|
1453
|
+
newChain._errorFormatter = this._errorFormatter;
|
|
1454
|
+
newChain._errorI18n = this._errorI18n;
|
|
1455
|
+
newChain._logger = this._logger;
|
|
1456
|
+
newChain._outputValidation = this._outputValidation;
|
|
1457
|
+
newChain._stripPrefix = this._stripPrefix;
|
|
1458
|
+
newChain._trailingSlash = this._trailingSlash;
|
|
1459
|
+
newChain._wsAdapter = this._wsAdapter;
|
|
1460
|
+
newChain._onError = this._onError;
|
|
1461
|
+
newChain._onNotFound = this._onNotFound;
|
|
1462
|
+
newChain._onMethodNotAllowed = this._onMethodNotAllowed;
|
|
1463
|
+
newChain._taps = this._taps;
|
|
1464
|
+
newChain._telemetry = this._telemetry;
|
|
1465
|
+
newChain._realtimeBus = this._realtimeBus;
|
|
1466
|
+
newChain._realtimeRoutes = this._realtimeRoutes;
|
|
1467
|
+
newChain._staticRoutes = this._staticRoutes;
|
|
1468
|
+
newChain._hasRouteTree = this._hasRouteTree;
|
|
1469
|
+
newChain._hasWsRoutes = this._hasWsRoutes;
|
|
1470
|
+
newChain._applyScopedEntryErrors(entry);
|
|
1471
|
+
return newChain;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
route<
|
|
1475
|
+
TSubRoutes,
|
|
1476
|
+
TSubMeta,
|
|
1477
|
+
TSubErrorFactory,
|
|
1478
|
+
TSubDefaultErrors extends string,
|
|
1479
|
+
TSubBasePath extends string,
|
|
1480
|
+
>(
|
|
1481
|
+
sub: Honey<
|
|
1482
|
+
TEnv,
|
|
1483
|
+
TCtx,
|
|
1484
|
+
TSubRoutes,
|
|
1485
|
+
TSubMeta,
|
|
1486
|
+
TSubErrorFactory,
|
|
1487
|
+
TSubDefaultErrors,
|
|
1488
|
+
TSubBasePath
|
|
1489
|
+
>,
|
|
1490
|
+
): Honey<
|
|
1491
|
+
TEnv,
|
|
1492
|
+
TCtx,
|
|
1493
|
+
TRoutes & TSubRoutes,
|
|
1494
|
+
TMeta,
|
|
1495
|
+
TErrorFactory,
|
|
1496
|
+
TDefaultErrors,
|
|
1497
|
+
TBasePath,
|
|
1498
|
+
TTaps,
|
|
1499
|
+
TScopedMw
|
|
1500
|
+
> {
|
|
1501
|
+
/* skip self-merge: .handler() already inserted into shared _root */
|
|
1502
|
+
if (sub._tree !== this._root) {
|
|
1503
|
+
mergeInto(this._root, sub._tree);
|
|
1504
|
+
if (sub._hasWsRoutes) this._hasWsRoutes = true;
|
|
1505
|
+
if (sub._hasRouteTree) this._hasRouteTree = true;
|
|
1506
|
+
/* carry sub's scoped mw entries into parent's runtime list (parent scopes run first) */
|
|
1507
|
+
for (const entry of sub._scopedMiddlewares) {
|
|
1508
|
+
this._scopedMiddlewares.push(entry);
|
|
1509
|
+
}
|
|
1510
|
+
if (sub._staticRoutes.map !== null) {
|
|
1511
|
+
if (this._staticRoutes.map === null) {
|
|
1512
|
+
this._staticRoutes.map = Object.create(null) as Record<string, RouteHandler>;
|
|
1513
|
+
}
|
|
1514
|
+
Object.assign(this._staticRoutes.map, sub._staticRoutes.map);
|
|
1515
|
+
}
|
|
1516
|
+
for (const [path, cfg] of sub._realtimeRoutes) {
|
|
1517
|
+
if (this._realtimeRoutes.has(path)) {
|
|
1518
|
+
throw new Error(`Duplicate realtime route: ${path}`);
|
|
1519
|
+
}
|
|
1520
|
+
this._realtimeRoutes.set(path, cfg);
|
|
1521
|
+
}
|
|
1522
|
+
if (!this._realtimeBus && sub._realtimeBus) {
|
|
1523
|
+
this._realtimeBus = sub._realtimeBus;
|
|
1524
|
+
}
|
|
1525
|
+
if (sub._taps !== null) {
|
|
1526
|
+
if (this._taps === null) this._taps = new Map();
|
|
1527
|
+
for (const [key, fn] of sub._taps) {
|
|
1528
|
+
if (!this._taps.has(key)) this._taps.set(key, fn);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
/* walk tree and apply all scoped error keys to matching handlers */
|
|
1532
|
+
this._applyAllScopedErrors();
|
|
1533
|
+
}
|
|
1534
|
+
return this as unknown as Honey<
|
|
1535
|
+
TEnv,
|
|
1536
|
+
TCtx,
|
|
1537
|
+
TRoutes & TSubRoutes,
|
|
1538
|
+
TMeta,
|
|
1539
|
+
TErrorFactory,
|
|
1540
|
+
TDefaultErrors,
|
|
1541
|
+
TBasePath,
|
|
1542
|
+
TTaps,
|
|
1543
|
+
TScopedMw
|
|
1544
|
+
>;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
private _registerRoute<
|
|
1548
|
+
TPath extends string,
|
|
1549
|
+
TMethod extends HttpMethod | "ALL",
|
|
1550
|
+
>(
|
|
1551
|
+
method: TMethod,
|
|
1552
|
+
path: TPath,
|
|
1553
|
+
extraMethods?: (HttpMethod | "ALL")[],
|
|
1554
|
+
): BuilderChain<
|
|
1555
|
+
TEnv,
|
|
1556
|
+
TCtx & ApplyScoped<TScopedMw, MergePath<TBasePath, TPath>>,
|
|
1557
|
+
{},
|
|
1558
|
+
never,
|
|
1559
|
+
{},
|
|
1560
|
+
MergePath<TBasePath, TPath>,
|
|
1561
|
+
TMethod,
|
|
1562
|
+
TRoutes,
|
|
1563
|
+
never,
|
|
1564
|
+
TCtx,
|
|
1565
|
+
TMeta,
|
|
1566
|
+
InitAccMeta<TMeta>,
|
|
1567
|
+
TErrorFactory,
|
|
1568
|
+
TDefaultErrors,
|
|
1569
|
+
TBasePath,
|
|
1570
|
+
TTaps,
|
|
1571
|
+
TScopedMw
|
|
1572
|
+
> {
|
|
1573
|
+
const fullPath = mergePath(this._basePath, path);
|
|
1574
|
+
const errorKeys = new Set(this._defaultErrorKeys);
|
|
1575
|
+
for (const entry of this._scopedMiddlewares) {
|
|
1576
|
+
if (entry.errors && scopeMatches(entry.prefix, fullPath)) {
|
|
1577
|
+
for (const k of entry.errors) errorKeys.add(k);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
return new RouteBuilder<
|
|
1581
|
+
TEnv,
|
|
1582
|
+
TCtx & ApplyScoped<TScopedMw, MergePath<TBasePath, TPath>>,
|
|
1583
|
+
{},
|
|
1584
|
+
never,
|
|
1585
|
+
{},
|
|
1586
|
+
MergePath<TBasePath, TPath>,
|
|
1587
|
+
TMethod,
|
|
1588
|
+
TRoutes,
|
|
1589
|
+
never,
|
|
1590
|
+
TCtx,
|
|
1591
|
+
TMeta,
|
|
1592
|
+
InitAccMeta<TMeta>,
|
|
1593
|
+
TErrorFactory,
|
|
1594
|
+
TDefaultErrors,
|
|
1595
|
+
TBasePath,
|
|
1596
|
+
TTaps,
|
|
1597
|
+
TScopedMw
|
|
1598
|
+
>({
|
|
1599
|
+
boundaryErrorKey: this._defaultBoundaryKey,
|
|
1600
|
+
errorKeys,
|
|
1601
|
+
extraMethods: extraMethods ?? null,
|
|
1602
|
+
handlerMap: this._handlerMap,
|
|
1603
|
+
inputSchemas: null,
|
|
1604
|
+
meta: this._chainMeta ? { ...this._chainMeta } : null,
|
|
1605
|
+
method,
|
|
1606
|
+
middlewares: [],
|
|
1607
|
+
outputSchemas: null,
|
|
1608
|
+
parent: this,
|
|
1609
|
+
parentMiddlewares: this._chainMiddlewares,
|
|
1610
|
+
path: fullPath,
|
|
1611
|
+
root: this._root,
|
|
1612
|
+
}) as unknown as BuilderChain<
|
|
1613
|
+
TEnv,
|
|
1614
|
+
TCtx & ApplyScoped<TScopedMw, MergePath<TBasePath, TPath>>,
|
|
1615
|
+
{},
|
|
1616
|
+
never,
|
|
1617
|
+
{},
|
|
1618
|
+
MergePath<TBasePath, TPath>,
|
|
1619
|
+
TMethod,
|
|
1620
|
+
TRoutes,
|
|
1621
|
+
never,
|
|
1622
|
+
TCtx,
|
|
1623
|
+
TMeta,
|
|
1624
|
+
InitAccMeta<TMeta>,
|
|
1625
|
+
TErrorFactory,
|
|
1626
|
+
TDefaultErrors,
|
|
1627
|
+
TBasePath,
|
|
1628
|
+
TTaps,
|
|
1629
|
+
TScopedMw
|
|
1630
|
+
>;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
on<
|
|
1634
|
+
const TPath extends string,
|
|
1635
|
+
const TMethods extends readonly [HttpMethod | "ALL", ...(HttpMethod | "ALL")[]],
|
|
1636
|
+
>(
|
|
1637
|
+
methods: TMethods,
|
|
1638
|
+
path: TPath,
|
|
1639
|
+
): BuilderChain<
|
|
1640
|
+
TEnv,
|
|
1641
|
+
TCtx & ApplyScoped<TScopedMw, MergePath<TBasePath, TPath>>,
|
|
1642
|
+
{},
|
|
1643
|
+
never,
|
|
1644
|
+
{},
|
|
1645
|
+
MergePath<TBasePath, TPath>,
|
|
1646
|
+
TMethods[number],
|
|
1647
|
+
TRoutes,
|
|
1648
|
+
never,
|
|
1649
|
+
TCtx,
|
|
1650
|
+
TMeta,
|
|
1651
|
+
InitAccMeta<TMeta>,
|
|
1652
|
+
TErrorFactory,
|
|
1653
|
+
TDefaultErrors,
|
|
1654
|
+
TBasePath,
|
|
1655
|
+
TTaps,
|
|
1656
|
+
TScopedMw
|
|
1657
|
+
> {
|
|
1658
|
+
const [first, ...rest] = methods;
|
|
1659
|
+
return this._registerRoute<TPath, TMethods[number]>(
|
|
1660
|
+
first,
|
|
1661
|
+
path,
|
|
1662
|
+
rest.length > 0 ? (rest as (HttpMethod | "ALL")[]) : undefined,
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
all<const TPath extends string>(path: TPath) {
|
|
1667
|
+
return this._registerRoute<TPath, "ALL">("ALL", path);
|
|
1668
|
+
}
|
|
1669
|
+
delete<const TPath extends string>(path: TPath) {
|
|
1670
|
+
return this._registerRoute<TPath, "DELETE">("DELETE", path);
|
|
1671
|
+
}
|
|
1672
|
+
get<const TPath extends string>(path: TPath) {
|
|
1673
|
+
return this._registerRoute<TPath, "GET">("GET", path);
|
|
1674
|
+
}
|
|
1675
|
+
head<const TPath extends string>(path: TPath) {
|
|
1676
|
+
return this._registerRoute<TPath, "HEAD">("HEAD", path);
|
|
1677
|
+
}
|
|
1678
|
+
options<const TPath extends string>(path: TPath) {
|
|
1679
|
+
return this._registerRoute<TPath, "OPTIONS">("OPTIONS", path);
|
|
1680
|
+
}
|
|
1681
|
+
patch<const TPath extends string>(path: TPath) {
|
|
1682
|
+
return this._registerRoute<TPath, "PATCH">("PATCH", path);
|
|
1683
|
+
}
|
|
1684
|
+
post<const TPath extends string>(path: TPath) {
|
|
1685
|
+
return this._registerRoute<TPath, "POST">("POST", path);
|
|
1686
|
+
}
|
|
1687
|
+
put<const TPath extends string>(path: TPath) {
|
|
1688
|
+
return this._registerRoute<TPath, "PUT">("PUT", path);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
ws<const TPath extends string>(
|
|
1692
|
+
path: TPath,
|
|
1693
|
+
): WSRouteBuilder<
|
|
1694
|
+
TEnv,
|
|
1695
|
+
TCtx & ApplyScoped<TScopedMw, MergePath<TBasePath, TPath>>,
|
|
1696
|
+
{},
|
|
1697
|
+
never,
|
|
1698
|
+
Honey<TEnv, TCtx, TRoutes, TMeta, TErrorFactory, TDefaultErrors, TBasePath, TTaps, TScopedMw>
|
|
1699
|
+
> {
|
|
1700
|
+
return new WSRouteBuilder<
|
|
1701
|
+
TEnv,
|
|
1702
|
+
TCtx & ApplyScoped<TScopedMw, MergePath<TBasePath, TPath>>,
|
|
1703
|
+
{},
|
|
1704
|
+
never,
|
|
1705
|
+
Honey<
|
|
1706
|
+
TEnv,
|
|
1707
|
+
TCtx,
|
|
1708
|
+
TRoutes,
|
|
1709
|
+
TMeta,
|
|
1710
|
+
TErrorFactory,
|
|
1711
|
+
TDefaultErrors,
|
|
1712
|
+
TBasePath,
|
|
1713
|
+
TTaps,
|
|
1714
|
+
TScopedMw
|
|
1715
|
+
>
|
|
1716
|
+
>({
|
|
1717
|
+
boundaryErrorKey: this._defaultBoundaryKey,
|
|
1718
|
+
errorKeys: new Set(),
|
|
1719
|
+
inputSchemas: null,
|
|
1720
|
+
meta: null,
|
|
1721
|
+
middlewares: [],
|
|
1722
|
+
parent: this,
|
|
1723
|
+
parentMiddlewares: this._chainMiddlewares,
|
|
1724
|
+
path: mergePath(this._basePath, path),
|
|
1725
|
+
root: this._root,
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
realtime(path: string, opts: RealtimeRouteOpts): this {
|
|
1730
|
+
const fullPath = mergePath(this._basePath, path) || "/";
|
|
1731
|
+
|
|
1732
|
+
if (!this._realtimeBus) {
|
|
1733
|
+
this._realtimeBus = createBus();
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
if (this._realtimeRoutes.has(fullPath)) {
|
|
1737
|
+
throw new Error(`Duplicate realtime route: ${fullPath}`);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
this._realtimeRoutes.set(fullPath, {
|
|
1741
|
+
handler: opts.handler,
|
|
1742
|
+
middlewares: opts.use,
|
|
1743
|
+
reconnectBuffer: opts.reconnectBuffer,
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
const mw: RuntimeMiddleware[] = [];
|
|
1747
|
+
if (opts.use) {
|
|
1748
|
+
for (const fn of opts.use) {
|
|
1749
|
+
mw.push(fn as RuntimeMiddleware);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
insertWsRoute(this._root, fullPath, {
|
|
1754
|
+
bek: this._defaultBoundaryKey,
|
|
1755
|
+
ek: new Set(),
|
|
1756
|
+
fn: Object.create(null),
|
|
1757
|
+
iv: null,
|
|
1758
|
+
mt: null,
|
|
1759
|
+
mw: [...this._chainMiddlewares, ...mw],
|
|
1760
|
+
rp: fullPath,
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
this._hasWsRoutes = true;
|
|
1764
|
+
return this;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/**
|
|
1768
|
+
* Deno must call `Deno.upgradeWebSocket` in the same turn as the serve
|
|
1769
|
+
* callback. When the adapter implements `preUpgrade`, we do that here
|
|
1770
|
+
* and return the 101 response before any middleware Promise.
|
|
1771
|
+
*/
|
|
1772
|
+
fetch(
|
|
1773
|
+
request: Request,
|
|
1774
|
+
env: TEnv,
|
|
1775
|
+
executionCtx?: { waitUntil?: (p: Promise<unknown>) => void },
|
|
1776
|
+
): Response | Promise<Response> {
|
|
1777
|
+
if (this._wsAdapter === null && !this._hasWsRoutes) {
|
|
1778
|
+
return this._doFetch(request, env, executionCtx);
|
|
1779
|
+
}
|
|
1780
|
+
const isWsUpgrade = request.headers.get("upgrade")?.toLowerCase() === "websocket"
|
|
1781
|
+
const path = this.pathFromRequest(request)
|
|
1782
|
+
const canPreUpgrade =
|
|
1783
|
+
isWsUpgrade &&
|
|
1784
|
+
this._wsAdapter?.preUpgrade !== undefined &&
|
|
1785
|
+
!this.trailingSlashRedirects(path) &&
|
|
1786
|
+
matchWsRoute(this._root, this.pathAfterPrefix(path)) !== null
|
|
1787
|
+
const pre = canPreUpgrade ? this._wsAdapter?.preUpgrade?.(request) : undefined
|
|
1788
|
+
const work = this._doFetch(request, env, executionCtx)
|
|
1789
|
+
if (!pre) return work
|
|
1790
|
+
void Promise.resolve(work)
|
|
1791
|
+
.then((res) => {
|
|
1792
|
+
if (res.status === 101) return
|
|
1793
|
+
const code = res.status === 401 ? 4401 : 1008
|
|
1794
|
+
const reason = res.status === 401 ? "unauthorized" : "upgrade rejected"
|
|
1795
|
+
const reject = () => {
|
|
1796
|
+
try {
|
|
1797
|
+
pre.socket.close(code, reason)
|
|
1798
|
+
} catch {
|
|
1799
|
+
/* already closed */
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
if (pre.whenOpen) pre.whenOpen(reject)
|
|
1803
|
+
else reject()
|
|
1804
|
+
})
|
|
1805
|
+
.catch(() => {
|
|
1806
|
+
try {
|
|
1807
|
+
pre.socket.close(1011, "internal error")
|
|
1808
|
+
} catch {
|
|
1809
|
+
/* already closed */
|
|
1810
|
+
}
|
|
1811
|
+
})
|
|
1812
|
+
return pre.response
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
private pathFromRequest(request: Request): string {
|
|
1816
|
+
const rawUrl = request.url
|
|
1817
|
+
const protoEnd = rawUrl.indexOf("//")
|
|
1818
|
+
const pathStart = protoEnd === -1 ? 0 : rawUrl.indexOf("/", protoEnd + 2)
|
|
1819
|
+
const searchOrHash = pathStart === -1 ? -1 : findSearchOrHash(rawUrl, pathStart)
|
|
1820
|
+
if (pathStart === -1) return "/"
|
|
1821
|
+
if (searchOrHash === -1) return rawUrl.substring(pathStart)
|
|
1822
|
+
return rawUrl.substring(pathStart, searchOrHash)
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
private trailingSlashRedirects(path: string): boolean {
|
|
1826
|
+
if (path.length <= 1) return false
|
|
1827
|
+
if (this._trailingSlash === "strip" && path.endsWith("/")) return true
|
|
1828
|
+
if (this._trailingSlash === "enforce" && !path.endsWith("/")) return true
|
|
1829
|
+
return false
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
private pathAfterPrefix(path: string): string {
|
|
1833
|
+
if (this._stripPrefix === null) return path
|
|
1834
|
+
if (path === this._stripPrefix) return "/"
|
|
1835
|
+
if (path.startsWith(this._stripPrefix) && path.charCodeAt(this._stripPrefix.length) === 47) {
|
|
1836
|
+
return path.slice(this._stripPrefix.length)
|
|
1837
|
+
}
|
|
1838
|
+
return path
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
private _doFetch(
|
|
1842
|
+
request: Request,
|
|
1843
|
+
env: TEnv,
|
|
1844
|
+
executionCtx?: { waitUntil?: (p: Promise<unknown>) => void },
|
|
1845
|
+
): Response | Promise<Response> {
|
|
1846
|
+
const startTime = performance.now();
|
|
1847
|
+
|
|
1848
|
+
/* fast path extraction — avoids expensive new URL() allocation */
|
|
1849
|
+
const rawUrl = request.url;
|
|
1850
|
+
const protoEnd = rawUrl.indexOf("//");
|
|
1851
|
+
const pathStart = protoEnd === -1 ? 0 : rawUrl.indexOf("/", protoEnd + 2);
|
|
1852
|
+
const searchOrHash =
|
|
1853
|
+
pathStart === -1 ? -1 : findSearchOrHash(rawUrl, pathStart);
|
|
1854
|
+
let path: string;
|
|
1855
|
+
if (pathStart === -1) {
|
|
1856
|
+
path = "/";
|
|
1857
|
+
} else if (searchOrHash === -1) {
|
|
1858
|
+
path = rawUrl.substring(pathStart);
|
|
1859
|
+
} else {
|
|
1860
|
+
path = rawUrl.substring(pathStart, searchOrHash);
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
/* lazily create URL only when actually needed (search params, redirects) */
|
|
1864
|
+
let _url: URL | undefined;
|
|
1865
|
+
const getUrl = (): URL => {
|
|
1866
|
+
if (_url === undefined) _url = new URL(rawUrl);
|
|
1867
|
+
return _url;
|
|
1868
|
+
};
|
|
1869
|
+
|
|
1870
|
+
const log = this._logger ?? undefined;
|
|
1871
|
+
const fc: FetchCtx<TEnv> = {
|
|
1872
|
+
env,
|
|
1873
|
+
executionCtx,
|
|
1874
|
+
log,
|
|
1875
|
+
request,
|
|
1876
|
+
startTime,
|
|
1877
|
+
url: getUrl,
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1880
|
+
if (this._telemetry !== null) {
|
|
1881
|
+
safeFire(() => this._telemetry?.onRequest?.({ env, req: request }), log);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
/* trailing slash handling */
|
|
1885
|
+
if (path.length > 1) {
|
|
1886
|
+
if (this._trailingSlash === "strip" && path.endsWith("/")) {
|
|
1887
|
+
const redirectUrl = getUrl();
|
|
1888
|
+
redirectUrl.pathname = path.slice(0, -1);
|
|
1889
|
+
return new Response(null, {
|
|
1890
|
+
headers: { location: redirectUrl.toString() },
|
|
1891
|
+
status: 308,
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
if (this._trailingSlash === "enforce" && !path.endsWith("/")) {
|
|
1895
|
+
const redirectUrl = getUrl();
|
|
1896
|
+
redirectUrl.pathname = `${path}/`;
|
|
1897
|
+
return new Response(null, {
|
|
1898
|
+
headers: { location: redirectUrl.toString() },
|
|
1899
|
+
status: 308,
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
/* prefix stripping — must run AFTER trailing slash so redirects preserve the full prefixed URL */
|
|
1905
|
+
if (this._stripPrefix !== null) {
|
|
1906
|
+
if (path === this._stripPrefix) {
|
|
1907
|
+
path = "/";
|
|
1908
|
+
} else if (
|
|
1909
|
+
path.startsWith(this._stripPrefix) &&
|
|
1910
|
+
path.charCodeAt(this._stripPrefix.length) === 47
|
|
1911
|
+
) {
|
|
1912
|
+
path = path.slice(this._stripPrefix.length);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
/* WebSocket route check */
|
|
1917
|
+
const isWsUpgrade = request.headers.get("upgrade") === "websocket";
|
|
1918
|
+
if (isWsUpgrade) {
|
|
1919
|
+
const wsMatch = matchWsRoute(this._root, path);
|
|
1920
|
+
if (wsMatch !== null) {
|
|
1921
|
+
return this._handleWs(fc, wsMatch);
|
|
1922
|
+
}
|
|
1923
|
+
} else if (this._realtimeRoutes.size > 0) {
|
|
1924
|
+
/* Non-upgrade request hitting a realtime-only path → 426 Upgrade Required */
|
|
1925
|
+
const wsMatch = matchWsRoute(this._root, path);
|
|
1926
|
+
if (wsMatch !== null && this._realtimeRoutes.has(wsMatch.handler.rp)) {
|
|
1927
|
+
return new Response(null, { headers: { upgrade: "websocket" }, status: 426 });
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
const method = request.method.toUpperCase() as HttpMethod;
|
|
1932
|
+
|
|
1933
|
+
/* fn:null fallthrough — tree match sets meta, catch-all dispatches */
|
|
1934
|
+
let fnNullMeta: Record<string, unknown> | null = null;
|
|
1935
|
+
let fnNullParams: Record<string, string> | null = null;
|
|
1936
|
+
let fnNullHit = false;
|
|
1937
|
+
|
|
1938
|
+
/* Tier 2: O(1) static route lookup — checks both precompiled and runtime maps */
|
|
1939
|
+
const smap = this._staticRoutes.map ?? this._handlerMap;
|
|
1940
|
+
if (smap !== null) {
|
|
1941
|
+
const key = `${method} ${path}`;
|
|
1942
|
+
const staticHandler = smap[key];
|
|
1943
|
+
if (staticHandler) {
|
|
1944
|
+
if (staticHandler.fn === null) {
|
|
1945
|
+
fnNullMeta = staticHandler.mt;
|
|
1946
|
+
fnNullParams = EMPTY_PARAMS;
|
|
1947
|
+
fnNullHit = true;
|
|
1948
|
+
} else {
|
|
1949
|
+
return this._handleMatched(
|
|
1950
|
+
fc,
|
|
1951
|
+
method,
|
|
1952
|
+
path,
|
|
1953
|
+
staticHandler,
|
|
1954
|
+
EMPTY_PARAMS,
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
/* HEAD falls back to GET */
|
|
1959
|
+
if (!fnNullHit && method === "HEAD") {
|
|
1960
|
+
const getHandler = smap[`GET ${path}`];
|
|
1961
|
+
if (getHandler) {
|
|
1962
|
+
if (getHandler.fn === null) {
|
|
1963
|
+
fnNullMeta = getHandler.mt;
|
|
1964
|
+
fnNullParams = EMPTY_PARAMS;
|
|
1965
|
+
fnNullHit = true;
|
|
1966
|
+
} else {
|
|
1967
|
+
return this._handleMatched(
|
|
1968
|
+
fc,
|
|
1969
|
+
method,
|
|
1970
|
+
path,
|
|
1971
|
+
getHandler,
|
|
1972
|
+
EMPTY_PARAMS,
|
|
1973
|
+
);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
if (!fnNullHit) {
|
|
1980
|
+
const result = matchRoute(this._root, method, path);
|
|
1981
|
+
|
|
1982
|
+
if (result?.matched) {
|
|
1983
|
+
/*
|
|
1984
|
+
* routeTree loaded → wildcard matches for unknown paths must 404.
|
|
1985
|
+
* Only fn:null fallthroughs (known routes) should reach the catch-all.
|
|
1986
|
+
* Detect wildcard: handler has no mt and fn is NOT null (builder-registered catch-all).
|
|
1987
|
+
*/
|
|
1988
|
+
const isWildcardCatchAll =
|
|
1989
|
+
this._hasRouteTree &&
|
|
1990
|
+
this._root.w !== null &&
|
|
1991
|
+
result.handler.fn !== null &&
|
|
1992
|
+
(this._root.w.m[method] === result.handler ||
|
|
1993
|
+
this._root.w.m["ALL"] === result.handler);
|
|
1994
|
+
|
|
1995
|
+
if (isWildcardCatchAll) {
|
|
1996
|
+
/* path not in routeTree — 404 */
|
|
1997
|
+
return this._handle404(fc, method, path);
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
if (result.handler.fn === null) {
|
|
2001
|
+
fnNullMeta = result.handler.mt;
|
|
2002
|
+
fnNullParams = result.params;
|
|
2003
|
+
fnNullHit = true;
|
|
2004
|
+
} else {
|
|
2005
|
+
return this._handleMatched(
|
|
2006
|
+
fc,
|
|
2007
|
+
method,
|
|
2008
|
+
path,
|
|
2009
|
+
result.handler,
|
|
2010
|
+
result.params,
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
if (!fnNullHit) {
|
|
2016
|
+
if (result === null) {
|
|
2017
|
+
if (!isWsUpgrade) {
|
|
2018
|
+
const wsMatch = matchWsRoute(this._root, path);
|
|
2019
|
+
if (wsMatch !== null) {
|
|
2020
|
+
return new Response("Upgrade Required", {
|
|
2021
|
+
headers: { connection: "Upgrade", upgrade: "websocket" },
|
|
2022
|
+
status: 426,
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
return this._handle404(fc, method, path);
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
if (!result.matched) {
|
|
2030
|
+
if (
|
|
2031
|
+
method === "OPTIONS" &&
|
|
2032
|
+
fc.request.headers.has("access-control-request-method") &&
|
|
2033
|
+
result.allowed.length > 0
|
|
2034
|
+
) {
|
|
2035
|
+
const fallback = this._preflightFallbackMethod(result.allowed);
|
|
2036
|
+
const retry = matchRoute(this._root, fallback, path);
|
|
2037
|
+
if (retry?.matched && retry.handler.fn !== null) {
|
|
2038
|
+
return this._handleCorsPreflight(
|
|
2039
|
+
fc,
|
|
2040
|
+
path,
|
|
2041
|
+
retry.handler,
|
|
2042
|
+
retry.params,
|
|
2043
|
+
result.allowed,
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
return this._handle405(fc, method, path, result.allowed);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
/* fn:null hit — find catch-all/wildcard to dispatch with stashed meta */
|
|
2053
|
+
const wildcardResult = matchRoute(this._root, method, "/*");
|
|
2054
|
+
if (wildcardResult?.matched && wildcardResult.handler.fn !== null) {
|
|
2055
|
+
return this._handleMatched(
|
|
2056
|
+
fc,
|
|
2057
|
+
method,
|
|
2058
|
+
path,
|
|
2059
|
+
wildcardResult.handler,
|
|
2060
|
+
fnNullParams ?? EMPTY_PARAMS,
|
|
2061
|
+
fnNullMeta,
|
|
2062
|
+
);
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
return this._handle404(fc, method, path);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
private _makeErrorCtx(fc: FetchCtx<TEnv>, allowed?: string[]) {
|
|
2069
|
+
return {
|
|
2070
|
+
env: fc.env,
|
|
2071
|
+
jsonFromError: (err: HoneyError) =>
|
|
2072
|
+
createErrorResponse(err, this._errorFormatter, this._customErrorFormatter),
|
|
2073
|
+
req: fc.request,
|
|
2074
|
+
...(allowed ? { allowed } : {}),
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
private async _handleWs(
|
|
2079
|
+
fc: FetchCtx<TEnv>,
|
|
2080
|
+
wsMatch: { handler: WSRouteHandler; params: Record<string, string> },
|
|
2081
|
+
): Promise<Response> {
|
|
2082
|
+
/* Dispatch to realtime handler if this path is registered as a realtime route */
|
|
2083
|
+
const realtimeConfig = this._realtimeRoutes.get(wsMatch.handler.rp);
|
|
2084
|
+
if (realtimeConfig) {
|
|
2085
|
+
return this._handleRealtime(fc, wsMatch, realtimeConfig);
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
const isUpgrade =
|
|
2089
|
+
fc.request.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
2090
|
+
if (!isUpgrade) {
|
|
2091
|
+
return new Response(null, {
|
|
2092
|
+
headers: { upgrade: "websocket" },
|
|
2093
|
+
status: 426,
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
const wsAdapter = this._wsAdapter;
|
|
2098
|
+
if (!wsAdapter) {
|
|
2099
|
+
fc.log?.warn?.("WebSocket adapter not configured — call .wsAdapter()");
|
|
2100
|
+
return createErrorResponse(
|
|
2101
|
+
this._createError(EK.internal_server_error, SK.internal_server_error),
|
|
2102
|
+
this._errorFormatter,
|
|
2103
|
+
this._customErrorFormatter,
|
|
2104
|
+
);
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
const wsCtx = new HoneyContext({
|
|
2108
|
+
env: fc.env,
|
|
2109
|
+
executionCtx: fc.executionCtx,
|
|
2110
|
+
params: wsMatch.params,
|
|
2111
|
+
req: fc.request,
|
|
2112
|
+
urlFn: fc.url,
|
|
2113
|
+
});
|
|
2114
|
+
if (this._contextValues) Object.assign(wsCtx, this._contextValues);
|
|
2115
|
+
|
|
2116
|
+
/*
|
|
2117
|
+
* WS ordering: [global → scoped → chain+handler-route-specific]
|
|
2118
|
+
* WS bakes chain mw into handler.mw at registration, so scoped runs before chain.
|
|
2119
|
+
* This is an unavoidable inconsistency vs HTTP (where chain runs before scoped).
|
|
2120
|
+
*/
|
|
2121
|
+
const scopedForPath = this._filterScopedForPath(wsMatch.handler.rp);
|
|
2122
|
+
const allWsMw: RuntimeMiddleware[] = [
|
|
2123
|
+
...this._globalMiddlewares,
|
|
2124
|
+
...scopedForPath,
|
|
2125
|
+
...wsMatch.handler.mw,
|
|
2126
|
+
];
|
|
2127
|
+
|
|
2128
|
+
try {
|
|
2129
|
+
return await executeChain(allWsMw, wsCtx, async (finalCtx) => {
|
|
2130
|
+
const userHandler = wsMatch.handler.fn;
|
|
2131
|
+
let messageQueue: Promise<void> = Promise.resolve();
|
|
2132
|
+
|
|
2133
|
+
const onOpenFn = userHandler.onOpen;
|
|
2134
|
+
const onMsgFn = userHandler.onMessage;
|
|
2135
|
+
const onCloseFn = userHandler.onClose;
|
|
2136
|
+
const onErrorFn = userHandler.onError;
|
|
2137
|
+
const onReconnectFn = userHandler.onReconnect;
|
|
2138
|
+
const reconnectToken = fc.url().searchParams.get("reconnect_token");
|
|
2139
|
+
|
|
2140
|
+
const wrappedHandler: WSHandler<unknown> = {};
|
|
2141
|
+
|
|
2142
|
+
if (reconnectToken && onReconnectFn) {
|
|
2143
|
+
wrappedHandler.onOpen = (_ctx, ws) => {
|
|
2144
|
+
onReconnectFn(finalCtx, ws, reconnectToken);
|
|
2145
|
+
};
|
|
2146
|
+
} else if (onOpenFn) {
|
|
2147
|
+
wrappedHandler.onOpen = (_ctx, ws) => {
|
|
2148
|
+
onOpenFn(finalCtx, ws);
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
if (onMsgFn) {
|
|
2153
|
+
wrappedHandler.onMessage = (_ctx, ws, data) => {
|
|
2154
|
+
messageQueue = messageQueue
|
|
2155
|
+
.then(() => onMsgFn(finalCtx, ws, data))
|
|
2156
|
+
.catch((err: unknown) => {
|
|
2157
|
+
onErrorFn?.(finalCtx, ws, err);
|
|
2158
|
+
});
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
if (onCloseFn) {
|
|
2163
|
+
wrappedHandler.onClose = (_ctx, ws, code, reason) => {
|
|
2164
|
+
onCloseFn(finalCtx, ws, code, reason);
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
if (onErrorFn) {
|
|
2169
|
+
wrappedHandler.onError = (_ctx, ws, error) => {
|
|
2170
|
+
onErrorFn(finalCtx, ws, error);
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
const upgradeResult = await wsAdapter.upgrade(
|
|
2175
|
+
fc.request,
|
|
2176
|
+
fc.env,
|
|
2177
|
+
wrappedHandler,
|
|
2178
|
+
);
|
|
2179
|
+
return upgradeResult.response;
|
|
2180
|
+
});
|
|
2181
|
+
} catch (thrown) {
|
|
2182
|
+
return this._toErrorResponse(thrown);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
private async _handleRealtime(
|
|
2187
|
+
fc: FetchCtx<TEnv>,
|
|
2188
|
+
wsMatch: { handler: WSRouteHandler; params: Record<string, string> },
|
|
2189
|
+
config: { handler: RealtimeRouteOpts["handler"]; middlewares?: RealtimeRouteOpts["use"]; reconnectBuffer?: number },
|
|
2190
|
+
): Promise<Response> {
|
|
2191
|
+
const isUpgrade =
|
|
2192
|
+
fc.request.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
2193
|
+
if (!isUpgrade) {
|
|
2194
|
+
return new Response(null, {
|
|
2195
|
+
headers: { upgrade: "websocket" },
|
|
2196
|
+
status: 426,
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
const wsAdapter = this._wsAdapter;
|
|
2201
|
+
if (!wsAdapter) {
|
|
2202
|
+
fc.log?.warn?.("WebSocket adapter not configured — call .wsAdapter()");
|
|
2203
|
+
return createErrorResponse(
|
|
2204
|
+
this._createError(EK.internal_server_error, SK.internal_server_error),
|
|
2205
|
+
this._errorFormatter,
|
|
2206
|
+
this._customErrorFormatter,
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
/* Lazily create bus if not yet initialized (happens when realtime() was called on a child chain) */
|
|
2211
|
+
if (!this._realtimeBus) {
|
|
2212
|
+
this._realtimeBus = createBus();
|
|
2213
|
+
}
|
|
2214
|
+
const bus = this._realtimeBus;
|
|
2215
|
+
|
|
2216
|
+
const ctx = new HoneyContext({
|
|
2217
|
+
env: fc.env,
|
|
2218
|
+
executionCtx: fc.executionCtx,
|
|
2219
|
+
params: wsMatch.params,
|
|
2220
|
+
req: fc.request,
|
|
2221
|
+
urlFn: fc.url,
|
|
2222
|
+
});
|
|
2223
|
+
if (this._contextValues) Object.assign(ctx, this._contextValues);
|
|
2224
|
+
Object.assign(ctx, {
|
|
2225
|
+
realtime: { publish: (topic: string, data: unknown) => bus.publish(topic, data) },
|
|
2226
|
+
});
|
|
2227
|
+
|
|
2228
|
+
const scopedForPath = this._filterScopedForPath(wsMatch.handler.rp);
|
|
2229
|
+
const allMw: RuntimeMiddleware[] = [
|
|
2230
|
+
...this._globalMiddlewares,
|
|
2231
|
+
...scopedForPath,
|
|
2232
|
+
...wsMatch.handler.mw,
|
|
2233
|
+
];
|
|
2234
|
+
|
|
2235
|
+
try {
|
|
2236
|
+
return await executeChain(allMw, ctx, async (finalCtx) => {
|
|
2237
|
+
const connId = crypto.randomUUID();
|
|
2238
|
+
|
|
2239
|
+
let socket: WSContext<unknown> | null = null;
|
|
2240
|
+
let conn: ReturnType<typeof createConnContext> | null = null;
|
|
2241
|
+
|
|
2242
|
+
/*
|
|
2243
|
+
* initConn creates the ConnContext and calls the user handler.
|
|
2244
|
+
* Called from onOpen (for Bun where socket arrives later)
|
|
2245
|
+
* or inline after upgrade (for Node/CF where socket is immediate).
|
|
2246
|
+
*/
|
|
2247
|
+
const initConn = (ws: WSContext<unknown>) => {
|
|
2248
|
+
socket = ws;
|
|
2249
|
+
conn = createConnContext({
|
|
2250
|
+
bus,
|
|
2251
|
+
closeFn: (reason) => {
|
|
2252
|
+
if (socket) socket.close(1000, reason);
|
|
2253
|
+
},
|
|
2254
|
+
id: connId,
|
|
2255
|
+
sendFn: (payload) => {
|
|
2256
|
+
if (socket) socket.send(typeof payload === "object" && payload !== null ? JSON.stringify(payload) : String(payload));
|
|
2257
|
+
},
|
|
2258
|
+
transport: "ws",
|
|
2259
|
+
userId: null,
|
|
2260
|
+
});
|
|
2261
|
+
|
|
2262
|
+
bus.onMessage(connId, (data) => {
|
|
2263
|
+
if (socket) {
|
|
2264
|
+
socket.send(typeof data === "object" && data !== null ? JSON.stringify(data) : String(data));
|
|
2265
|
+
}
|
|
2266
|
+
});
|
|
2267
|
+
|
|
2268
|
+
config.handler(finalCtx, conn);
|
|
2269
|
+
};
|
|
2270
|
+
|
|
2271
|
+
const wrappedHandler: WSHandler<unknown> = {
|
|
2272
|
+
onClose: (_ctx, _ws, _code, reason) => {
|
|
2273
|
+
if (!conn) return;
|
|
2274
|
+
const handlers = conn._handlers;
|
|
2275
|
+
if (handlers.close) {
|
|
2276
|
+
handlers.close(reason || "normal");
|
|
2277
|
+
}
|
|
2278
|
+
bus.unsubscribeAll(connId);
|
|
2279
|
+
bus.removeHandler(connId);
|
|
2280
|
+
},
|
|
2281
|
+
onMessage: (_ctx, _ws, data) => {
|
|
2282
|
+
if (!conn) return;
|
|
2283
|
+
const handlers = conn._handlers;
|
|
2284
|
+
if (handlers.message && typeof data === "string") {
|
|
2285
|
+
try {
|
|
2286
|
+
const parsed: unknown = JSON.parse(data);
|
|
2287
|
+
if (isMsgFrame(parsed)) {
|
|
2288
|
+
handlers.message(parsed.data);
|
|
2289
|
+
}
|
|
2290
|
+
} catch { /* ignore malformed frames */ }
|
|
2291
|
+
}
|
|
2292
|
+
},
|
|
2293
|
+
onOpen: (_ctx, ws) => {
|
|
2294
|
+
if (!socket) initConn(ws);
|
|
2295
|
+
},
|
|
2296
|
+
};
|
|
2297
|
+
|
|
2298
|
+
const upgradeResult = await wsAdapter.upgrade(fc.request, fc.env, wrappedHandler);
|
|
2299
|
+
|
|
2300
|
+
/* Node/CF adapters return the socket from upgrade(); Bun returns undefined (socket comes via onOpen).
|
|
2301
|
+
* Deno pre-upgrade may still be CONNECTING — wait for onOpen so the first send is not dropped. */
|
|
2302
|
+
if (upgradeResult.socket && !socket && upgradeResult.socket.readyState === 1) {
|
|
2303
|
+
initConn(upgradeResult.socket);
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
return upgradeResult.response;
|
|
2307
|
+
});
|
|
2308
|
+
} catch (thrown) {
|
|
2309
|
+
return this._toErrorResponse(thrown);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
private async _handle404(
|
|
2314
|
+
fc: FetchCtx<TEnv>,
|
|
2315
|
+
method: string,
|
|
2316
|
+
path: string,
|
|
2317
|
+
): Promise<Response> {
|
|
2318
|
+
safeFire(
|
|
2319
|
+
() => this._telemetry?.onNotFound?.({ method, path, req: fc.request }),
|
|
2320
|
+
fc.log,
|
|
2321
|
+
);
|
|
2322
|
+
const make404 = () => {
|
|
2323
|
+
if (this._onNotFound) {
|
|
2324
|
+
return this._onNotFound(this._makeErrorCtx(fc));
|
|
2325
|
+
}
|
|
2326
|
+
return this._makeErrorCtx(fc).jsonFromError(
|
|
2327
|
+
this._createError(EK.not_found, SK.not_found),
|
|
2328
|
+
);
|
|
2329
|
+
};
|
|
2330
|
+
try {
|
|
2331
|
+
const ctx404 = new HoneyContext({
|
|
2332
|
+
env: fc.env,
|
|
2333
|
+
executionCtx: fc.executionCtx,
|
|
2334
|
+
params: {},
|
|
2335
|
+
req: fc.request,
|
|
2336
|
+
urlFn: fc.url,
|
|
2337
|
+
});
|
|
2338
|
+
if (this._contextValues) Object.assign(ctx404, this._contextValues);
|
|
2339
|
+
const res =
|
|
2340
|
+
this._chainMiddlewares.length > 0
|
|
2341
|
+
? await executeChain(this._chainMiddlewares, ctx404, make404)
|
|
2342
|
+
: await make404();
|
|
2343
|
+
safeFire(
|
|
2344
|
+
() =>
|
|
2345
|
+
this._telemetry?.onResponse?.({
|
|
2346
|
+
duration: performance.now() - fc.startTime,
|
|
2347
|
+
req: fc.request,
|
|
2348
|
+
status: res.status,
|
|
2349
|
+
}),
|
|
2350
|
+
fc.log,
|
|
2351
|
+
);
|
|
2352
|
+
return res;
|
|
2353
|
+
} catch (thrown) {
|
|
2354
|
+
return this._toErrorResponse(thrown);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
private _preflightFallbackMethod(allowed: string[]): HttpMethod {
|
|
2359
|
+
if (allowed.includes("GET")) return "GET";
|
|
2360
|
+
if (allowed.includes("HEAD")) return "HEAD";
|
|
2361
|
+
if (allowed.includes("POST")) return "POST";
|
|
2362
|
+
return allowed[0] as HttpMethod;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
/** Run the existing method's middleware for a CORS preflight. Do not invoke the route handler. */
|
|
2366
|
+
private async _handleCorsPreflight(
|
|
2367
|
+
fc: FetchCtx<TEnv>,
|
|
2368
|
+
path: string,
|
|
2369
|
+
handler: RouteHandler,
|
|
2370
|
+
params: Record<string, string>,
|
|
2371
|
+
allowed: string[],
|
|
2372
|
+
): Promise<Response> {
|
|
2373
|
+
const ctx = new HoneyContext({
|
|
2374
|
+
env: fc.env,
|
|
2375
|
+
executionCtx: fc.executionCtx,
|
|
2376
|
+
meta: handler.mt ? Object.freeze(handler.mt) : undefined,
|
|
2377
|
+
params,
|
|
2378
|
+
path,
|
|
2379
|
+
req: fc.request,
|
|
2380
|
+
routePattern: handler.rp,
|
|
2381
|
+
urlFn: fc.url,
|
|
2382
|
+
});
|
|
2383
|
+
if (this._contextValues) Object.assign(ctx, this._contextValues);
|
|
2384
|
+
try {
|
|
2385
|
+
return await executeChain(
|
|
2386
|
+
[...this._globalMiddlewares, ...handler.mw],
|
|
2387
|
+
ctx,
|
|
2388
|
+
() => this._handle405(fc, "OPTIONS", path, allowed),
|
|
2389
|
+
);
|
|
2390
|
+
} catch (thrown) {
|
|
2391
|
+
return this._toErrorResponse(thrown);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
private async _handle405(
|
|
2396
|
+
fc: FetchCtx<TEnv>,
|
|
2397
|
+
method: string,
|
|
2398
|
+
path: string,
|
|
2399
|
+
allowed: string[],
|
|
2400
|
+
): Promise<Response> {
|
|
2401
|
+
safeFire(
|
|
2402
|
+
() =>
|
|
2403
|
+
this._telemetry?.onMethodNotAllowed?.({
|
|
2404
|
+
allowed,
|
|
2405
|
+
method,
|
|
2406
|
+
path,
|
|
2407
|
+
req: fc.request,
|
|
2408
|
+
}),
|
|
2409
|
+
fc.log,
|
|
2410
|
+
);
|
|
2411
|
+
const make405 = async () => {
|
|
2412
|
+
if (this._onMethodNotAllowed) {
|
|
2413
|
+
const res = await this._onMethodNotAllowed(
|
|
2414
|
+
this._makeErrorCtx(fc, allowed) as {
|
|
2415
|
+
allowed: string[];
|
|
2416
|
+
env: TEnv;
|
|
2417
|
+
jsonFromError: (err: HoneyError) => Response;
|
|
2418
|
+
req: Request;
|
|
2419
|
+
},
|
|
2420
|
+
);
|
|
2421
|
+
const responseHeaders = new Headers(res.headers);
|
|
2422
|
+
responseHeaders.set("allow", allowed.join(", "));
|
|
2423
|
+
return new Response(res.body, {
|
|
2424
|
+
headers: responseHeaders,
|
|
2425
|
+
status: res.status,
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
const err = this._createError(
|
|
2429
|
+
EK.method_not_allowed,
|
|
2430
|
+
SK.method_not_allowed,
|
|
2431
|
+
);
|
|
2432
|
+
const res = this._makeErrorCtx(fc).jsonFromError(err);
|
|
2433
|
+
const responseHeaders = new Headers(res.headers);
|
|
2434
|
+
responseHeaders.set("allow", allowed.join(", "));
|
|
2435
|
+
return new Response(res.body, {
|
|
2436
|
+
headers: responseHeaders,
|
|
2437
|
+
status: res.status,
|
|
2438
|
+
});
|
|
2439
|
+
};
|
|
2440
|
+
try {
|
|
2441
|
+
const ctx405 = new HoneyContext({
|
|
2442
|
+
env: fc.env,
|
|
2443
|
+
executionCtx: fc.executionCtx,
|
|
2444
|
+
params: {},
|
|
2445
|
+
req: fc.request,
|
|
2446
|
+
urlFn: fc.url,
|
|
2447
|
+
});
|
|
2448
|
+
if (this._contextValues) Object.assign(ctx405, this._contextValues);
|
|
2449
|
+
const finalRes =
|
|
2450
|
+
this._chainMiddlewares.length > 0
|
|
2451
|
+
? await executeChain(this._chainMiddlewares, ctx405, make405)
|
|
2452
|
+
: await make405();
|
|
2453
|
+
safeFire(
|
|
2454
|
+
() =>
|
|
2455
|
+
this._telemetry?.onResponse?.({
|
|
2456
|
+
duration: performance.now() - fc.startTime,
|
|
2457
|
+
req: fc.request,
|
|
2458
|
+
status: finalRes.status,
|
|
2459
|
+
}),
|
|
2460
|
+
fc.log,
|
|
2461
|
+
);
|
|
2462
|
+
return finalRes;
|
|
2463
|
+
} catch (thrown) {
|
|
2464
|
+
return this._toErrorResponse(thrown);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
private _handleMatched(
|
|
2469
|
+
fc: FetchCtx<TEnv>,
|
|
2470
|
+
method: HttpMethod,
|
|
2471
|
+
path: string,
|
|
2472
|
+
handler: RouteHandler,
|
|
2473
|
+
params: Record<string, string>,
|
|
2474
|
+
stashedMeta?: Record<string, unknown> | null,
|
|
2475
|
+
): Response | Promise<Response> {
|
|
2476
|
+
const { env, executionCtx, log, request } = fc;
|
|
2477
|
+
|
|
2478
|
+
/* resolve error factory — pre-computed ef preferred, else build/use global */
|
|
2479
|
+
let errors: Record<string, (...args: never[]) => unknown> | undefined;
|
|
2480
|
+
if (handler.ef !== null) {
|
|
2481
|
+
errors = handler.ef;
|
|
2482
|
+
} else if (this._errorFactory !== null) {
|
|
2483
|
+
if (handler.ek.size > 0) {
|
|
2484
|
+
const factory = this._errorFactory as Record<
|
|
2485
|
+
string,
|
|
2486
|
+
(...args: never[]) => unknown
|
|
2487
|
+
>;
|
|
2488
|
+
const subset = Object.create(null) as Record<string, unknown>;
|
|
2489
|
+
for (const key of handler.ek) {
|
|
2490
|
+
if (key in factory) {
|
|
2491
|
+
subset[key] = factory[key];
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
errors = Object.freeze(subset) as Record<
|
|
2495
|
+
string,
|
|
2496
|
+
(...args: never[]) => unknown
|
|
2497
|
+
>;
|
|
2498
|
+
} else {
|
|
2499
|
+
errors = this._errorFactory as Record<
|
|
2500
|
+
string,
|
|
2501
|
+
(...args: never[]) => unknown
|
|
2502
|
+
>;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
/* meta: stashed meta from fn:null tree match takes priority over handler meta */
|
|
2507
|
+
const resolvedMeta = stashedMeta ?? handler.mt;
|
|
2508
|
+
|
|
2509
|
+
const ctx = new HoneyContext({
|
|
2510
|
+
env,
|
|
2511
|
+
executionCtx,
|
|
2512
|
+
meta: resolvedMeta ?? undefined,
|
|
2513
|
+
params,
|
|
2514
|
+
path,
|
|
2515
|
+
req: request,
|
|
2516
|
+
routePattern: handler.rp,
|
|
2517
|
+
urlFn: fc.url,
|
|
2518
|
+
});
|
|
2519
|
+
if (this._contextValues) Object.assign(ctx, this._contextValues);
|
|
2520
|
+
if (this._realtimeBus) {
|
|
2521
|
+
const rtBus = this._realtimeBus;
|
|
2522
|
+
Object.assign(ctx, {
|
|
2523
|
+
realtime: { publish: (topic: string, data: unknown) => rtBus.publish(topic, data) },
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
if (errors) {
|
|
2527
|
+
ctx._setErrors(errors);
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
if (this._telemetry !== null) {
|
|
2531
|
+
try {
|
|
2532
|
+
this._telemetry.onRoute?.({ method, params, path, req: request });
|
|
2533
|
+
} catch {
|
|
2534
|
+
/* telemetry must not crash request */
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
/*
|
|
2539
|
+
* Tier 3: Use pre-compiled chain when possible.
|
|
2540
|
+
* Compiled chains are cached on the handler — created once, reused per request.
|
|
2541
|
+
* Falls back to dynamic assembly when telemetry wrapping, input validation, or
|
|
2542
|
+
* scoped middleware is in play (scoped mw cannot be baked into the compiled cache
|
|
2543
|
+
* because each route may match a different subset).
|
|
2544
|
+
*/
|
|
2545
|
+
const hasTelemetryMw = this._telemetry?.onMiddleware !== undefined;
|
|
2546
|
+
const hasInputValidation = handler.iv !== null;
|
|
2547
|
+
|
|
2548
|
+
/*
|
|
2549
|
+
* Error resolver — stored on ctx so the cached handler wrapper can read it.
|
|
2550
|
+
* Converts handler errors into error Responses inside the middleware chain,
|
|
2551
|
+
* allowing all post-next() middleware code (headers, logging, timing) to run.
|
|
2552
|
+
*/
|
|
2553
|
+
ctx._errorToResponse = (thrown: unknown) =>
|
|
2554
|
+
this._resolveErrorResponse(thrown, handler, fc, method, path, ctx);
|
|
2555
|
+
|
|
2556
|
+
const onError = (thrown: unknown): Response | Promise<Response> => {
|
|
2557
|
+
if (ctx._errorToResponse) return ctx._errorToResponse(thrown);
|
|
2558
|
+
return this._toErrorResponse(thrown);
|
|
2559
|
+
};
|
|
2560
|
+
|
|
2561
|
+
const after = (response: Response): Response | Promise<Response> => {
|
|
2562
|
+
try {
|
|
2563
|
+
const done = this._afterMatched(fc, method, path, handler, ctx, response);
|
|
2564
|
+
if (done instanceof Promise) return done.catch(onError);
|
|
2565
|
+
return done;
|
|
2566
|
+
} catch (thrown) {
|
|
2567
|
+
return onError(thrown);
|
|
2568
|
+
}
|
|
2569
|
+
};
|
|
2570
|
+
|
|
2571
|
+
try {
|
|
2572
|
+
if (!hasTelemetryMw && !hasInputValidation && this._scopedMiddlewares.length === 0) {
|
|
2573
|
+
if (!handler._compiled) {
|
|
2574
|
+
const chainMw = this._chainMiddlewares;
|
|
2575
|
+
const handlerHasChain =
|
|
2576
|
+
chainMw.length > 0 && chainMw.every((mw, i) => handler.mw[i] === mw);
|
|
2577
|
+
const allMw = handlerHasChain
|
|
2578
|
+
? [...this._globalMiddlewares, ...handler.mw]
|
|
2579
|
+
: [...this._globalMiddlewares, ...chainMw, ...handler.mw];
|
|
2580
|
+
handler._compiled = compileChain(allMw, (c) => {
|
|
2581
|
+
try {
|
|
2582
|
+
const result = handler.fn(c);
|
|
2583
|
+
if (result instanceof Promise) {
|
|
2584
|
+
return result.catch((thrown: unknown) => {
|
|
2585
|
+
const hCtx = c as HoneyContext<TEnv>;
|
|
2586
|
+
if (hCtx._errorToResponse)
|
|
2587
|
+
return hCtx._errorToResponse(thrown);
|
|
2588
|
+
throw thrown;
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
return result;
|
|
2592
|
+
} catch (thrown) {
|
|
2593
|
+
const hCtx = c as HoneyContext<TEnv>;
|
|
2594
|
+
if (hCtx._errorToResponse) return hCtx._errorToResponse(thrown);
|
|
2595
|
+
throw thrown;
|
|
2596
|
+
}
|
|
2597
|
+
});
|
|
2598
|
+
}
|
|
2599
|
+
const result = handler._compiled(ctx);
|
|
2600
|
+
if (result instanceof Promise) return result.then(after, onError);
|
|
2601
|
+
return after(result);
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
const scopedForPath = this._filterScopedForPath(handler.rp);
|
|
2605
|
+
const chainMw = this._chainMiddlewares;
|
|
2606
|
+
const handlerHasChain =
|
|
2607
|
+
chainMw.length > 0 && chainMw.every((mw, i) => handler.mw[i] === mw);
|
|
2608
|
+
/*
|
|
2609
|
+
* Ordering: [global → chain → scoped → handler-route-specific]
|
|
2610
|
+
* When handlerHasChain, handler.mw = [chain..., routeSpecific...].
|
|
2611
|
+
* Scoped must go after chain but before route-specific, so we split.
|
|
2612
|
+
*/
|
|
2613
|
+
let allMiddlewares: RuntimeMiddleware[] = handlerHasChain
|
|
2614
|
+
? [
|
|
2615
|
+
...this._globalMiddlewares,
|
|
2616
|
+
...handler.mw.slice(0, chainMw.length),
|
|
2617
|
+
...scopedForPath,
|
|
2618
|
+
...handler.mw.slice(chainMw.length),
|
|
2619
|
+
]
|
|
2620
|
+
: [...this._globalMiddlewares, ...chainMw, ...scopedForPath, ...handler.mw];
|
|
2621
|
+
|
|
2622
|
+
if (hasTelemetryMw) {
|
|
2623
|
+
const onMw = this._telemetry?.onMiddleware;
|
|
2624
|
+
if (onMw) {
|
|
2625
|
+
allMiddlewares = allMiddlewares.map((mw) => {
|
|
2626
|
+
const name = mw.name || "anonymous";
|
|
2627
|
+
const wrapped: RuntimeMiddleware = async (wCtx, wNext) => {
|
|
2628
|
+
const mwStart = performance.now();
|
|
2629
|
+
try {
|
|
2630
|
+
const res = await mw(wCtx, wNext);
|
|
2631
|
+
safeFire(
|
|
2632
|
+
() => onMw({ duration: performance.now() - mwStart, name }),
|
|
2633
|
+
log,
|
|
2634
|
+
);
|
|
2635
|
+
return res;
|
|
2636
|
+
} catch (error) {
|
|
2637
|
+
safeFire(
|
|
2638
|
+
() =>
|
|
2639
|
+
onMw({
|
|
2640
|
+
duration: performance.now() - mwStart,
|
|
2641
|
+
error,
|
|
2642
|
+
name,
|
|
2643
|
+
}),
|
|
2644
|
+
log,
|
|
2645
|
+
);
|
|
2646
|
+
throw error;
|
|
2647
|
+
}
|
|
2648
|
+
};
|
|
2649
|
+
return wrapped;
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
if (hasInputValidation) {
|
|
2655
|
+
const schemas = handler.iv;
|
|
2656
|
+
if (schemas) {
|
|
2657
|
+
const inputMw: RuntimeMiddleware = async (inputCtx, inputNext) => {
|
|
2658
|
+
const validated = await validateInput(
|
|
2659
|
+
schemas,
|
|
2660
|
+
inputCtx["req"] as Request,
|
|
2661
|
+
params,
|
|
2662
|
+
);
|
|
2663
|
+
return inputNext({ input: validated });
|
|
2664
|
+
};
|
|
2665
|
+
allMiddlewares.push(inputMw);
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
return executeChain(allMiddlewares, ctx, (finalCtx) => {
|
|
2670
|
+
try {
|
|
2671
|
+
const result = handler.fn(finalCtx);
|
|
2672
|
+
if (result instanceof Promise) {
|
|
2673
|
+
return result.catch((thrown: unknown) => {
|
|
2674
|
+
const hCtx = finalCtx as HoneyContext<TEnv>;
|
|
2675
|
+
if (hCtx._errorToResponse) return hCtx._errorToResponse(thrown);
|
|
2676
|
+
throw thrown;
|
|
2677
|
+
});
|
|
2678
|
+
}
|
|
2679
|
+
return result;
|
|
2680
|
+
} catch (thrown) {
|
|
2681
|
+
const hCtx = finalCtx as HoneyContext<TEnv>;
|
|
2682
|
+
if (hCtx._errorToResponse) return hCtx._errorToResponse(thrown);
|
|
2683
|
+
throw thrown;
|
|
2684
|
+
}
|
|
2685
|
+
}).then(after, onError);
|
|
2686
|
+
} catch (thrown) {
|
|
2687
|
+
/* safety net — middleware-level errors (input validation, middleware crash) */
|
|
2688
|
+
return onError(thrown);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
private _afterMatched(
|
|
2693
|
+
fc: FetchCtx<TEnv>,
|
|
2694
|
+
method: HttpMethod,
|
|
2695
|
+
path: string,
|
|
2696
|
+
handler: RouteHandler,
|
|
2697
|
+
ctx: HoneyContext<TEnv>,
|
|
2698
|
+
response: Response,
|
|
2699
|
+
): Response | Promise<Response> {
|
|
2700
|
+
const validateOut =
|
|
2701
|
+
this._outputValidation === "always" ||
|
|
2702
|
+
(this._outputValidation === "dev" &&
|
|
2703
|
+
(globalThis as { process?: { env?: { NODE_ENV?: string } } }).process?.env
|
|
2704
|
+
?.NODE_ENV !== "production")
|
|
2705
|
+
if (
|
|
2706
|
+
handler.os &&
|
|
2707
|
+
validateOut &&
|
|
2708
|
+
response.body !== null &&
|
|
2709
|
+
!ctx._isErrorResponse
|
|
2710
|
+
) {
|
|
2711
|
+
return this._validateThenFinish(fc, method, path, handler, ctx, response);
|
|
2712
|
+
}
|
|
2713
|
+
return this._finishMatched(fc, method, path, handler, ctx, response);
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
private async _validateThenFinish(
|
|
2717
|
+
fc: FetchCtx<TEnv>,
|
|
2718
|
+
method: HttpMethod,
|
|
2719
|
+
path: string,
|
|
2720
|
+
handler: RouteHandler,
|
|
2721
|
+
ctx: HoneyContext<TEnv>,
|
|
2722
|
+
response: Response,
|
|
2723
|
+
): Promise<Response> {
|
|
2724
|
+
const ct = response.headers.get("content-type");
|
|
2725
|
+
|
|
2726
|
+
/* content-type mismatch check */
|
|
2727
|
+
if (ct && handler.os) {
|
|
2728
|
+
const declaredTypes = Object.keys(handler.os);
|
|
2729
|
+
const matches = declaredTypes.some((t) => ct.startsWith(t));
|
|
2730
|
+
if (!matches) {
|
|
2731
|
+
throw new HoneyError({
|
|
2732
|
+
errorKey: EK.output_content_type_mismatch,
|
|
2733
|
+
status: SK.internal_server_error,
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
/* JSON schema validation — read original, return clone (Bun clone() drains original) */
|
|
2739
|
+
if (ct?.startsWith("application/json") && handler.ov) {
|
|
2740
|
+
const sk = codeToStatusKey[response.status];
|
|
2741
|
+
if (sk) {
|
|
2742
|
+
const forReturn = response.clone();
|
|
2743
|
+
const data: unknown = await response.json();
|
|
2744
|
+
await handler.ov(sk, data);
|
|
2745
|
+
response = forReturn;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
return this._finishMatched(fc, method, path, handler, ctx, response);
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
private _finishMatched(
|
|
2752
|
+
fc: FetchCtx<TEnv>,
|
|
2753
|
+
method: HttpMethod,
|
|
2754
|
+
path: string,
|
|
2755
|
+
handler: RouteHandler,
|
|
2756
|
+
ctx: HoneyContext<TEnv>,
|
|
2757
|
+
response: Response,
|
|
2758
|
+
): Response {
|
|
2759
|
+
/* taps — fire after successful handler, non-blocking */
|
|
2760
|
+
if (this._taps !== null && !ctx._isErrorResponse) {
|
|
2761
|
+
const taps = this._taps;
|
|
2762
|
+
const log = fc.log;
|
|
2763
|
+
|
|
2764
|
+
/* meta-driven taps — fire for each registered key found in route meta */
|
|
2765
|
+
if (handler.mt !== null) {
|
|
2766
|
+
for (const [key, tapFn] of taps) {
|
|
2767
|
+
const metaValue = handler.mt[key];
|
|
2768
|
+
if (metaValue !== undefined) {
|
|
2769
|
+
ctx.background(
|
|
2770
|
+
Promise.resolve()
|
|
2771
|
+
.then(() => tapFn(ctx, metaValue))
|
|
2772
|
+
.catch((e) => log?.warn?.("tap failed", key, e)),
|
|
2773
|
+
);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
/* dynamic taps — fire for each c.tap() call */
|
|
2779
|
+
if (ctx._pendingTaps !== null) {
|
|
2780
|
+
for (const pending of ctx._pendingTaps) {
|
|
2781
|
+
const tapFn = taps.get(pending.key);
|
|
2782
|
+
if (tapFn !== undefined) {
|
|
2783
|
+
ctx.background(
|
|
2784
|
+
Promise.resolve()
|
|
2785
|
+
.then(() => tapFn(ctx, pending.payload))
|
|
2786
|
+
.catch((e) => log?.warn?.("tap failed", pending.key, e)),
|
|
2787
|
+
);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
ctx._pendingTaps = null;
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
if (this._telemetry !== null) {
|
|
2795
|
+
try {
|
|
2796
|
+
const duration = performance.now() - fc.startTime;
|
|
2797
|
+
this._telemetry.onHandler?.({
|
|
2798
|
+
duration,
|
|
2799
|
+
method,
|
|
2800
|
+
path,
|
|
2801
|
+
status: response.status,
|
|
2802
|
+
});
|
|
2803
|
+
this._telemetry.onResponse?.({
|
|
2804
|
+
duration,
|
|
2805
|
+
req: fc.request,
|
|
2806
|
+
status: response.status,
|
|
2807
|
+
});
|
|
2808
|
+
} catch {
|
|
2809
|
+
/* telemetry must never crash the response path */
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
/* HEAD responses must have empty body — preserve headers + status */
|
|
2813
|
+
if (method === "HEAD") {
|
|
2814
|
+
return new Response(null, {
|
|
2815
|
+
headers: response.headers,
|
|
2816
|
+
status: response.status,
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
return response;
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
/*
|
|
2824
|
+
* Handler Type Algebra
|
|
2825
|
+
* These types compose the handler context from route configuration.
|
|
2826
|
+
* NarrowMethod → ApplyOutput → ApplyParams → HandlerCtx
|
|
2827
|
+
*/
|
|
2828
|
+
|
|
2829
|
+
/**
|
|
2830
|
+
* When content type IS declared → narrow the method (constrain status keys + body).
|
|
2831
|
+
* When content type NOT declared → remove the method (Omit).
|
|
2832
|
+
* Narrowed methods return TypedResponse<CT, K> for compile-time CT+SK safety.
|
|
2833
|
+
*/
|
|
2834
|
+
/** Resolve body type: schema → InferOutput, plain type → use as-is */
|
|
2835
|
+
type ResolveBody<T, TBody> = TBody extends "infer"
|
|
2836
|
+
? T extends StandardSchemaLike
|
|
2837
|
+
? InferOutput<T>
|
|
2838
|
+
: T
|
|
2839
|
+
: TBody;
|
|
2840
|
+
|
|
2841
|
+
type NarrowMethod<
|
|
2842
|
+
TRes,
|
|
2843
|
+
TSchemas,
|
|
2844
|
+
Method extends string,
|
|
2845
|
+
CT extends string,
|
|
2846
|
+
TBody,
|
|
2847
|
+
> = [TSchemas] extends [never]
|
|
2848
|
+
? Omit<TRes, Method>
|
|
2849
|
+
: TSchemas extends Record<string, unknown>
|
|
2850
|
+
? Omit<TRes, Method> & {
|
|
2851
|
+
[M in Method]: <K extends keyof TSchemas & string>(
|
|
2852
|
+
statusKey: K,
|
|
2853
|
+
body: ResolveBody<TSchemas[K], TBody>,
|
|
2854
|
+
opts?: ResponseOptions,
|
|
2855
|
+
) => TypedResponse<CT, K>;
|
|
2856
|
+
}
|
|
2857
|
+
: Omit<TRes, Method>;
|
|
2858
|
+
|
|
2859
|
+
/**
|
|
2860
|
+
* SSE uses a callback signature, not statusKey+body like other methods.
|
|
2861
|
+
* Gate presence on text/event-stream declaration, preserve original signature.
|
|
2862
|
+
*/
|
|
2863
|
+
type NarrowSSE<TOutput> = [
|
|
2864
|
+
ExtractSchemas<TOutput, "text/event-stream">,
|
|
2865
|
+
] extends [never]
|
|
2866
|
+
? {}
|
|
2867
|
+
: { sse: HoneyRes["sse"] };
|
|
2868
|
+
|
|
2869
|
+
/** Universal methods — always available, not gated by output declaration */
|
|
2870
|
+
type UniversalRes = Pick<HoneyRes, "noContent" | "raw" | "redirect" | "stream">;
|
|
2871
|
+
|
|
2872
|
+
/**
|
|
2873
|
+
* Constrain ctx.res when output schemas are declared.
|
|
2874
|
+
* Declared content types → method constrained (status keys + body type).
|
|
2875
|
+
* Undeclared content types → method removed.
|
|
2876
|
+
* Universal methods (noContent, redirect, stream, raw) always available.
|
|
2877
|
+
* HoneyContext has no private brand, so Omit<TCtx, "res"> replaces the wide HoneyRes.
|
|
2878
|
+
*/
|
|
2879
|
+
type ApplyOutput<TCtx, TOutput> = [keyof TOutput] extends [never]
|
|
2880
|
+
? TCtx
|
|
2881
|
+
: Omit<TCtx, "res"> & {
|
|
2882
|
+
readonly res: UniversalRes &
|
|
2883
|
+
NarrowMethod<
|
|
2884
|
+
{},
|
|
2885
|
+
ExtractSchemas<TOutput, "application/json">,
|
|
2886
|
+
"json",
|
|
2887
|
+
"application/json",
|
|
2888
|
+
"infer"
|
|
2889
|
+
> &
|
|
2890
|
+
NarrowMethod<
|
|
2891
|
+
{},
|
|
2892
|
+
ExtractSchemas<TOutput, "text/plain">,
|
|
2893
|
+
"text",
|
|
2894
|
+
"text/plain",
|
|
2895
|
+
string
|
|
2896
|
+
> &
|
|
2897
|
+
NarrowMethod<
|
|
2898
|
+
{},
|
|
2899
|
+
ExtractSchemas<TOutput, "text/html">,
|
|
2900
|
+
"html",
|
|
2901
|
+
"text/html",
|
|
2902
|
+
string
|
|
2903
|
+
> &
|
|
2904
|
+
NarrowMethod<
|
|
2905
|
+
{},
|
|
2906
|
+
ExtractSchemas<TOutput, "application/xml">,
|
|
2907
|
+
"xml",
|
|
2908
|
+
"application/xml",
|
|
2909
|
+
string
|
|
2910
|
+
> &
|
|
2911
|
+
NarrowMethod<
|
|
2912
|
+
{},
|
|
2913
|
+
ExtractSchemas<TOutput, "text/csv">,
|
|
2914
|
+
"csv",
|
|
2915
|
+
"text/csv",
|
|
2916
|
+
string
|
|
2917
|
+
> &
|
|
2918
|
+
NarrowMethod<
|
|
2919
|
+
{},
|
|
2920
|
+
ExtractSchemas<TOutput, "application/octet-stream">,
|
|
2921
|
+
"binary",
|
|
2922
|
+
"application/octet-stream",
|
|
2923
|
+
ArrayBuffer | Uint8Array<ArrayBuffer>
|
|
2924
|
+
> &
|
|
2925
|
+
NarrowSSE<TOutput>;
|
|
2926
|
+
};
|
|
2927
|
+
|
|
2928
|
+
/** Public alias for codegen — applies output schema constraints to a context type */
|
|
2929
|
+
export type WithOutput<TCtx, TOutput> = ApplyOutput<TCtx, TOutput>;
|
|
2930
|
+
|
|
2931
|
+
/** @internal — tuple entry describing a scoped middleware at the type level */
|
|
2932
|
+
type ScopedMwEntry = { readonly path: string; readonly adds: unknown }
|
|
2933
|
+
|
|
2934
|
+
/**
|
|
2935
|
+
* Walk TScopedMw tuple, intersect `adds` for every entry whose `path` is a prefix of TFullPath.
|
|
2936
|
+
* Prefix semantics: TFullPath extends `${P}` (exact) | `${P}/${string}` (descendant).
|
|
2937
|
+
*
|
|
2938
|
+
* Non-literal-path guard: if Head["path"] is the base `string` type (happens when
|
|
2939
|
+
* the user passes a widened variable), skip the entry via `string extends Head["path"] ? {}`.
|
|
2940
|
+
* This prevents the always-true `'/anywhere' extends string` from polluting every route.
|
|
2941
|
+
*/
|
|
2942
|
+
type ApplyScoped<
|
|
2943
|
+
TScopedMw extends readonly ScopedMwEntry[],
|
|
2944
|
+
TFullPath extends string,
|
|
2945
|
+
> = TScopedMw extends readonly [
|
|
2946
|
+
infer Head extends ScopedMwEntry,
|
|
2947
|
+
...infer Rest extends readonly ScopedMwEntry[],
|
|
2948
|
+
]
|
|
2949
|
+
? (string extends Head["path"]
|
|
2950
|
+
? {}
|
|
2951
|
+
: TFullPath extends Head["path"]
|
|
2952
|
+
? Head["adds"]
|
|
2953
|
+
: TFullPath extends `${Head["path"] & string}/${string}`
|
|
2954
|
+
? Head["adds"]
|
|
2955
|
+
: {}) &
|
|
2956
|
+
ApplyScoped<Rest, TFullPath>
|
|
2957
|
+
: {}
|
|
2958
|
+
|
|
2959
|
+
/** @internal — runtime entry for a scoped middleware */
|
|
2960
|
+
type ScopedEntry = {
|
|
2961
|
+
/** merged full-path prefix (already rebased against basePath at .use time) */
|
|
2962
|
+
prefix: string
|
|
2963
|
+
mw: RuntimeMiddleware
|
|
2964
|
+
/** cached from mw.errors at registration; undefined when none */
|
|
2965
|
+
errors: readonly string[] | undefined
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
/** Apply typed params — override params with specific keys when route has :param segments */
|
|
2969
|
+
type ApplyParams<TCtx, TParams> = [keyof TParams] extends [string]
|
|
2970
|
+
? string extends keyof TParams
|
|
2971
|
+
? TCtx
|
|
2972
|
+
: TCtx & { readonly params: TParams }
|
|
2973
|
+
: TCtx;
|
|
2974
|
+
|
|
2975
|
+
/** Typed tap method — constrained to registered tap keys when TTaps is non-empty */
|
|
2976
|
+
type TypedTap<TTaps extends Record<string, unknown>> =
|
|
2977
|
+
[keyof TTaps] extends [never]
|
|
2978
|
+
? { tap(key: string, payload: unknown): void }
|
|
2979
|
+
: { tap<K extends string & keyof TTaps>(key: K, payload: TTaps[K]): void };
|
|
2980
|
+
|
|
2981
|
+
/** Build the full handler context: base ctx + params + input + meta + errors + taps + output-constrained methods */
|
|
2982
|
+
type HandlerCtx<
|
|
2983
|
+
TCtx,
|
|
2984
|
+
TInput,
|
|
2985
|
+
TOutput,
|
|
2986
|
+
TParams,
|
|
2987
|
+
TAccMeta = {},
|
|
2988
|
+
TErrorFactory = never,
|
|
2989
|
+
TErrorKeys extends string = never,
|
|
2990
|
+
TPath extends string = string,
|
|
2991
|
+
TTaps extends Record<string, unknown> = {},
|
|
2992
|
+
> = ApplyOutput<
|
|
2993
|
+
ApplyParams<
|
|
2994
|
+
[keyof TInput] extends [never] ? TCtx : TCtx & { input: TInput },
|
|
2995
|
+
TParams
|
|
2996
|
+
> & {
|
|
2997
|
+
readonly meta: Readonly<Omit<TAccMeta, "openApi">>;
|
|
2998
|
+
readonly routePattern: TPath;
|
|
2999
|
+
} & ([TErrorFactory] extends [never]
|
|
3000
|
+
? {}
|
|
3001
|
+
: [TErrorKeys] extends [never]
|
|
3002
|
+
? { readonly errors: TErrorFactory }
|
|
3003
|
+
: {
|
|
3004
|
+
readonly errors: Pick<
|
|
3005
|
+
TErrorFactory,
|
|
3006
|
+
TErrorKeys & keyof TErrorFactory
|
|
3007
|
+
>;
|
|
3008
|
+
})
|
|
3009
|
+
& TypedTap<TTaps>,
|
|
3010
|
+
TOutput
|
|
3011
|
+
>;
|
|
3012
|
+
|
|
3013
|
+
/** @internal — exposes private Honey members for RouteBuilder/WSRouteBuilder access */
|
|
3014
|
+
type HoneyInternal = {
|
|
3015
|
+
_factory: unknown;
|
|
3016
|
+
_markWsRoutes(): void;
|
|
3017
|
+
_registerStatic(key: string, handler: RouteHandler): void;
|
|
3018
|
+
};
|
|
3019
|
+
|
|
3020
|
+
type RouteBuilderState<TParent> = {
|
|
3021
|
+
boundaryErrorKey: string | null;
|
|
3022
|
+
errorKeys: Set<string>;
|
|
3023
|
+
extraMethods: (HttpMethod | "ALL")[] | null;
|
|
3024
|
+
handlerMap: Record<string, RouteHandler> | null;
|
|
3025
|
+
inputSchemas: InputSchemasDef | null;
|
|
3026
|
+
meta: Record<string, unknown> | null;
|
|
3027
|
+
method: HttpMethod | "ALL";
|
|
3028
|
+
middlewares: RuntimeMiddleware[];
|
|
3029
|
+
outputSchemas: OutputSchemaDef | null;
|
|
3030
|
+
parent: TParent;
|
|
3031
|
+
parentMiddlewares: RuntimeMiddleware[];
|
|
3032
|
+
path: string;
|
|
3033
|
+
root: TreeNode;
|
|
3034
|
+
};
|
|
3035
|
+
|
|
3036
|
+
/** Honey.TMeta defaults to never; never & X is never, so start acc meta at {}. */
|
|
3037
|
+
type InitAccMeta<TMeta> = [TMeta] extends [never] ? {} : TMeta
|
|
3038
|
+
|
|
3039
|
+
/** Skip $routes merge only for a real `{ internal: true }` meta — not for `never`. */
|
|
3040
|
+
type SkipRouteRecord<TAccMeta> = [TAccMeta] extends [never]
|
|
3041
|
+
? false
|
|
3042
|
+
: [TAccMeta] extends [{ internal: true }]
|
|
3043
|
+
? true
|
|
3044
|
+
: false
|
|
3045
|
+
|
|
3046
|
+
/** Return type for handler() — extracted to avoid 3x duplication */
|
|
3047
|
+
type HandlerReturn<
|
|
3048
|
+
TEnv,
|
|
3049
|
+
TBaseCtx,
|
|
3050
|
+
TRoutes,
|
|
3051
|
+
TPath extends string,
|
|
3052
|
+
TMethod extends string,
|
|
3053
|
+
TInput,
|
|
3054
|
+
TOutput,
|
|
3055
|
+
TCtx,
|
|
3056
|
+
TAccMeta,
|
|
3057
|
+
TErrorFactory,
|
|
3058
|
+
_TErrorKeys extends string,
|
|
3059
|
+
TDefaultErrors extends string,
|
|
3060
|
+
TMeta,
|
|
3061
|
+
TBasePath extends string = "/",
|
|
3062
|
+
TTaps extends Record<string, unknown> = {},
|
|
3063
|
+
TScopedMw extends readonly ScopedMwEntry[] = [],
|
|
3064
|
+
> = Honey<
|
|
3065
|
+
TEnv,
|
|
3066
|
+
TBaseCtx,
|
|
3067
|
+
SkipRouteRecord<TAccMeta> extends true
|
|
3068
|
+
? TRoutes
|
|
3069
|
+
: MergeRoute<
|
|
3070
|
+
TRoutes,
|
|
3071
|
+
TPath,
|
|
3072
|
+
TMethod,
|
|
3073
|
+
TInput,
|
|
3074
|
+
TOutput,
|
|
3075
|
+
HandlerCtx<
|
|
3076
|
+
TCtx,
|
|
3077
|
+
TInput,
|
|
3078
|
+
TOutput,
|
|
3079
|
+
ParamsFromPath<TPath>,
|
|
3080
|
+
TAccMeta,
|
|
3081
|
+
TErrorFactory,
|
|
3082
|
+
_TErrorKeys | TDefaultErrors,
|
|
3083
|
+
TPath,
|
|
3084
|
+
TTaps
|
|
3085
|
+
>,
|
|
3086
|
+
TAccMeta,
|
|
3087
|
+
_TErrorKeys | TDefaultErrors,
|
|
3088
|
+
ComputeErrorsByStatus<TErrorFactory, _TErrorKeys | TDefaultErrors, typeof ERROR_META>
|
|
3089
|
+
>,
|
|
3090
|
+
TMeta,
|
|
3091
|
+
TErrorFactory,
|
|
3092
|
+
TDefaultErrors,
|
|
3093
|
+
TBasePath,
|
|
3094
|
+
TTaps,
|
|
3095
|
+
TScopedMw
|
|
3096
|
+
>;
|
|
3097
|
+
|
|
3098
|
+
type OneShotKey = "boundary" | "errors" | "input" | "meta" | "output";
|
|
3099
|
+
|
|
3100
|
+
type BuilderChain<
|
|
3101
|
+
TEnv,
|
|
3102
|
+
TCtx,
|
|
3103
|
+
TInput,
|
|
3104
|
+
TErrorKeys extends string,
|
|
3105
|
+
TOutput,
|
|
3106
|
+
TPath extends string,
|
|
3107
|
+
TMethod extends string,
|
|
3108
|
+
TRoutes,
|
|
3109
|
+
TUsed extends string,
|
|
3110
|
+
TBaseCtx,
|
|
3111
|
+
TMeta,
|
|
3112
|
+
TAccMeta,
|
|
3113
|
+
TErrorFactory,
|
|
3114
|
+
TDefaultErrors extends string,
|
|
3115
|
+
TBasePath extends string,
|
|
3116
|
+
TTaps extends Record<string, unknown> = {},
|
|
3117
|
+
TScopedMw extends readonly ScopedMwEntry[] = [],
|
|
3118
|
+
> = Omit<
|
|
3119
|
+
RouteBuilder<
|
|
3120
|
+
TEnv, TCtx, TInput, TErrorKeys, TOutput, TPath, TMethod,
|
|
3121
|
+
TRoutes, TUsed, TBaseCtx, TMeta, TAccMeta, TErrorFactory,
|
|
3122
|
+
TDefaultErrors, TBasePath, TTaps, TScopedMw
|
|
3123
|
+
>,
|
|
3124
|
+
TUsed & OneShotKey
|
|
3125
|
+
>;
|
|
3126
|
+
|
|
3127
|
+
class RouteBuilder<
|
|
3128
|
+
TEnv,
|
|
3129
|
+
TCtx,
|
|
3130
|
+
TInput = {},
|
|
3131
|
+
_TErrorKeys extends string = never,
|
|
3132
|
+
TOutput = {},
|
|
3133
|
+
TPath extends string = string,
|
|
3134
|
+
TMethod extends string = string,
|
|
3135
|
+
TRoutes = {},
|
|
3136
|
+
TUsed extends string = never,
|
|
3137
|
+
TBaseCtx = TCtx,
|
|
3138
|
+
TMeta = never,
|
|
3139
|
+
TAccMeta = {},
|
|
3140
|
+
TErrorFactory = never,
|
|
3141
|
+
TDefaultErrors extends string = never,
|
|
3142
|
+
TBasePath extends string = "/",
|
|
3143
|
+
TTaps extends Record<string, unknown> = {},
|
|
3144
|
+
TScopedMw extends readonly ScopedMwEntry[] = [],
|
|
3145
|
+
> {
|
|
3146
|
+
private _s: RouteBuilderState<
|
|
3147
|
+
Honey<
|
|
3148
|
+
TEnv,
|
|
3149
|
+
TBaseCtx,
|
|
3150
|
+
TRoutes,
|
|
3151
|
+
TMeta,
|
|
3152
|
+
TErrorFactory,
|
|
3153
|
+
TDefaultErrors,
|
|
3154
|
+
TBasePath,
|
|
3155
|
+
TTaps,
|
|
3156
|
+
TScopedMw
|
|
3157
|
+
>
|
|
3158
|
+
>;
|
|
3159
|
+
|
|
3160
|
+
constructor(
|
|
3161
|
+
state: RouteBuilderState<
|
|
3162
|
+
Honey<
|
|
3163
|
+
TEnv,
|
|
3164
|
+
TBaseCtx,
|
|
3165
|
+
TRoutes,
|
|
3166
|
+
TMeta,
|
|
3167
|
+
TErrorFactory,
|
|
3168
|
+
TDefaultErrors,
|
|
3169
|
+
TBasePath,
|
|
3170
|
+
TTaps,
|
|
3171
|
+
TScopedMw
|
|
3172
|
+
>
|
|
3173
|
+
>,
|
|
3174
|
+
) {
|
|
3175
|
+
this._s = state;
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
errors<
|
|
3179
|
+
TFactory extends Record<string, (...args: never[]) => unknown>,
|
|
3180
|
+
TKeys extends Exclude<keyof TFactory & string, TDefaultErrors>,
|
|
3181
|
+
>(
|
|
3182
|
+
factory: TFactory,
|
|
3183
|
+
...keys: TKeys[]
|
|
3184
|
+
): BuilderChain<
|
|
3185
|
+
TEnv,
|
|
3186
|
+
TCtx,
|
|
3187
|
+
TInput,
|
|
3188
|
+
_TErrorKeys | TKeys,
|
|
3189
|
+
TOutput,
|
|
3190
|
+
TPath,
|
|
3191
|
+
TMethod,
|
|
3192
|
+
TRoutes,
|
|
3193
|
+
TUsed | "errors",
|
|
3194
|
+
TBaseCtx,
|
|
3195
|
+
TMeta,
|
|
3196
|
+
TAccMeta,
|
|
3197
|
+
TErrorFactory,
|
|
3198
|
+
TDefaultErrors,
|
|
3199
|
+
TBasePath,
|
|
3200
|
+
TTaps,
|
|
3201
|
+
TScopedMw
|
|
3202
|
+
>;
|
|
3203
|
+
errors<
|
|
3204
|
+
TKeys extends [TErrorFactory] extends [never]
|
|
3205
|
+
? never
|
|
3206
|
+
: Exclude<keyof TErrorFactory & string, TDefaultErrors>,
|
|
3207
|
+
>(
|
|
3208
|
+
...keys: TKeys[]
|
|
3209
|
+
): BuilderChain<
|
|
3210
|
+
TEnv,
|
|
3211
|
+
TCtx,
|
|
3212
|
+
TInput,
|
|
3213
|
+
_TErrorKeys | TKeys,
|
|
3214
|
+
TOutput,
|
|
3215
|
+
TPath,
|
|
3216
|
+
TMethod,
|
|
3217
|
+
TRoutes,
|
|
3218
|
+
TUsed | "errors",
|
|
3219
|
+
TBaseCtx,
|
|
3220
|
+
TMeta,
|
|
3221
|
+
TAccMeta,
|
|
3222
|
+
TErrorFactory,
|
|
3223
|
+
TDefaultErrors,
|
|
3224
|
+
TBasePath,
|
|
3225
|
+
TTaps,
|
|
3226
|
+
TScopedMw
|
|
3227
|
+
>;
|
|
3228
|
+
errors(
|
|
3229
|
+
...args: unknown[]
|
|
3230
|
+
): BuilderChain<
|
|
3231
|
+
TEnv,
|
|
3232
|
+
TCtx,
|
|
3233
|
+
TInput,
|
|
3234
|
+
string,
|
|
3235
|
+
TOutput,
|
|
3236
|
+
TPath,
|
|
3237
|
+
TMethod,
|
|
3238
|
+
TRoutes,
|
|
3239
|
+
TUsed | "errors",
|
|
3240
|
+
TBaseCtx,
|
|
3241
|
+
TMeta,
|
|
3242
|
+
TAccMeta,
|
|
3243
|
+
TErrorFactory,
|
|
3244
|
+
TDefaultErrors,
|
|
3245
|
+
TBasePath,
|
|
3246
|
+
TTaps,
|
|
3247
|
+
TScopedMw
|
|
3248
|
+
> {
|
|
3249
|
+
const keys =
|
|
3250
|
+
typeof args[0] === "object" && args[0] !== null
|
|
3251
|
+
? (args.slice(1) as string[])
|
|
3252
|
+
: (args as string[]);
|
|
3253
|
+
for (const k of keys) {
|
|
3254
|
+
this._s.errorKeys.add(k);
|
|
3255
|
+
}
|
|
3256
|
+
return new RouteBuilder<
|
|
3257
|
+
TEnv,
|
|
3258
|
+
TCtx,
|
|
3259
|
+
TInput,
|
|
3260
|
+
string,
|
|
3261
|
+
TOutput,
|
|
3262
|
+
TPath,
|
|
3263
|
+
TMethod,
|
|
3264
|
+
TRoutes,
|
|
3265
|
+
TUsed | "errors",
|
|
3266
|
+
TBaseCtx,
|
|
3267
|
+
TMeta,
|
|
3268
|
+
TAccMeta,
|
|
3269
|
+
TErrorFactory,
|
|
3270
|
+
TDefaultErrors,
|
|
3271
|
+
TBasePath,
|
|
3272
|
+
TTaps,
|
|
3273
|
+
TScopedMw
|
|
3274
|
+
>({
|
|
3275
|
+
...this._s,
|
|
3276
|
+
errorKeys: this._s.errorKeys,
|
|
3277
|
+
});
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
boundary<
|
|
3281
|
+
TKey extends [TErrorFactory] extends [never]
|
|
3282
|
+
? never
|
|
3283
|
+
: Exclude<keyof TErrorFactory & string, TDefaultErrors>,
|
|
3284
|
+
>(
|
|
3285
|
+
key: TKey,
|
|
3286
|
+
): BuilderChain<
|
|
3287
|
+
TEnv,
|
|
3288
|
+
TCtx,
|
|
3289
|
+
TInput,
|
|
3290
|
+
_TErrorKeys | TKey,
|
|
3291
|
+
TOutput,
|
|
3292
|
+
TPath,
|
|
3293
|
+
TMethod,
|
|
3294
|
+
TRoutes,
|
|
3295
|
+
TUsed | "boundary",
|
|
3296
|
+
TBaseCtx,
|
|
3297
|
+
TMeta,
|
|
3298
|
+
TAccMeta,
|
|
3299
|
+
TErrorFactory,
|
|
3300
|
+
TDefaultErrors,
|
|
3301
|
+
TBasePath,
|
|
3302
|
+
TTaps,
|
|
3303
|
+
TScopedMw
|
|
3304
|
+
> {
|
|
3305
|
+
this._s.boundaryErrorKey = key;
|
|
3306
|
+
this._s.errorKeys.add(key);
|
|
3307
|
+
return new RouteBuilder<
|
|
3308
|
+
TEnv,
|
|
3309
|
+
TCtx,
|
|
3310
|
+
TInput,
|
|
3311
|
+
_TErrorKeys | TKey,
|
|
3312
|
+
TOutput,
|
|
3313
|
+
TPath,
|
|
3314
|
+
TMethod,
|
|
3315
|
+
TRoutes,
|
|
3316
|
+
TUsed | "boundary",
|
|
3317
|
+
TBaseCtx,
|
|
3318
|
+
TMeta,
|
|
3319
|
+
TAccMeta,
|
|
3320
|
+
TErrorFactory,
|
|
3321
|
+
TDefaultErrors,
|
|
3322
|
+
TBasePath,
|
|
3323
|
+
TTaps,
|
|
3324
|
+
TScopedMw
|
|
3325
|
+
>({
|
|
3326
|
+
...this._s,
|
|
3327
|
+
boundaryErrorKey: key,
|
|
3328
|
+
errorKeys: this._s.errorKeys,
|
|
3329
|
+
});
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
handler(
|
|
3333
|
+
fn: (
|
|
3334
|
+
ctx: HandlerCtx<
|
|
3335
|
+
TCtx,
|
|
3336
|
+
TInput,
|
|
3337
|
+
TOutput,
|
|
3338
|
+
ParamsFromPath<TPath>,
|
|
3339
|
+
TAccMeta,
|
|
3340
|
+
TErrorFactory,
|
|
3341
|
+
_TErrorKeys | TDefaultErrors,
|
|
3342
|
+
TPath,
|
|
3343
|
+
TTaps
|
|
3344
|
+
>,
|
|
3345
|
+
) => TypedResponse | Promise<TypedResponse>,
|
|
3346
|
+
): HandlerReturn<
|
|
3347
|
+
TEnv,
|
|
3348
|
+
TBaseCtx,
|
|
3349
|
+
TRoutes,
|
|
3350
|
+
TPath,
|
|
3351
|
+
TMethod,
|
|
3352
|
+
TInput,
|
|
3353
|
+
TOutput,
|
|
3354
|
+
TCtx,
|
|
3355
|
+
TAccMeta,
|
|
3356
|
+
TErrorFactory,
|
|
3357
|
+
_TErrorKeys,
|
|
3358
|
+
TDefaultErrors,
|
|
3359
|
+
TMeta,
|
|
3360
|
+
TBasePath,
|
|
3361
|
+
TTaps,
|
|
3362
|
+
TScopedMw
|
|
3363
|
+
> {
|
|
3364
|
+
let ov: OutputValidator | null = null;
|
|
3365
|
+
const outputSchemas = this._s.outputSchemas;
|
|
3366
|
+
if (outputSchemas) {
|
|
3367
|
+
const jsonSchemas = outputSchemas["application/json"];
|
|
3368
|
+
if (jsonSchemas) {
|
|
3369
|
+
ov = async (statusKey: string, data: unknown) => {
|
|
3370
|
+
const schema = jsonSchemas[statusKey as keyof typeof jsonSchemas];
|
|
3371
|
+
if (schema) {
|
|
3372
|
+
await validateOutput(schema, statusKey, data);
|
|
3373
|
+
}
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
const isInternal = Symbol.for("honey.internal") in fn
|
|
3379
|
+
if (isInternal) {
|
|
3380
|
+
Object.defineProperty(fn, Symbol.for("honey.app"), { value: this._s.parent })
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
const routeHandler: RouteHandler = {
|
|
3384
|
+
_skip: isInternal || undefined,
|
|
3385
|
+
bek: this._s.boundaryErrorKey,
|
|
3386
|
+
ef: null,
|
|
3387
|
+
ek: this._s.errorKeys,
|
|
3388
|
+
fn: fn as (ctx: unknown) => Response | Promise<Response>,
|
|
3389
|
+
iv: this._s.inputSchemas,
|
|
3390
|
+
mt: this._s.meta ? Object.freeze(this._s.meta) : null,
|
|
3391
|
+
mw: [...this._s.parentMiddlewares, ...this._s.middlewares],
|
|
3392
|
+
os: this._s.outputSchemas,
|
|
3393
|
+
ov,
|
|
3394
|
+
rp: this._s.path,
|
|
3395
|
+
};
|
|
3396
|
+
|
|
3397
|
+
/* patch mode: if handler map has this route, patch existing handler instead of insertRoute */
|
|
3398
|
+
const handlerMap = this._s.handlerMap;
|
|
3399
|
+
if (handlerMap) {
|
|
3400
|
+
const key = `${this._s.method} ${this._s.path}`;
|
|
3401
|
+
const existing = handlerMap[key];
|
|
3402
|
+
if (existing) {
|
|
3403
|
+
existing.fn = routeHandler.fn;
|
|
3404
|
+
existing.mw = routeHandler.mw;
|
|
3405
|
+
existing.iv = routeHandler.iv;
|
|
3406
|
+
existing.os = routeHandler.os;
|
|
3407
|
+
existing.ov = routeHandler.ov;
|
|
3408
|
+
/* pre-compute filtered error factory */
|
|
3409
|
+
const factory = (this._s.parent as unknown as HoneyInternal)._factory;
|
|
3410
|
+
if (factory !== null && existing.ek.size > 0) {
|
|
3411
|
+
const ef = Object.create(null) as Record<
|
|
3412
|
+
string,
|
|
3413
|
+
(...args: never[]) => unknown
|
|
3414
|
+
>;
|
|
3415
|
+
const fac = factory as Record<string, (...args: never[]) => unknown>;
|
|
3416
|
+
for (const k of existing.ek) {
|
|
3417
|
+
const factoryFn = fac[k]
|
|
3418
|
+
if (factoryFn !== undefined) {
|
|
3419
|
+
ef[k] = factoryFn
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
existing.ef = Object.freeze(ef);
|
|
3423
|
+
}
|
|
3424
|
+
return this._s.parent as HandlerReturn<
|
|
3425
|
+
TEnv,
|
|
3426
|
+
TBaseCtx,
|
|
3427
|
+
TRoutes,
|
|
3428
|
+
TPath,
|
|
3429
|
+
TMethod,
|
|
3430
|
+
TInput,
|
|
3431
|
+
TOutput,
|
|
3432
|
+
TCtx,
|
|
3433
|
+
TAccMeta,
|
|
3434
|
+
TErrorFactory,
|
|
3435
|
+
_TErrorKeys,
|
|
3436
|
+
TDefaultErrors,
|
|
3437
|
+
TMeta,
|
|
3438
|
+
TBasePath,
|
|
3439
|
+
TTaps,
|
|
3440
|
+
TScopedMw
|
|
3441
|
+
>;
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
insertRoute(this._s.root, this._s.method, this._s.path, routeHandler);
|
|
3446
|
+
|
|
3447
|
+
/* .on() extra methods — insert same handler for each additional method */
|
|
3448
|
+
if (this._s.extraMethods) {
|
|
3449
|
+
for (const m of this._s.extraMethods) {
|
|
3450
|
+
insertRoute(this._s.root, m, this._s.path, routeHandler);
|
|
3451
|
+
if (!this._s.path.includes(":") && !this._s.path.includes("*")) {
|
|
3452
|
+
(this._s.parent as unknown as HoneyInternal)._registerStatic(`${m} ${this._s.path}`, routeHandler);
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
/* populate static route map for O(1) lookup on non-parameterized routes */
|
|
3458
|
+
if (!this._s.path.includes(":") && !this._s.path.includes("*")) {
|
|
3459
|
+
(this._s.parent as unknown as HoneyInternal)._registerStatic(
|
|
3460
|
+
`${this._s.method} ${this._s.path}`,
|
|
3461
|
+
routeHandler,
|
|
3462
|
+
);
|
|
3463
|
+
}
|
|
3464
|
+
return this._s.parent as HandlerReturn<
|
|
3465
|
+
TEnv,
|
|
3466
|
+
TBaseCtx,
|
|
3467
|
+
TRoutes,
|
|
3468
|
+
TPath,
|
|
3469
|
+
TMethod,
|
|
3470
|
+
TInput,
|
|
3471
|
+
TOutput,
|
|
3472
|
+
TCtx,
|
|
3473
|
+
TAccMeta,
|
|
3474
|
+
TErrorFactory,
|
|
3475
|
+
_TErrorKeys,
|
|
3476
|
+
TDefaultErrors,
|
|
3477
|
+
TMeta,
|
|
3478
|
+
TBasePath,
|
|
3479
|
+
TTaps,
|
|
3480
|
+
TScopedMw
|
|
3481
|
+
>;
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
proxy(
|
|
3485
|
+
config: ProxyConfig<
|
|
3486
|
+
HandlerCtx<
|
|
3487
|
+
TCtx,
|
|
3488
|
+
TInput,
|
|
3489
|
+
TOutput,
|
|
3490
|
+
ParamsFromPath<TPath>,
|
|
3491
|
+
TAccMeta,
|
|
3492
|
+
TErrorFactory,
|
|
3493
|
+
_TErrorKeys | TDefaultErrors,
|
|
3494
|
+
TPath,
|
|
3495
|
+
TTaps
|
|
3496
|
+
>
|
|
3497
|
+
>,
|
|
3498
|
+
): HandlerReturn<
|
|
3499
|
+
TEnv,
|
|
3500
|
+
TBaseCtx,
|
|
3501
|
+
TRoutes,
|
|
3502
|
+
TPath,
|
|
3503
|
+
TMethod,
|
|
3504
|
+
TInput,
|
|
3505
|
+
TOutput,
|
|
3506
|
+
TCtx,
|
|
3507
|
+
TAccMeta,
|
|
3508
|
+
TErrorFactory,
|
|
3509
|
+
_TErrorKeys,
|
|
3510
|
+
TDefaultErrors,
|
|
3511
|
+
TMeta,
|
|
3512
|
+
TBasePath,
|
|
3513
|
+
TTaps,
|
|
3514
|
+
TScopedMw
|
|
3515
|
+
> {
|
|
3516
|
+
const proxyHandler = createProxyHandler(config);
|
|
3517
|
+
return this.handler(
|
|
3518
|
+
proxyHandler as (
|
|
3519
|
+
ctx: HandlerCtx<
|
|
3520
|
+
TCtx,
|
|
3521
|
+
TInput,
|
|
3522
|
+
TOutput,
|
|
3523
|
+
ParamsFromPath<TPath>,
|
|
3524
|
+
TAccMeta,
|
|
3525
|
+
TErrorFactory,
|
|
3526
|
+
_TErrorKeys | TDefaultErrors,
|
|
3527
|
+
TPath,
|
|
3528
|
+
TTaps
|
|
3529
|
+
>,
|
|
3530
|
+
) => Promise<TypedResponse>,
|
|
3531
|
+
);
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
input<TSchemas extends InputSchemasDef>(
|
|
3535
|
+
schemas: TSchemas,
|
|
3536
|
+
): BuilderChain<
|
|
3537
|
+
TEnv,
|
|
3538
|
+
TCtx,
|
|
3539
|
+
TInput & InferInputMap<TSchemas>,
|
|
3540
|
+
_TErrorKeys,
|
|
3541
|
+
TOutput,
|
|
3542
|
+
TPath,
|
|
3543
|
+
TMethod,
|
|
3544
|
+
TRoutes,
|
|
3545
|
+
TUsed | "input",
|
|
3546
|
+
TBaseCtx,
|
|
3547
|
+
TMeta,
|
|
3548
|
+
TAccMeta,
|
|
3549
|
+
TErrorFactory,
|
|
3550
|
+
TDefaultErrors,
|
|
3551
|
+
TBasePath,
|
|
3552
|
+
TTaps,
|
|
3553
|
+
TScopedMw
|
|
3554
|
+
> {
|
|
3555
|
+
return new RouteBuilder<
|
|
3556
|
+
TEnv,
|
|
3557
|
+
TCtx,
|
|
3558
|
+
TInput & InferInputMap<TSchemas>,
|
|
3559
|
+
_TErrorKeys,
|
|
3560
|
+
TOutput,
|
|
3561
|
+
TPath,
|
|
3562
|
+
TMethod,
|
|
3563
|
+
TRoutes,
|
|
3564
|
+
TUsed | "input",
|
|
3565
|
+
TBaseCtx,
|
|
3566
|
+
TMeta,
|
|
3567
|
+
TAccMeta,
|
|
3568
|
+
TErrorFactory,
|
|
3569
|
+
TDefaultErrors,
|
|
3570
|
+
TBasePath,
|
|
3571
|
+
TTaps,
|
|
3572
|
+
TScopedMw
|
|
3573
|
+
>({
|
|
3574
|
+
...this._s,
|
|
3575
|
+
inputSchemas: schemas,
|
|
3576
|
+
});
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
meta<
|
|
3580
|
+
TRouteMeta extends Partial<DefaultMeta> &
|
|
3581
|
+
([TMeta] extends [never] ? {} : TMeta),
|
|
3582
|
+
>(
|
|
3583
|
+
meta: TRouteMeta,
|
|
3584
|
+
): BuilderChain<
|
|
3585
|
+
TEnv,
|
|
3586
|
+
TCtx,
|
|
3587
|
+
TInput,
|
|
3588
|
+
_TErrorKeys,
|
|
3589
|
+
TOutput,
|
|
3590
|
+
TPath,
|
|
3591
|
+
TMethod,
|
|
3592
|
+
TRoutes,
|
|
3593
|
+
TUsed | "meta",
|
|
3594
|
+
TBaseCtx,
|
|
3595
|
+
TMeta,
|
|
3596
|
+
TAccMeta & TRouteMeta,
|
|
3597
|
+
TErrorFactory,
|
|
3598
|
+
TDefaultErrors,
|
|
3599
|
+
TBasePath,
|
|
3600
|
+
TTaps,
|
|
3601
|
+
TScopedMw
|
|
3602
|
+
> {
|
|
3603
|
+
return new RouteBuilder<
|
|
3604
|
+
TEnv,
|
|
3605
|
+
TCtx,
|
|
3606
|
+
TInput,
|
|
3607
|
+
_TErrorKeys,
|
|
3608
|
+
TOutput,
|
|
3609
|
+
TPath,
|
|
3610
|
+
TMethod,
|
|
3611
|
+
TRoutes,
|
|
3612
|
+
TUsed | "meta",
|
|
3613
|
+
TBaseCtx,
|
|
3614
|
+
TMeta,
|
|
3615
|
+
TAccMeta & TRouteMeta,
|
|
3616
|
+
TErrorFactory,
|
|
3617
|
+
TDefaultErrors,
|
|
3618
|
+
TBasePath,
|
|
3619
|
+
TTaps,
|
|
3620
|
+
TScopedMw
|
|
3621
|
+
>({
|
|
3622
|
+
...this._s,
|
|
3623
|
+
meta: (() => {
|
|
3624
|
+
const merged: Record<string, unknown> = { ...this._s.meta, ...meta }
|
|
3625
|
+
const inv = merged["invalidate"]
|
|
3626
|
+
if (Array.isArray(inv) && inv.length > 1) {
|
|
3627
|
+
merged["invalidate"] = [...new Set(inv)]
|
|
3628
|
+
}
|
|
3629
|
+
return merged
|
|
3630
|
+
})(),
|
|
3631
|
+
});
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3634
|
+
output<TOutputSchemas extends OutputSchemaDef>(
|
|
3635
|
+
_schemas: TOutputSchemas,
|
|
3636
|
+
): BuilderChain<
|
|
3637
|
+
TEnv,
|
|
3638
|
+
TCtx,
|
|
3639
|
+
TInput,
|
|
3640
|
+
_TErrorKeys,
|
|
3641
|
+
TOutputSchemas,
|
|
3642
|
+
TPath,
|
|
3643
|
+
TMethod,
|
|
3644
|
+
TRoutes,
|
|
3645
|
+
TUsed | "output",
|
|
3646
|
+
TBaseCtx,
|
|
3647
|
+
TMeta,
|
|
3648
|
+
TAccMeta,
|
|
3649
|
+
TErrorFactory,
|
|
3650
|
+
TDefaultErrors,
|
|
3651
|
+
TBasePath,
|
|
3652
|
+
TTaps,
|
|
3653
|
+
TScopedMw
|
|
3654
|
+
> {
|
|
3655
|
+
return new RouteBuilder<
|
|
3656
|
+
TEnv,
|
|
3657
|
+
TCtx,
|
|
3658
|
+
TInput,
|
|
3659
|
+
_TErrorKeys,
|
|
3660
|
+
TOutputSchemas,
|
|
3661
|
+
TPath,
|
|
3662
|
+
TMethod,
|
|
3663
|
+
TRoutes,
|
|
3664
|
+
TUsed | "output",
|
|
3665
|
+
TBaseCtx,
|
|
3666
|
+
TMeta,
|
|
3667
|
+
TAccMeta,
|
|
3668
|
+
TErrorFactory,
|
|
3669
|
+
TDefaultErrors,
|
|
3670
|
+
TBasePath,
|
|
3671
|
+
TTaps,
|
|
3672
|
+
TScopedMw
|
|
3673
|
+
>({
|
|
3674
|
+
...this._s,
|
|
3675
|
+
outputSchemas: _schemas,
|
|
3676
|
+
});
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
use<TAdds>(
|
|
3680
|
+
mw: MiddlewareFn<TCtx, TAdds>,
|
|
3681
|
+
): BuilderChain<
|
|
3682
|
+
TEnv,
|
|
3683
|
+
TCtx & TAdds,
|
|
3684
|
+
TInput,
|
|
3685
|
+
_TErrorKeys,
|
|
3686
|
+
TOutput,
|
|
3687
|
+
TPath,
|
|
3688
|
+
TMethod,
|
|
3689
|
+
TRoutes,
|
|
3690
|
+
TUsed,
|
|
3691
|
+
TBaseCtx,
|
|
3692
|
+
TMeta,
|
|
3693
|
+
TAccMeta,
|
|
3694
|
+
TErrorFactory,
|
|
3695
|
+
TDefaultErrors,
|
|
3696
|
+
TBasePath,
|
|
3697
|
+
TTaps,
|
|
3698
|
+
TScopedMw
|
|
3699
|
+
> {
|
|
3700
|
+
if (mw.errors) {
|
|
3701
|
+
for (const k of mw.errors) {
|
|
3702
|
+
this._s.errorKeys.add(k);
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
return new RouteBuilder<
|
|
3706
|
+
TEnv,
|
|
3707
|
+
TCtx & TAdds,
|
|
3708
|
+
TInput,
|
|
3709
|
+
_TErrorKeys,
|
|
3710
|
+
TOutput,
|
|
3711
|
+
TPath,
|
|
3712
|
+
TMethod,
|
|
3713
|
+
TRoutes,
|
|
3714
|
+
TUsed,
|
|
3715
|
+
TBaseCtx,
|
|
3716
|
+
TMeta,
|
|
3717
|
+
TAccMeta,
|
|
3718
|
+
TErrorFactory,
|
|
3719
|
+
TDefaultErrors,
|
|
3720
|
+
TBasePath,
|
|
3721
|
+
TTaps,
|
|
3722
|
+
TScopedMw
|
|
3723
|
+
>({
|
|
3724
|
+
...this._s,
|
|
3725
|
+
middlewares: [...this._s.middlewares, mw as RuntimeMiddleware],
|
|
3726
|
+
});
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
type WSRouteBuilderState<TParent> = {
|
|
3731
|
+
boundaryErrorKey: string | null;
|
|
3732
|
+
errorKeys: Set<string>;
|
|
3733
|
+
inputSchemas: InputSchemasDef | null;
|
|
3734
|
+
meta: Record<string, unknown> | null;
|
|
3735
|
+
middlewares: RuntimeMiddleware[];
|
|
3736
|
+
parent: TParent;
|
|
3737
|
+
parentMiddlewares: RuntimeMiddleware[];
|
|
3738
|
+
path: string;
|
|
3739
|
+
root: TreeNode;
|
|
3740
|
+
};
|
|
3741
|
+
|
|
3742
|
+
class WSRouteBuilder<
|
|
3743
|
+
TEnv,
|
|
3744
|
+
TCtx,
|
|
3745
|
+
TInput = {},
|
|
3746
|
+
_TErrorKeys extends string = never,
|
|
3747
|
+
TParent extends { _markWsRoutes(): void } = { _markWsRoutes(): void },
|
|
3748
|
+
> {
|
|
3749
|
+
private _s: WSRouteBuilderState<TParent>;
|
|
3750
|
+
|
|
3751
|
+
constructor(state: WSRouteBuilderState<TParent>) {
|
|
3752
|
+
this._s = state;
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
errors<TFactory extends Record<string, (...args: never[]) => unknown>>(
|
|
3756
|
+
factory: TFactory,
|
|
3757
|
+
...keys: Array<keyof TFactory & string>
|
|
3758
|
+
): WSRouteBuilder<
|
|
3759
|
+
TEnv,
|
|
3760
|
+
TCtx,
|
|
3761
|
+
TInput,
|
|
3762
|
+
_TErrorKeys | (keyof TFactory & string),
|
|
3763
|
+
TParent
|
|
3764
|
+
> {
|
|
3765
|
+
void factory;
|
|
3766
|
+
for (const k of keys) {
|
|
3767
|
+
this._s.errorKeys.add(k);
|
|
3768
|
+
}
|
|
3769
|
+
return new WSRouteBuilder<
|
|
3770
|
+
TEnv,
|
|
3771
|
+
TCtx,
|
|
3772
|
+
TInput,
|
|
3773
|
+
_TErrorKeys | (keyof TFactory & string),
|
|
3774
|
+
TParent
|
|
3775
|
+
>({
|
|
3776
|
+
...this._s,
|
|
3777
|
+
errorKeys: this._s.errorKeys,
|
|
3778
|
+
});
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
handler(wsHandler: WSHandler<TCtx>): TParent {
|
|
3782
|
+
const routeHandler: WSRouteHandler = {
|
|
3783
|
+
bek: this._s.boundaryErrorKey,
|
|
3784
|
+
ek: this._s.errorKeys,
|
|
3785
|
+
fn: wsHandler as WSHandler<unknown>,
|
|
3786
|
+
iv: this._s.inputSchemas,
|
|
3787
|
+
mt: this._s.meta ? Object.freeze(this._s.meta) : null,
|
|
3788
|
+
mw: [...this._s.parentMiddlewares, ...this._s.middlewares],
|
|
3789
|
+
rp: this._s.path,
|
|
3790
|
+
};
|
|
3791
|
+
insertWsRoute(this._s.root, this._s.path, routeHandler);
|
|
3792
|
+
;(this._s.parent as unknown as HoneyInternal)._markWsRoutes();
|
|
3793
|
+
return this._s.parent;
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
input<
|
|
3797
|
+
TSchemas extends Pick<InputSchemasDef, "cookies" | "headers" | "search">,
|
|
3798
|
+
>(
|
|
3799
|
+
schemas: TSchemas,
|
|
3800
|
+
): WSRouteBuilder<
|
|
3801
|
+
TEnv,
|
|
3802
|
+
TCtx,
|
|
3803
|
+
TInput & InferInputMap<TSchemas>,
|
|
3804
|
+
_TErrorKeys,
|
|
3805
|
+
TParent
|
|
3806
|
+
> {
|
|
3807
|
+
return new WSRouteBuilder<
|
|
3808
|
+
TEnv,
|
|
3809
|
+
TCtx,
|
|
3810
|
+
TInput & InferInputMap<TSchemas>,
|
|
3811
|
+
_TErrorKeys,
|
|
3812
|
+
TParent
|
|
3813
|
+
>({
|
|
3814
|
+
...this._s,
|
|
3815
|
+
inputSchemas: schemas,
|
|
3816
|
+
});
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
meta(meta: Record<string, unknown>): this {
|
|
3820
|
+
this._s.meta = meta;
|
|
3821
|
+
return this;
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
use<TAdds>(
|
|
3825
|
+
mw: MiddlewareFn<TCtx, TAdds>,
|
|
3826
|
+
): WSRouteBuilder<TEnv, TCtx & TAdds, TInput, _TErrorKeys, TParent> {
|
|
3827
|
+
if (mw.errors) {
|
|
3828
|
+
for (const k of mw.errors) {
|
|
3829
|
+
this._s.errorKeys.add(k);
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
return new WSRouteBuilder<TEnv, TCtx & TAdds, TInput, _TErrorKeys, TParent>(
|
|
3833
|
+
{
|
|
3834
|
+
...this._s,
|
|
3835
|
+
middlewares: [...this._s.middlewares, mw as RuntimeMiddleware],
|
|
3836
|
+
},
|
|
3837
|
+
);
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
export function honey<TEnv>(): Honey<TEnv> {
|
|
3842
|
+
return new Honey<TEnv>();
|
|
3843
|
+
}
|