@beignet/core 0.0.33 → 0.0.35
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/CHANGELOG.md +24 -0
- package/README.md +207 -30
- package/dist/agent-capabilities/index.d.ts +167 -0
- package/dist/agent-capabilities/index.d.ts.map +1 -0
- package/dist/agent-capabilities/index.js +275 -0
- package/dist/agent-capabilities/index.js.map +1 -0
- package/dist/application/index.d.ts +2 -2
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +13 -3
- package/dist/application/index.js.map +1 -1
- package/dist/client/client.d.ts +2 -0
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +33 -16
- package/dist/client/client.js.map +1 -1
- package/dist/contracts/contract-builder.d.ts +5 -7
- package/dist/contracts/contract-builder.d.ts.map +1 -1
- package/dist/contracts/contract-builder.js +3 -18
- package/dist/contracts/contract-builder.js.map +1 -1
- package/dist/contracts/contract-group.d.ts +2 -2
- package/dist/contracts/contract-group.d.ts.map +1 -1
- package/dist/contracts/contract-group.js +2 -1
- package/dist/contracts/contract-group.js.map +1 -1
- package/dist/contracts/metadata.d.ts +3 -0
- package/dist/contracts/metadata.d.ts.map +1 -0
- package/dist/contracts/metadata.js +14 -0
- package/dist/contracts/metadata.js.map +1 -0
- package/dist/contracts/types.d.ts +20 -0
- package/dist/contracts/types.d.ts.map +1 -1
- package/dist/contracts/types.js.map +1 -1
- package/dist/events/index.d.ts +4 -4
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +5 -5
- package/dist/events/index.js.map +1 -1
- package/dist/flags/index.d.ts +0 -4
- package/dist/flags/index.d.ts.map +1 -1
- package/dist/flags/index.js +0 -4
- package/dist/flags/index.js.map +1 -1
- package/dist/idempotency/index.d.ts.map +1 -1
- package/dist/idempotency/index.js +14 -9
- package/dist/idempotency/index.js.map +1 -1
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +10 -4
- package/dist/jobs/index.js.map +1 -1
- package/dist/notifications/index.d.ts +194 -10
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +397 -59
- package/dist/notifications/index.js.map +1 -1
- package/dist/openapi/index.js +15 -8
- package/dist/openapi/index.js.map +1 -1
- package/dist/ports/auth.d.ts +2 -0
- package/dist/ports/auth.d.ts.map +1 -1
- package/dist/ports/auth.js +2 -0
- package/dist/ports/auth.js.map +1 -1
- package/dist/ports/events.d.ts +2 -2
- package/dist/ports/index.d.ts +1 -1
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +1 -1
- package/dist/ports/index.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/provider.d.ts +10 -2
- package/dist/providers/provider.d.ts.map +1 -1
- package/dist/providers/provider.js.map +1 -1
- package/dist/schedules/index.d.ts +6 -13
- package/dist/schedules/index.d.ts.map +1 -1
- package/dist/schedules/index.js +24 -44
- package/dist/schedules/index.js.map +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/request-executor.d.ts +72 -0
- package/dist/server/request-executor.d.ts.map +1 -0
- package/dist/server/request-executor.js +781 -0
- package/dist/server/request-executor.js.map +1 -0
- package/dist/server/request-preparation.d.ts +39 -0
- package/dist/server/request-preparation.d.ts.map +1 -0
- package/dist/server/request-preparation.js +248 -0
- package/dist/server/request-preparation.js.map +1 -0
- package/dist/server/response-finalization.d.ts +34 -0
- package/dist/server/response-finalization.d.ts.map +1 -0
- package/dist/server/response-finalization.js +279 -0
- package/dist/server/response-finalization.js.map +1 -0
- package/dist/server/route-definitions.d.ts +139 -0
- package/dist/server/route-definitions.d.ts.map +1 -0
- package/dist/server/route-definitions.js +94 -0
- package/dist/server/route-definitions.js.map +1 -0
- package/dist/server/route-matching.d.ts +15 -0
- package/dist/server/route-matching.d.ts.map +1 -0
- package/dist/server/route-matching.js +46 -0
- package/dist/server/route-matching.js.map +1 -0
- package/dist/server/server.d.ts +6 -157
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +29 -1340
- package/dist/server/server.js.map +1 -1
- package/dist/server/use-case-route.d.ts +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/tracing/index.d.ts +1 -1
- package/dist/tracing/index.d.ts.map +1 -1
- package/dist/tracing/index.js.map +1 -1
- package/dist/uploads/client.js +4 -7
- package/dist/uploads/client.js.map +1 -1
- package/dist/uploads/index.d.ts.map +1 -1
- package/dist/uploads/index.js +59 -12
- package/dist/uploads/index.js.map +1 -1
- package/package.json +5 -5
- package/skills/app-architecture/SKILL.md +31 -1
- package/src/agent-capabilities/index.ts +578 -0
- package/src/application/index.ts +19 -5
- package/src/client/client.ts +41 -17
- package/src/contracts/contract-builder.ts +8 -23
- package/src/contracts/contract-group.ts +4 -6
- package/src/contracts/metadata.ts +20 -0
- package/src/contracts/types.ts +39 -0
- package/src/events/index.ts +16 -20
- package/src/flags/index.ts +0 -5
- package/src/idempotency/index.ts +17 -9
- package/src/jobs/index.ts +8 -3
- package/src/notifications/index.ts +678 -70
- package/src/openapi/index.ts +24 -12
- package/src/ports/auth.ts +2 -0
- package/src/ports/events.ts +2 -2
- package/src/ports/index.ts +0 -1
- package/src/providers/index.ts +1 -0
- package/src/providers/provider.ts +18 -3
- package/src/schedules/index.ts +57 -63
- package/src/server/index.ts +4 -2
- package/src/server/request-executor.ts +1170 -0
- package/src/server/request-preparation.ts +409 -0
- package/src/server/response-finalization.ts +385 -0
- package/src/server/route-definitions.ts +338 -0
- package/src/server/route-matching.ts +64 -0
- package/src/server/server.ts +63 -2171
- package/src/server/use-case-route.ts +1 -1
- package/src/testing/index.ts +2 -0
- package/src/tracing/index.ts +1 -0
- package/src/uploads/client.ts +4 -7
- package/src/uploads/index.ts +70 -12
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
import { httpErrors, isAppError, toErrorResponseBody, } from "../errors/index.js";
|
|
2
|
+
import { IdempotencyConflictError, IdempotencyInProgressError, } from "../idempotency/index.js";
|
|
3
|
+
import { runWithMemoScope, } from "../memo/index.js";
|
|
4
|
+
import { AuthUnauthorizedError, EntitlementRequiredError, GateAuthorizationError, TenantRequiredError, } from "../ports/index.js";
|
|
5
|
+
import { createProviderInstrumentation, resolveProviderInstrumentationPort, } from "../providers/index.js";
|
|
6
|
+
import { parseTraceparent, resolveTracingPort, runWithTracing, } from "../tracing/index.js";
|
|
7
|
+
import { getResponseFinalizerHook, } from "./internal-hooks.js";
|
|
8
|
+
import { readContextActor, readContextTenant, setActiveRequestIdentity, } from "./request-context.js";
|
|
9
|
+
import { prepareRequestInputs, requestBodyLimit, requestHeadersToRecord, } from "./request-preparation.js";
|
|
10
|
+
import { defaultErrorResponse, errorResponse, finalizeResponse, isHttpResponseLike, isWebResponse, mergeNativeResponseHeaders, normalizeHttpResponse, normalizeResponse, ResponseContractViolationError, responseForHooks, responseOwnerFor, toContractViolationResponse, withFrameworkErrorOwnerHeader, } from "./response-finalization.js";
|
|
11
|
+
import { compilePath, decodeMatchedParams, PathDecodeError, } from "./route-matching.js";
|
|
12
|
+
function withoutHeadResponseBody(response, requestMethod) {
|
|
13
|
+
if (requestMethod.toUpperCase() !== "HEAD")
|
|
14
|
+
return response;
|
|
15
|
+
if (isWebResponse(response)) {
|
|
16
|
+
if (response.body === null)
|
|
17
|
+
return response;
|
|
18
|
+
void response.body.cancel().catch(() => { });
|
|
19
|
+
return new Response(null, {
|
|
20
|
+
status: response.status,
|
|
21
|
+
statusText: response.statusText,
|
|
22
|
+
headers: response.headers,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const { body: _body, ...headResponse } = response;
|
|
26
|
+
return headResponse;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build the per-request execution pipeline once.
|
|
30
|
+
*
|
|
31
|
+
* The returned executor takes the contract, compiled pattern, and user handler
|
|
32
|
+
* per invocation so fallback responses (404/405) can reuse a single pipeline
|
|
33
|
+
* across requests instead of rebuilding it per unmatched request.
|
|
34
|
+
*/
|
|
35
|
+
export function createRequestExecutor(options, finalPorts, contextRuntime, hooks, routeHooks = [], optionsOverrides) {
|
|
36
|
+
const warnedNativeReplacementHooks = new WeakSet();
|
|
37
|
+
const maxRequestBodyBytes = requestBodyLimit(options.requestBody);
|
|
38
|
+
const executeRequest = async (target, req, preMatchedParams) => {
|
|
39
|
+
const { contract, handler: userHandler } = target;
|
|
40
|
+
let baseCtx;
|
|
41
|
+
let pathValue;
|
|
42
|
+
let queryValue;
|
|
43
|
+
let headersValue;
|
|
44
|
+
let bodyValue;
|
|
45
|
+
const startedAt = Date.now();
|
|
46
|
+
// Stage timings accumulate because retries re-enter the send phase and
|
|
47
|
+
// beforeHandle spans the route-hook and server-hook loops.
|
|
48
|
+
const stages = {
|
|
49
|
+
onRequestMs: 0,
|
|
50
|
+
parseMs: 0,
|
|
51
|
+
contextMs: 0,
|
|
52
|
+
beforeHandleMs: 0,
|
|
53
|
+
handlerMs: 0,
|
|
54
|
+
sendMs: 0,
|
|
55
|
+
};
|
|
56
|
+
const timeStage = async (stage, run) => {
|
|
57
|
+
const stageStartedAt = performance.now();
|
|
58
|
+
try {
|
|
59
|
+
return await run();
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
stages[stage] += performance.now() - stageStartedAt;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const resolveErrorResult = async (error, ctx, path, query, headers, body, resultOptions) => {
|
|
66
|
+
let currentError = error;
|
|
67
|
+
const notifyCaughtError = async (caught) => {
|
|
68
|
+
const args = {
|
|
69
|
+
err: caught,
|
|
70
|
+
req,
|
|
71
|
+
ctx,
|
|
72
|
+
contract,
|
|
73
|
+
path,
|
|
74
|
+
query,
|
|
75
|
+
headers,
|
|
76
|
+
body,
|
|
77
|
+
};
|
|
78
|
+
for (const hook of hooks) {
|
|
79
|
+
if (!hook.onCaughtError)
|
|
80
|
+
continue;
|
|
81
|
+
try {
|
|
82
|
+
await hook.onCaughtError(args);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Observers must not change response behavior.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (options.onCaughtError) {
|
|
89
|
+
try {
|
|
90
|
+
await options.onCaughtError(args);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Observers must not change response behavior.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
await notifyCaughtError(currentError);
|
|
98
|
+
if (currentError instanceof ResponseContractViolationError) {
|
|
99
|
+
return {
|
|
100
|
+
ctx,
|
|
101
|
+
response: toContractViolationResponse(currentError),
|
|
102
|
+
error: currentError,
|
|
103
|
+
owner: "framework",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (isAppError(currentError)) {
|
|
107
|
+
return {
|
|
108
|
+
ctx,
|
|
109
|
+
response: {
|
|
110
|
+
status: currentError.status,
|
|
111
|
+
...(currentError.headers
|
|
112
|
+
? { headers: { ...currentError.headers } }
|
|
113
|
+
: {}),
|
|
114
|
+
body: toErrorResponseBody(currentError),
|
|
115
|
+
},
|
|
116
|
+
error: currentError,
|
|
117
|
+
owner: resultOptions?.owner ?? "route",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (currentError instanceof AuthUnauthorizedError) {
|
|
121
|
+
return {
|
|
122
|
+
ctx,
|
|
123
|
+
response: errorResponse(401, currentError.code, currentError.message),
|
|
124
|
+
error: currentError,
|
|
125
|
+
owner: "framework",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (currentError instanceof TenantRequiredError) {
|
|
129
|
+
return {
|
|
130
|
+
ctx,
|
|
131
|
+
response: errorResponse(currentError.status, currentError.code, currentError.message),
|
|
132
|
+
error: currentError,
|
|
133
|
+
owner: "framework",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (currentError instanceof IdempotencyConflictError) {
|
|
137
|
+
return {
|
|
138
|
+
ctx,
|
|
139
|
+
response: errorResponse(httpErrors.IdempotencyConflict.status, httpErrors.IdempotencyConflict.code, currentError.message, {
|
|
140
|
+
namespace: currentError.namespace,
|
|
141
|
+
key: currentError.key,
|
|
142
|
+
}),
|
|
143
|
+
error: currentError,
|
|
144
|
+
owner: "framework",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (currentError instanceof IdempotencyInProgressError) {
|
|
148
|
+
return {
|
|
149
|
+
ctx,
|
|
150
|
+
response: errorResponse(httpErrors.IdempotencyInProgress.status, httpErrors.IdempotencyInProgress.code, currentError.message, {
|
|
151
|
+
namespace: currentError.namespace,
|
|
152
|
+
key: currentError.key,
|
|
153
|
+
}),
|
|
154
|
+
error: currentError,
|
|
155
|
+
owner: "framework",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (currentError instanceof GateAuthorizationError) {
|
|
159
|
+
return {
|
|
160
|
+
ctx,
|
|
161
|
+
response: errorResponse(currentError.status, currentError.code, currentError.message, currentError.details),
|
|
162
|
+
error: currentError,
|
|
163
|
+
owner: "framework",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (currentError instanceof EntitlementRequiredError) {
|
|
167
|
+
return {
|
|
168
|
+
ctx,
|
|
169
|
+
response: errorResponse(currentError.status, currentError.code, currentError.message, currentError.details),
|
|
170
|
+
error: currentError,
|
|
171
|
+
owner: "framework",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
for (const hook of hooks) {
|
|
175
|
+
if (!hook.mapUnhandledError)
|
|
176
|
+
continue;
|
|
177
|
+
try {
|
|
178
|
+
const handled = await hook.mapUnhandledError({
|
|
179
|
+
err: currentError,
|
|
180
|
+
req,
|
|
181
|
+
ctx,
|
|
182
|
+
contract,
|
|
183
|
+
path,
|
|
184
|
+
query,
|
|
185
|
+
headers,
|
|
186
|
+
body,
|
|
187
|
+
});
|
|
188
|
+
if (handled) {
|
|
189
|
+
const response = normalizeHttpResponse(handled);
|
|
190
|
+
return {
|
|
191
|
+
ctx,
|
|
192
|
+
response,
|
|
193
|
+
error: currentError,
|
|
194
|
+
owner: responseOwnerFor(response, "framework"),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (hookError) {
|
|
199
|
+
currentError = hookError;
|
|
200
|
+
await notifyCaughtError(currentError);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (options.mapUnhandledError) {
|
|
204
|
+
try {
|
|
205
|
+
const handled = await options.mapUnhandledError({
|
|
206
|
+
err: currentError,
|
|
207
|
+
req,
|
|
208
|
+
ctx,
|
|
209
|
+
contract,
|
|
210
|
+
path,
|
|
211
|
+
query,
|
|
212
|
+
headers,
|
|
213
|
+
body,
|
|
214
|
+
});
|
|
215
|
+
if (handled) {
|
|
216
|
+
const response = normalizeHttpResponse(handled);
|
|
217
|
+
return {
|
|
218
|
+
ctx,
|
|
219
|
+
response,
|
|
220
|
+
error: currentError,
|
|
221
|
+
owner: responseOwnerFor(response, "framework"),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch (hookError) {
|
|
226
|
+
currentError = hookError;
|
|
227
|
+
await notifyCaughtError(currentError);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
ctx,
|
|
232
|
+
response: defaultErrorResponse(currentError, ctx),
|
|
233
|
+
error: currentError,
|
|
234
|
+
owner: "framework",
|
|
235
|
+
};
|
|
236
|
+
};
|
|
237
|
+
try {
|
|
238
|
+
const url = new URL(req.url);
|
|
239
|
+
let matchedParams;
|
|
240
|
+
if (preMatchedParams) {
|
|
241
|
+
matchedParams = preMatchedParams;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const compiled = target.compiled;
|
|
245
|
+
const match = compiled ? compiled.pattern.exec(url.pathname) : null;
|
|
246
|
+
const contractMethod = contract.method.toUpperCase();
|
|
247
|
+
const requestMethod = req.method.toUpperCase();
|
|
248
|
+
const methodMatches = contractMethod === requestMethod ||
|
|
249
|
+
(contractMethod === "GET" && requestMethod === "HEAD");
|
|
250
|
+
if (!compiled || !match || !methodMatches) {
|
|
251
|
+
return errorResponse(404, "NOT_FOUND", "Not found");
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
matchedParams = decodeMatchedParams(compiled.keys, match);
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (error instanceof PathDecodeError) {
|
|
258
|
+
return errorResponse(400, "INVALID_PATH", "Malformed URL path");
|
|
259
|
+
}
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const rawHeaders = requestHeadersToRecord(req.headers);
|
|
264
|
+
const runNativeBeforeSend = async (initialResult, nativeResponse) => {
|
|
265
|
+
const originalView = responseForHooks(nativeResponse);
|
|
266
|
+
const originalHeaders = originalView.headers ?? {};
|
|
267
|
+
let transformed = originalView;
|
|
268
|
+
for (const hook of hooks) {
|
|
269
|
+
if (!hook.beforeSend)
|
|
270
|
+
continue;
|
|
271
|
+
const nextResponse = await hook.beforeSend({
|
|
272
|
+
req,
|
|
273
|
+
ctx: initialResult.ctx,
|
|
274
|
+
contract,
|
|
275
|
+
path: pathValue,
|
|
276
|
+
query: queryValue,
|
|
277
|
+
headers: headersValue,
|
|
278
|
+
body: bodyValue,
|
|
279
|
+
response: transformed,
|
|
280
|
+
error: initialResult.error,
|
|
281
|
+
native: true,
|
|
282
|
+
});
|
|
283
|
+
if (nextResponse) {
|
|
284
|
+
if ((nextResponse.status !== nativeResponse.status ||
|
|
285
|
+
nextResponse.body !== undefined) &&
|
|
286
|
+
!warnedNativeReplacementHooks.has(hook) &&
|
|
287
|
+
process.env.NODE_ENV !== "production") {
|
|
288
|
+
warnedNativeReplacementHooks.add(hook);
|
|
289
|
+
console.warn(`[beignet] beforeSend hook "${hook.name ?? "(anonymous)"}" returned a replacement status or body for a native Response on ${contract.method} ${contract.path}. Native responses are headers-only in beforeSend; status and body changes are ignored.`);
|
|
290
|
+
}
|
|
291
|
+
transformed = {
|
|
292
|
+
status: nativeResponse.status,
|
|
293
|
+
headers: nextResponse.headers,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return mergeNativeResponseHeaders(nativeResponse, originalHeaders, transformed.headers ?? {});
|
|
298
|
+
};
|
|
299
|
+
const applyTransformHooks = async (initialResult, allowRetry) => {
|
|
300
|
+
try {
|
|
301
|
+
if (isWebResponse(initialResult.response)) {
|
|
302
|
+
return {
|
|
303
|
+
...initialResult,
|
|
304
|
+
response: await runNativeBeforeSend(initialResult, initialResult.response),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
let transformed = normalizeResponse(initialResult.response);
|
|
308
|
+
for (const hook of hooks) {
|
|
309
|
+
if (!hook.beforeSend)
|
|
310
|
+
continue;
|
|
311
|
+
const nextResponse = await hook.beforeSend({
|
|
312
|
+
req,
|
|
313
|
+
ctx: initialResult.ctx,
|
|
314
|
+
contract,
|
|
315
|
+
path: pathValue,
|
|
316
|
+
query: queryValue,
|
|
317
|
+
headers: headersValue,
|
|
318
|
+
body: bodyValue,
|
|
319
|
+
response: transformed,
|
|
320
|
+
error: initialResult.error,
|
|
321
|
+
});
|
|
322
|
+
if (nextResponse) {
|
|
323
|
+
transformed = normalizeResponse(nextResponse);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
...initialResult,
|
|
328
|
+
response: transformed,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
const mapped = await resolveErrorResult(error, initialResult.ctx, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" });
|
|
333
|
+
if (!allowRetry) {
|
|
334
|
+
return mapped;
|
|
335
|
+
}
|
|
336
|
+
return applyTransformHooks(mapped, false);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
const applyResponseFinalizerHooks = async (initialResult, allowRetry, responseValidation) => {
|
|
340
|
+
const response = normalizeHttpResponse(initialResult.response);
|
|
341
|
+
const native = isWebResponse(response);
|
|
342
|
+
const owner = responseOwnerFor(response, initialResult.owner);
|
|
343
|
+
try {
|
|
344
|
+
for (const hook of hooks) {
|
|
345
|
+
const finalizer = getResponseFinalizerHook(hook);
|
|
346
|
+
if (!finalizer)
|
|
347
|
+
continue;
|
|
348
|
+
await finalizer({
|
|
349
|
+
req,
|
|
350
|
+
ctx: initialResult.ctx,
|
|
351
|
+
contract,
|
|
352
|
+
path: pathValue,
|
|
353
|
+
query: queryValue,
|
|
354
|
+
headers: headersValue,
|
|
355
|
+
body: bodyValue,
|
|
356
|
+
response: responseForHooks(response),
|
|
357
|
+
error: initialResult.error,
|
|
358
|
+
native: native ? true : undefined,
|
|
359
|
+
owner,
|
|
360
|
+
responseValidation,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
...initialResult,
|
|
365
|
+
response,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
const mapped = await resolveErrorResult(error, initialResult.ctx, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" });
|
|
370
|
+
if (!allowRetry) {
|
|
371
|
+
return mapped;
|
|
372
|
+
}
|
|
373
|
+
const transformed = await applyTransformHooks(mapped, true);
|
|
374
|
+
return applyResponseFinalizerHooks(transformed, false, "not-applicable");
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
let result;
|
|
378
|
+
const onRequestStartedAt = performance.now();
|
|
379
|
+
for (const hook of hooks) {
|
|
380
|
+
if (!hook.onRequest)
|
|
381
|
+
continue;
|
|
382
|
+
try {
|
|
383
|
+
const hookResult = await hook.onRequest({
|
|
384
|
+
req,
|
|
385
|
+
ports: finalPorts,
|
|
386
|
+
contract,
|
|
387
|
+
params: matchedParams,
|
|
388
|
+
});
|
|
389
|
+
if (hookResult) {
|
|
390
|
+
const response = normalizeHttpResponse(hookResult);
|
|
391
|
+
result = {
|
|
392
|
+
response,
|
|
393
|
+
owner: responseOwnerFor(response, "framework"),
|
|
394
|
+
};
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
result = await resolveErrorResult(error, undefined, undefined, undefined, undefined, undefined, { owner: "framework" });
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
stages.onRequestMs = performance.now() - onRequestStartedAt;
|
|
404
|
+
if (!result) {
|
|
405
|
+
if (optionsOverrides?.skipRoutePreparation) {
|
|
406
|
+
let createdCtx;
|
|
407
|
+
try {
|
|
408
|
+
createdCtx = await timeStage("contextMs", () => contextRuntime.createRequestContext(req, contract));
|
|
409
|
+
baseCtx = createdCtx;
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
result = await resolveErrorResult(error, undefined, undefined, undefined, undefined, undefined, { owner: "framework" });
|
|
413
|
+
}
|
|
414
|
+
if (!result) {
|
|
415
|
+
try {
|
|
416
|
+
result = {
|
|
417
|
+
ctx: createdCtx,
|
|
418
|
+
response: normalizeHttpResponse(await timeStage("handlerMs", () => userHandler({
|
|
419
|
+
req,
|
|
420
|
+
ctx: createdCtx,
|
|
421
|
+
contract,
|
|
422
|
+
path: {},
|
|
423
|
+
query: {},
|
|
424
|
+
headers: rawHeaders,
|
|
425
|
+
body: undefined,
|
|
426
|
+
}))),
|
|
427
|
+
owner: "framework",
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
result = await resolveErrorResult(error, createdCtx, undefined, undefined, undefined, undefined, { owner: "framework" });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
const parseStartedAt = performance.now();
|
|
437
|
+
let path = undefined;
|
|
438
|
+
let query = undefined;
|
|
439
|
+
let headers = undefined;
|
|
440
|
+
let body = undefined;
|
|
441
|
+
const prepared = await prepareRequestInputs({
|
|
442
|
+
contract,
|
|
443
|
+
req,
|
|
444
|
+
url,
|
|
445
|
+
rawHeaders,
|
|
446
|
+
matchedParams,
|
|
447
|
+
maxRequestBodyBytes,
|
|
448
|
+
rawRoute: optionsOverrides?.rawRoute,
|
|
449
|
+
});
|
|
450
|
+
if (!prepared.ok) {
|
|
451
|
+
result = {
|
|
452
|
+
response: prepared.response,
|
|
453
|
+
owner: "framework",
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
path = prepared.inputs.path;
|
|
458
|
+
query = prepared.inputs.query;
|
|
459
|
+
headers = prepared.inputs.headers;
|
|
460
|
+
body = prepared.inputs.body;
|
|
461
|
+
}
|
|
462
|
+
stages.parseMs = performance.now() - parseStartedAt;
|
|
463
|
+
if (!result) {
|
|
464
|
+
pathValue = path;
|
|
465
|
+
queryValue = query;
|
|
466
|
+
headersValue = headers;
|
|
467
|
+
bodyValue = body;
|
|
468
|
+
let createdCtx;
|
|
469
|
+
try {
|
|
470
|
+
createdCtx = await timeStage("contextMs", () => contextRuntime.createRequestContext(req, contract));
|
|
471
|
+
baseCtx = createdCtx;
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
result = await resolveErrorResult(error, undefined, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" });
|
|
475
|
+
}
|
|
476
|
+
if (!result) {
|
|
477
|
+
const baseArgs = {
|
|
478
|
+
req,
|
|
479
|
+
ctx: createdCtx,
|
|
480
|
+
contract,
|
|
481
|
+
path: path,
|
|
482
|
+
query: query,
|
|
483
|
+
headers: headers,
|
|
484
|
+
body: body,
|
|
485
|
+
};
|
|
486
|
+
let currentCtx = createdCtx;
|
|
487
|
+
const beforeHandleStartedAt = performance.now();
|
|
488
|
+
for (const hook of routeHooks) {
|
|
489
|
+
try {
|
|
490
|
+
const additions = await hook.resolve({
|
|
491
|
+
req,
|
|
492
|
+
ctx: currentCtx,
|
|
493
|
+
contract,
|
|
494
|
+
path,
|
|
495
|
+
query,
|
|
496
|
+
headers,
|
|
497
|
+
body,
|
|
498
|
+
});
|
|
499
|
+
if (additions && typeof additions === "object") {
|
|
500
|
+
currentCtx = contextRuntime.finalizeContext({
|
|
501
|
+
...currentCtx,
|
|
502
|
+
...additions,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
result = await resolveErrorResult(error, currentCtx, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" });
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (!result) {
|
|
512
|
+
for (const hook of hooks) {
|
|
513
|
+
if (!hook.beforeHandle)
|
|
514
|
+
continue;
|
|
515
|
+
try {
|
|
516
|
+
const hookResult = await hook.beforeHandle({
|
|
517
|
+
req,
|
|
518
|
+
ctx: currentCtx,
|
|
519
|
+
contract,
|
|
520
|
+
path,
|
|
521
|
+
query,
|
|
522
|
+
headers,
|
|
523
|
+
body,
|
|
524
|
+
});
|
|
525
|
+
if (isWebResponse(hookResult)) {
|
|
526
|
+
result = {
|
|
527
|
+
ctx: currentCtx,
|
|
528
|
+
response: hookResult,
|
|
529
|
+
owner: "transport",
|
|
530
|
+
};
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
if (isHttpResponseLike(hookResult)) {
|
|
534
|
+
result = {
|
|
535
|
+
ctx: currentCtx,
|
|
536
|
+
response: normalizeResponse(hookResult),
|
|
537
|
+
owner: "framework",
|
|
538
|
+
};
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
if (hookResult?.ctx !== undefined) {
|
|
542
|
+
currentCtx = contextRuntime.finalizeContext(hookResult.ctx);
|
|
543
|
+
}
|
|
544
|
+
if (hookResult?.response) {
|
|
545
|
+
const response = normalizeHttpResponse(hookResult.response);
|
|
546
|
+
result = {
|
|
547
|
+
ctx: currentCtx,
|
|
548
|
+
response,
|
|
549
|
+
owner: responseOwnerFor(response, "framework"),
|
|
550
|
+
};
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
result = await resolveErrorResult(error, currentCtx, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" });
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
stages.beforeHandleMs = performance.now() - beforeHandleStartedAt;
|
|
561
|
+
if (!result) {
|
|
562
|
+
// Hooks may have elevated the actor or resolved a tenant.
|
|
563
|
+
// Refresh the ambient request context so record-time
|
|
564
|
+
// consumers such as createAmbientAuditLog see the finalized
|
|
565
|
+
// identity.
|
|
566
|
+
setActiveRequestIdentity({
|
|
567
|
+
actor: readContextActor(currentCtx),
|
|
568
|
+
tenant: readContextTenant(currentCtx),
|
|
569
|
+
});
|
|
570
|
+
try {
|
|
571
|
+
result = {
|
|
572
|
+
ctx: currentCtx,
|
|
573
|
+
response: normalizeHttpResponse(await timeStage("handlerMs", () => userHandler({ ...baseArgs, ctx: currentCtx }))),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
result = await resolveErrorResult(error, currentCtx, pathValue, queryValue, headersValue, bodyValue);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const sendStartedAt = performance.now();
|
|
585
|
+
result = await applyTransformHooks(result, true);
|
|
586
|
+
let finalResponse = normalizeHttpResponse(result.response);
|
|
587
|
+
let finalError = result.error;
|
|
588
|
+
let finalOwner = responseOwnerFor(finalResponse, result.owner);
|
|
589
|
+
let responseValidation = "not-applicable";
|
|
590
|
+
if (finalOwner === "route" &&
|
|
591
|
+
!isWebResponse(finalResponse) &&
|
|
592
|
+
!(options.validateResponses ?? true)) {
|
|
593
|
+
responseValidation = "disabled";
|
|
594
|
+
}
|
|
595
|
+
else if (finalOwner === "route" &&
|
|
596
|
+
!isWebResponse(finalResponse) &&
|
|
597
|
+
(options.validateResponses ?? true)) {
|
|
598
|
+
try {
|
|
599
|
+
finalResponse = await finalizeResponse(contract, finalResponse, target.responseValidationExemptStatus);
|
|
600
|
+
result = {
|
|
601
|
+
...result,
|
|
602
|
+
response: finalResponse,
|
|
603
|
+
};
|
|
604
|
+
responseValidation = "validated";
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
if (error instanceof ResponseContractViolationError) {
|
|
608
|
+
result = {
|
|
609
|
+
ctx: result.ctx,
|
|
610
|
+
response: toContractViolationResponse(error),
|
|
611
|
+
error,
|
|
612
|
+
owner: "framework",
|
|
613
|
+
};
|
|
614
|
+
finalResponse = normalizeHttpResponse(result.response);
|
|
615
|
+
finalError = result.error;
|
|
616
|
+
finalOwner = responseOwnerFor(finalResponse, result.owner);
|
|
617
|
+
result = await applyTransformHooks(result, true);
|
|
618
|
+
finalResponse = normalizeHttpResponse(result.response);
|
|
619
|
+
finalError = result.error;
|
|
620
|
+
finalOwner = responseOwnerFor(finalResponse, result.owner);
|
|
621
|
+
responseValidation = "not-applicable";
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
throw error;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
result = await applyResponseFinalizerHooks(result, true, responseValidation);
|
|
629
|
+
finalResponse = normalizeHttpResponse(result.response);
|
|
630
|
+
finalError = result.error;
|
|
631
|
+
finalOwner = responseOwnerFor(finalResponse, result.owner);
|
|
632
|
+
if (!isWebResponse(finalResponse)) {
|
|
633
|
+
finalResponse = withFrameworkErrorOwnerHeader(finalResponse, finalOwner);
|
|
634
|
+
}
|
|
635
|
+
finalResponse = withoutHeadResponseBody(finalResponse, req.method);
|
|
636
|
+
stages.sendMs = performance.now() - sendStartedAt;
|
|
637
|
+
const durationMs = Date.now() - startedAt;
|
|
638
|
+
const stageTimings = roundStageTimings(stages);
|
|
639
|
+
for (const hook of hooks) {
|
|
640
|
+
if (!hook.afterSend)
|
|
641
|
+
continue;
|
|
642
|
+
try {
|
|
643
|
+
await hook.afterSend({
|
|
644
|
+
req,
|
|
645
|
+
ctx: result.ctx,
|
|
646
|
+
contract,
|
|
647
|
+
path: pathValue,
|
|
648
|
+
query: queryValue,
|
|
649
|
+
headers: headersValue,
|
|
650
|
+
body: bodyValue,
|
|
651
|
+
response: responseForHooks(finalResponse),
|
|
652
|
+
error: finalError,
|
|
653
|
+
durationMs,
|
|
654
|
+
stages: stageTimings,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
// Ignore after-response hook failures; they should never change the response.
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return finalResponse;
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
const result = await resolveErrorResult(error, baseCtx, pathValue, queryValue, headersValue, bodyValue, {
|
|
665
|
+
owner: "framework",
|
|
666
|
+
});
|
|
667
|
+
const response = withoutHeadResponseBody(normalizeHttpResponse(result.response), req.method);
|
|
668
|
+
if (isWebResponse(response)) {
|
|
669
|
+
return response;
|
|
670
|
+
}
|
|
671
|
+
return withFrameworkErrorOwnerHeader(response, responseOwnerFor(response, result.owner));
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
return async (target, req, preMatchedParams) => {
|
|
675
|
+
const tracing = resolveTracingPort(finalPorts);
|
|
676
|
+
const instrumentationOptions = options.instrumentation === false ? undefined : options.instrumentation;
|
|
677
|
+
const pathname = (() => {
|
|
678
|
+
try {
|
|
679
|
+
return new URL(req.url).pathname;
|
|
680
|
+
}
|
|
681
|
+
catch {
|
|
682
|
+
return req.url;
|
|
683
|
+
}
|
|
684
|
+
})();
|
|
685
|
+
const ignored = (instrumentationOptions?.ignorePaths ?? ["/api/devtools"]).some((prefix) => {
|
|
686
|
+
const normalized = prefix.replace(/\/+$/, "");
|
|
687
|
+
return pathname === normalized || pathname.startsWith(`${normalized}/`);
|
|
688
|
+
});
|
|
689
|
+
if (!tracing || ignored) {
|
|
690
|
+
return executeRequest(target, req, preMatchedParams);
|
|
691
|
+
}
|
|
692
|
+
const traceContextHeader = instrumentationOptions?.traceContextHeader ?? "traceparent";
|
|
693
|
+
const active = tracing.current();
|
|
694
|
+
const parsedTraceparent = traceContextHeader === false
|
|
695
|
+
? undefined
|
|
696
|
+
: parseTraceparent(req.headers.get(traceContextHeader));
|
|
697
|
+
const parent = active
|
|
698
|
+
? undefined
|
|
699
|
+
: !parsedTraceparent
|
|
700
|
+
? undefined
|
|
701
|
+
: {
|
|
702
|
+
traceparent: parsedTraceparent.traceparent,
|
|
703
|
+
tracestate: req.headers.get("tracestate") ?? undefined,
|
|
704
|
+
};
|
|
705
|
+
const traceAttributes = {
|
|
706
|
+
"beignet.contract.name": target.contract.name,
|
|
707
|
+
"http.request.method": req.method.toUpperCase(),
|
|
708
|
+
"http.route": target.contract.path,
|
|
709
|
+
};
|
|
710
|
+
return await runWithTracing(tracing, {
|
|
711
|
+
name: `beignet.request ${target.contract.name}`,
|
|
712
|
+
type: "request",
|
|
713
|
+
kind: active ? "internal" : "server",
|
|
714
|
+
parent,
|
|
715
|
+
attributes: traceAttributes,
|
|
716
|
+
metricAttributes: traceAttributes,
|
|
717
|
+
}, async (span) => {
|
|
718
|
+
const response = await executeRequest(target, req, preMatchedParams);
|
|
719
|
+
span?.setAttribute("http.response.status_code", response.status);
|
|
720
|
+
if (response.status >= 500)
|
|
721
|
+
span?.setStatus("error");
|
|
722
|
+
return response;
|
|
723
|
+
});
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Build a memo instrumentation recorder from the app ports, or undefined when
|
|
728
|
+
* no instrumentation sink is wired.
|
|
729
|
+
*
|
|
730
|
+
* Resolved per execution rather than at route-build time because providers
|
|
731
|
+
* contribute `ports.instrumentation`/`ports.devtools` during setup, after
|
|
732
|
+
* routes are registered.
|
|
733
|
+
*/
|
|
734
|
+
export function createMemoScopeRecorder(ports) {
|
|
735
|
+
const target = ports;
|
|
736
|
+
if (!resolveProviderInstrumentationPort(target))
|
|
737
|
+
return undefined;
|
|
738
|
+
const instrumentation = createProviderInstrumentation(target, {
|
|
739
|
+
providerName: "memo",
|
|
740
|
+
watcher: "memo",
|
|
741
|
+
});
|
|
742
|
+
return (event) => {
|
|
743
|
+
instrumentation.custom({
|
|
744
|
+
name: `memo.${event.kind}`,
|
|
745
|
+
label: event.kind === "hit" ? "Memo hit" : "Memo fill",
|
|
746
|
+
summary: `Memo ${event.kind} for ${event.memo}`,
|
|
747
|
+
details: {
|
|
748
|
+
memo: event.memo,
|
|
749
|
+
key: event.key,
|
|
750
|
+
...(event.durationMs !== undefined
|
|
751
|
+
? { durationMs: event.durationMs }
|
|
752
|
+
: {}),
|
|
753
|
+
...(event.failed ? { failed: true } : {}),
|
|
754
|
+
},
|
|
755
|
+
});
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
export function buildHandler(options, finalPorts, contextRuntime, contract, userHandler, hooks, routeHooks = [], optionsOverrides, responseValidationExemptStatus) {
|
|
759
|
+
const execute = createRequestExecutor(options, finalPorts, contextRuntime, hooks, routeHooks, optionsOverrides);
|
|
760
|
+
const executionTarget = {
|
|
761
|
+
contract,
|
|
762
|
+
compiled: compilePath(contract.path),
|
|
763
|
+
handler: userHandler,
|
|
764
|
+
responseValidationExemptStatus,
|
|
765
|
+
};
|
|
766
|
+
// Every HTTP execution runs inside a fresh memo scope so createMemo(...)
|
|
767
|
+
// wrappers dedupe lookups for exactly one request.
|
|
768
|
+
return (req, preMatchedParams) => runWithMemoScope({ record: createMemoScopeRecorder(finalPorts) }, () => execute(executionTarget, req, preMatchedParams));
|
|
769
|
+
}
|
|
770
|
+
function roundStageTimings(stages) {
|
|
771
|
+
const round = (value) => Math.round(value * 100) / 100;
|
|
772
|
+
return {
|
|
773
|
+
onRequestMs: round(stages.onRequestMs),
|
|
774
|
+
parseMs: round(stages.parseMs),
|
|
775
|
+
contextMs: round(stages.contextMs),
|
|
776
|
+
beforeHandleMs: round(stages.beforeHandleMs),
|
|
777
|
+
handlerMs: round(stages.handlerMs),
|
|
778
|
+
sendMs: round(stages.sendMs),
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
//# sourceMappingURL=request-executor.js.map
|