@loutrejs/loutre 0.5.0 → 0.6.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 +63 -32
- package/dist/adapters/aws-lambda.d.ts +5 -3
- package/dist/adapters/aws-lambda.js +53 -23
- package/dist/adapters/bun.d.ts +7 -4
- package/dist/adapters/bun.js +52 -42
- package/dist/adapters/cloudflare-workers.d.ts +5 -3
- package/dist/adapters/cloudflare-workers.js +23 -28
- package/dist/adapters/deno.d.ts +7 -4
- package/dist/adapters/deno.js +94 -57
- package/dist/adapters/electron.d.ts +13 -5
- package/dist/adapters/electron.js +76 -28
- package/dist/application/definition.d.ts +31 -0
- package/dist/application/definition.js +16 -0
- package/dist/application/index.d.ts +4 -63
- package/dist/application/index.js +2 -11
- package/dist/application/kernel-internal.d.ts +2 -0
- package/dist/application/kernel-internal.js +10 -0
- package/dist/application/kernel.d.ts +21 -0
- package/dist/application/kernel.js +65 -0
- package/dist/core/diagnostic.d.ts +9 -0
- package/dist/core/diagnostic.js +9 -0
- package/dist/core/extension.d.ts +107 -0
- package/dist/core/extension.js +73 -0
- package/dist/core/generic-layer.d.ts +26 -0
- package/dist/core/generic-layer.js +116 -0
- package/dist/core/index.d.ts +31 -15
- package/dist/core/index.js +15 -15
- package/dist/core/injection.d.ts +4 -13
- package/dist/core/injection.js +63 -0
- package/dist/core/model/context.d.ts +18 -0
- package/dist/core/model/context.js +34 -0
- package/dist/core/model/lifecycle.d.ts +5 -0
- package/dist/core/model/lifecycle.js +44 -0
- package/dist/core/model/modules.d.ts +2 -0
- package/dist/core/model/modules.js +101 -0
- package/dist/core/model/providers.d.ts +4 -0
- package/dist/core/model/providers.js +74 -0
- package/dist/core/model/snapshot.d.ts +6 -0
- package/dist/core/model/snapshot.js +31 -0
- package/dist/core/model/types.d.ts +78 -0
- package/dist/core/model/validation.d.ts +4 -0
- package/dist/core/model/validation.js +40 -0
- package/dist/core/model/visibility.d.ts +4 -0
- package/dist/core/model/visibility.js +20 -0
- package/dist/core/model.d.ts +10 -0
- package/dist/core/model.js +346 -0
- package/dist/core/module.d.ts +8 -13
- package/dist/core/source-location.d.ts +8 -0
- package/dist/core/source-location.js +31 -0
- package/dist/core/token-internal.d.ts +3 -0
- package/dist/core/token-internal.js +4 -0
- package/dist/core/type.d.ts +7 -0
- package/dist/core/type.js +4 -0
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/graph/index.d.ts +2 -3
- package/dist/graph/index.js +1 -3
- package/dist/graph/model.d.ts +34 -0
- package/dist/graph/model.js +127 -0
- package/dist/http/auth.d.ts +46 -0
- package/dist/http/auth.js +96 -0
- package/dist/http/client.d.ts +28 -31
- package/dist/http/client.js +58 -44
- package/dist/http/cors-internal.d.ts +29 -0
- package/dist/http/cors-internal.js +160 -0
- package/dist/http/cors.d.ts +5 -8
- package/dist/http/cors.js +7 -165
- package/dist/http/extension.d.ts +348 -0
- package/dist/http/extension.js +947 -0
- package/dist/http/index.d.ts +9 -7
- package/dist/http/index.js +5 -7
- package/dist/{application → http}/openapi.d.ts +12 -3
- package/dist/{application → http}/openapi.js +164 -102
- package/dist/http/path.d.ts +1 -42
- package/dist/http/path.js +1 -2
- package/dist/http/runtime/request.d.ts +17 -0
- package/dist/http/runtime/request.js +109 -0
- package/dist/http/runtime/response.d.ts +18 -0
- package/dist/http/runtime/response.js +108 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/message-port/extension.d.ts +66 -0
- package/dist/message-port/extension.js +395 -0
- package/dist/message-port/index.d.ts +4 -73
- package/dist/message-port/index.js +2 -189
- package/dist/message-port/transport.d.ts +11 -0
- package/dist/message-port/transport.js +45 -0
- package/dist/runtime/async-iterator-lifecycle.d.ts +12 -0
- package/dist/runtime/async-iterator-lifecycle.js +95 -0
- package/dist/runtime/capability.d.ts +11 -11
- package/dist/runtime/capability.js +3 -3
- package/dist/runtime/di.d.ts +2 -22
- package/dist/runtime/di.js +8 -285
- package/dist/runtime/execution-tracker.d.ts +8 -0
- package/dist/runtime/execution-tracker.js +54 -0
- package/dist/runtime/index.d.ts +15 -9
- package/dist/runtime/index.js +9 -9
- package/dist/runtime/ingress-gate.d.ts +7 -0
- package/dist/runtime/ingress-gate.js +33 -0
- package/dist/runtime/kernel.d.ts +23 -0
- package/dist/runtime/kernel.js +354 -0
- package/dist/runtime/logger.d.ts +2 -0
- package/dist/runtime/logger.js +2 -0
- package/dist/runtime/node-capabilities.d.ts +1 -1
- package/dist/runtime/node-capabilities.js +1 -1
- package/dist/runtime/server-port.d.ts +2 -0
- package/dist/runtime/server-port.js +11 -0
- package/dist/tasks/extension.d.ts +144 -0
- package/dist/tasks/extension.js +499 -0
- package/dist/tasks/index.d.ts +2 -0
- package/dist/tasks/index.js +1 -0
- package/dist/tasks/runtime/cron.d.ts +6 -0
- package/dist/tasks/runtime/cron.js +89 -0
- package/dist/websocket/errors.d.ts +9 -0
- package/dist/websocket/errors.js +18 -0
- package/dist/websocket/extension.d.ts +154 -0
- package/dist/websocket/extension.js +524 -0
- package/dist/websocket/index.d.ts +3 -0
- package/dist/websocket/index.js +2 -0
- package/package.json +12 -16
- package/dist/application/binding.d.ts +0 -33
- package/dist/application/binding.js +0 -295
- package/dist/application/host.d.ts +0 -8
- package/dist/application/host.js +0 -14
- package/dist/application/queue.d.ts +0 -3
- package/dist/application/queue.js +0 -4
- package/dist/application/scheduler.d.ts +0 -2
- package/dist/application/scheduler.js +0 -45
- package/dist/core/context-key.d.ts +0 -16
- package/dist/core/context-key.js +0 -17
- package/dist/core/contract-internal.d.ts +0 -22
- package/dist/core/contract-internal.js +0 -61
- package/dist/core/contract.d.ts +0 -96
- package/dist/core/contract.js +0 -88
- package/dist/core/execution.d.ts +0 -78
- package/dist/core/execution.js +0 -61
- package/dist/core/implementation.d.ts +0 -44
- package/dist/core/implementation.js +0 -35
- package/dist/core/layer.d.ts +0 -137
- package/dist/core/layer.js +0 -81
- package/dist/graph/compiler.d.ts +0 -4
- package/dist/graph/compiler.js +0 -228
- package/dist/graph/graph.d.ts +0 -18
- package/dist/graph/graph.js +0 -1118
- package/dist/graph/ir.d.ts +0 -175
- package/dist/http/application.d.ts +0 -18
- package/dist/http/application.js +0 -515
- package/dist/http/basic-auth.d.ts +0 -37
- package/dist/http/basic-auth.js +0 -67
- package/dist/http/definitions.d.ts +0 -363
- package/dist/http/definitions.js +0 -310
- package/dist/http/hosted-protocol.d.ts +0 -4
- package/dist/http/hosted-protocol.js +0 -4
- package/dist/http/internal.d.ts +0 -8
- package/dist/http/internal.js +0 -1
- package/dist/http/params.d.ts +0 -2
- package/dist/http/params.js +0 -19
- package/dist/http/request-body.d.ts +0 -4
- package/dist/http/request-body.js +0 -6
- package/dist/http/validate.d.ts +0 -20
- package/dist/http/validate.js +0 -6
- package/dist/message-port/environment.d.ts +0 -2
- package/dist/message-port/environment.js +0 -4
- package/dist/runtime/application.d.ts +0 -38
- package/dist/runtime/application.js +0 -339
- package/dist/runtime/pipeline.d.ts +0 -11
- package/dist/runtime/pipeline.js +0 -161
- /package/dist/{graph/ir.js → core/model/types.js} +0 -0
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
import { collectInjectedDependencies, getSourceLocation, inheritSourceLocation, composeLayers, defineExecution, defineExecutionExtension, defineLayer, runInInjectionContext, runtimeCapability, SchemaValidationError, validateSchema, } from '../core/index.js';
|
|
2
|
+
import { createCorsActualResponseHeaders, createCorsPreflightResponseHeaders, } from './cors-internal.js';
|
|
3
|
+
import { assertValidHttpMethod, compareHttpPathSpecificity, createHttpDispatchKey, HttpPathDecodeError, matchHttpPath, parseHttpPath, } from './path.js';
|
|
4
|
+
import { IngressGate } from '../runtime/ingress-gate.js';
|
|
5
|
+
import { AsyncIteratorCleanupDeadlineError, AsyncIteratorLifecycle, } from '../runtime/async-iterator-lifecycle.js';
|
|
6
|
+
import { decodeBody, decodeQuery, HttpInputDecodeError, HttpInputValidationError, HttpUnsupportedMediaTypeError, validatedContentType, validateRequestHeaders, } from './runtime/request.js';
|
|
7
|
+
import { applyFrameworkHeadersToResponse, applyFrameworkResponseHeaders, applyResponseHeaders, isResponseHeadersWithDefaults, isStandardSchema, mergeResponseHeaders, responseHeadersDefaults, responseHeadersSchema, validateResponseHeaders, } from './runtime/response.js';
|
|
8
|
+
export const HTTP_SERVER = runtimeCapability('http.server');
|
|
9
|
+
export const httpExecutionExtension = defineExecutionExtension({
|
|
10
|
+
kind: 'execution-extension',
|
|
11
|
+
abiVersion: '1',
|
|
12
|
+
name: 'loutre:http',
|
|
13
|
+
compile(definition, context) {
|
|
14
|
+
const contractSource = getSourceLocation(definition.contract);
|
|
15
|
+
const routes = Object.entries(definition.contract.routes).map(([name, route]) => compileHttpRoute(name, route, contractSource));
|
|
16
|
+
const id = definition.name || `${context.moduleId}.http.${context.definitionIndex}`;
|
|
17
|
+
const dependencies = new Set(collectInjectedDependencies({
|
|
18
|
+
kind: 'execution',
|
|
19
|
+
id: `http:${id}`,
|
|
20
|
+
name: id,
|
|
21
|
+
}, () => definition.factory()));
|
|
22
|
+
routes.forEach((route) => {
|
|
23
|
+
route.middlewares.forEach((middleware, index) => {
|
|
24
|
+
for (const dependency of collectInjectedDependencies({
|
|
25
|
+
kind: 'layer',
|
|
26
|
+
id: `http:${id}:${route.name}:${index}`,
|
|
27
|
+
name: middleware.name,
|
|
28
|
+
}, () => middleware.factory())) {
|
|
29
|
+
dependencies.add(dependency);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
kind: 'execution',
|
|
35
|
+
id,
|
|
36
|
+
executionKind: 'http.request',
|
|
37
|
+
dependencies: [...dependencies],
|
|
38
|
+
capabilities: [
|
|
39
|
+
...new Set([
|
|
40
|
+
HTTP_SERVER,
|
|
41
|
+
...routes.flatMap((route) => route.middlewares.flatMap((middleware) => middleware.capabilities)),
|
|
42
|
+
]),
|
|
43
|
+
],
|
|
44
|
+
compiled: Object.freeze({
|
|
45
|
+
routes: Object.freeze(routes),
|
|
46
|
+
factory: definition.factory,
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
validate({ executions }) {
|
|
51
|
+
const dispatches = new Map();
|
|
52
|
+
return executions.flatMap((execution) => execution.compiled.routes.flatMap((route) => {
|
|
53
|
+
const owner = dispatches.get(route.dispatch);
|
|
54
|
+
if (owner) {
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
code: 'LUTRE_HTTP_DUPLICATE_ROUTE',
|
|
58
|
+
message: `${route.method} ${route.path} conflicts with ${owner}.`,
|
|
59
|
+
path: execution.id,
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
dispatches.set(route.dispatch, execution.id);
|
|
64
|
+
return [];
|
|
65
|
+
}));
|
|
66
|
+
},
|
|
67
|
+
createRuntime(context) {
|
|
68
|
+
context.capabilities.get(HTTP_SERVER);
|
|
69
|
+
return createHttpExtensionRuntime(context.executions, context.applicationRuntime);
|
|
70
|
+
},
|
|
71
|
+
projectGraph: ({ execution }) => ({
|
|
72
|
+
routes: execution.compiled.routes.map((route) => ({
|
|
73
|
+
name: route.name,
|
|
74
|
+
method: route.method,
|
|
75
|
+
path: route.path,
|
|
76
|
+
...(route.source === undefined ? {} : { source: route.source }),
|
|
77
|
+
middlewares: route.middlewares.map((middleware) => ({
|
|
78
|
+
name: middleware.name,
|
|
79
|
+
capabilities: middleware.capabilities.map((capability) => capability.id),
|
|
80
|
+
...(getSourceLocation(middleware) === undefined
|
|
81
|
+
? {}
|
|
82
|
+
: { source: getSourceLocation(middleware) }),
|
|
83
|
+
})),
|
|
84
|
+
responses: Object.fromEntries(Object.entries(route.definition.responses).map(([name, response]) => [
|
|
85
|
+
name,
|
|
86
|
+
{ status: response.status },
|
|
87
|
+
])),
|
|
88
|
+
})),
|
|
89
|
+
}),
|
|
90
|
+
host: {
|
|
91
|
+
namespace: 'http',
|
|
92
|
+
create: ({ runtime }) => ({ fetch: (request) => runtime.fetch(request) }),
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
export function httpError(definition, map) {
|
|
96
|
+
return Object.freeze({
|
|
97
|
+
kind: 'http-error-mapping',
|
|
98
|
+
definition,
|
|
99
|
+
map: map ?? ((error) => ({ body: error.data })),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
export function defineHttpContract(routes) {
|
|
103
|
+
const resolvedRoutes = resolveHttpContractRoutes(routes);
|
|
104
|
+
for (const [name, route] of Object.entries(resolvedRoutes)) {
|
|
105
|
+
compileHttpRoute(name, route);
|
|
106
|
+
}
|
|
107
|
+
return Object.freeze({
|
|
108
|
+
kind: 'http-contract',
|
|
109
|
+
routes: Object.freeze(resolvedRoutes),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function resolveHttpContractRoutes(tree) {
|
|
113
|
+
const resolved = {};
|
|
114
|
+
const visit = (current, pathPrefix, inheritedResponses, inheritedMiddlewares) => {
|
|
115
|
+
for (const [name, node] of Object.entries(current)) {
|
|
116
|
+
if ('method' in node) {
|
|
117
|
+
if ('routes' in node) {
|
|
118
|
+
throw new TypeError(`HTTP Contract node ${name} must be either a branch or a leaf route.`);
|
|
119
|
+
}
|
|
120
|
+
if (resolved[name]) {
|
|
121
|
+
throw new TypeError(`Duplicate nested HTTP route name: ${name}`);
|
|
122
|
+
}
|
|
123
|
+
assertNoInheritedResponseCollision(name, inheritedResponses, node.responses);
|
|
124
|
+
resolved[name] = inheritSourceLocation({
|
|
125
|
+
...node,
|
|
126
|
+
path: joinHttpPath(pathPrefix, node.path),
|
|
127
|
+
responses: { ...inheritedResponses, ...node.responses },
|
|
128
|
+
...(inheritedMiddlewares.length === 0 && !node.middlewares
|
|
129
|
+
? {}
|
|
130
|
+
: {
|
|
131
|
+
middlewares: [
|
|
132
|
+
...inheritedMiddlewares,
|
|
133
|
+
...(node.middlewares ?? []),
|
|
134
|
+
],
|
|
135
|
+
}),
|
|
136
|
+
}, node);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (!('routes' in node)) {
|
|
140
|
+
throw new TypeError(`HTTP Contract node ${name} must declare method or routes.`);
|
|
141
|
+
}
|
|
142
|
+
const branchPath = node.path ?? '';
|
|
143
|
+
if (branchPath !== '')
|
|
144
|
+
parseHttpPath(branchPath);
|
|
145
|
+
assertNoInheritedResponseCollision(name, inheritedResponses, node.responses ?? {});
|
|
146
|
+
visit(node.routes, joinHttpPath(pathPrefix, branchPath), { ...inheritedResponses, ...node.responses }, [...inheritedMiddlewares, ...(node.middlewares ?? [])]);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
visit(tree, '', {}, []);
|
|
150
|
+
return resolved;
|
|
151
|
+
}
|
|
152
|
+
function joinHttpPath(prefix, path) {
|
|
153
|
+
if (prefix === '')
|
|
154
|
+
return path;
|
|
155
|
+
if (prefix === '/')
|
|
156
|
+
return path;
|
|
157
|
+
if (path === '/')
|
|
158
|
+
return prefix;
|
|
159
|
+
return `${prefix}${path}`;
|
|
160
|
+
}
|
|
161
|
+
function assertNoInheritedResponseCollision(nodeName, inherited, declared) {
|
|
162
|
+
for (const name of Object.keys(declared)) {
|
|
163
|
+
if (name in inherited) {
|
|
164
|
+
throw new TypeError(`Duplicate inherited HTTP response ${name} at ${nodeName}.`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export function defineHttpImplementation(definition) {
|
|
169
|
+
return defineExecution(httpExecutionExtension, {
|
|
170
|
+
name: definition.name ?? '',
|
|
171
|
+
contract: definition.contract,
|
|
172
|
+
factory: definition.factory,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
export function bindHttpServer(driver) {
|
|
176
|
+
return { capability: HTTP_SERVER, value: driver };
|
|
177
|
+
}
|
|
178
|
+
export function defineHttpMiddleware(definition) {
|
|
179
|
+
return defineLayer(definition);
|
|
180
|
+
}
|
|
181
|
+
const bodyValidation = Object.freeze({
|
|
182
|
+
kind: 'http-validation',
|
|
183
|
+
part: 'body',
|
|
184
|
+
});
|
|
185
|
+
export const validate = Object.freeze({
|
|
186
|
+
body: bodyValidation,
|
|
187
|
+
});
|
|
188
|
+
export const executionHttp = Object.freeze({
|
|
189
|
+
contract: defineHttpContract,
|
|
190
|
+
implementation: defineHttpImplementation,
|
|
191
|
+
middleware: defineHttpMiddleware,
|
|
192
|
+
validate,
|
|
193
|
+
error: httpError,
|
|
194
|
+
extension: httpExecutionExtension,
|
|
195
|
+
serverCapability: HTTP_SERVER,
|
|
196
|
+
bindServer: bindHttpServer,
|
|
197
|
+
});
|
|
198
|
+
const httpFrameworkHeaders = Symbol('loutre.http.framework-headers');
|
|
199
|
+
export function withHttpFrameworkHeaders(result, headers) {
|
|
200
|
+
const current = result[httpFrameworkHeaders];
|
|
201
|
+
return {
|
|
202
|
+
...result,
|
|
203
|
+
[httpFrameworkHeaders]: { ...current, ...headers },
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function compileHttpRoute(name, route, routeSourceFallback) {
|
|
207
|
+
assertValidHttpMethod(route.method);
|
|
208
|
+
const segments = parseHttpPath(route.path);
|
|
209
|
+
assertValidHttpRouteDefinition(route, segments);
|
|
210
|
+
const definition = snapshotHttpRouteDefinition(route);
|
|
211
|
+
const routeSource = getSourceLocation(route) ?? routeSourceFallback;
|
|
212
|
+
return Object.freeze({
|
|
213
|
+
name,
|
|
214
|
+
method: route.method.toUpperCase(),
|
|
215
|
+
path: route.path,
|
|
216
|
+
segments,
|
|
217
|
+
dispatch: createHttpDispatchKey(route.method, segments),
|
|
218
|
+
definition,
|
|
219
|
+
middlewares: compileHttpMiddlewares(definition),
|
|
220
|
+
...(routeSource === undefined ? {} : { source: routeSource }),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function snapshotHttpRouteDefinition(route) {
|
|
224
|
+
const request = route.request
|
|
225
|
+
? Object.freeze({
|
|
226
|
+
...route.request,
|
|
227
|
+
...(route.request.params === undefined
|
|
228
|
+
? {}
|
|
229
|
+
: { params: Object.freeze({ ...route.request.params }) }),
|
|
230
|
+
})
|
|
231
|
+
: undefined;
|
|
232
|
+
const responses = Object.freeze(Object.fromEntries(Object.entries(route.responses).map(([name, response]) => [
|
|
233
|
+
name,
|
|
234
|
+
Object.freeze({
|
|
235
|
+
...response,
|
|
236
|
+
...(response.headers === undefined
|
|
237
|
+
? {}
|
|
238
|
+
: { headers: snapshotHttpResponseHeaders(response.headers) }),
|
|
239
|
+
...(response.error === undefined
|
|
240
|
+
? {}
|
|
241
|
+
: { error: Object.freeze({ ...response.error }) }),
|
|
242
|
+
}),
|
|
243
|
+
])));
|
|
244
|
+
return inheritSourceLocation(Object.freeze({
|
|
245
|
+
...route,
|
|
246
|
+
...(route.tags === undefined
|
|
247
|
+
? {}
|
|
248
|
+
: { tags: Object.freeze([...route.tags]) }),
|
|
249
|
+
...(request === undefined ? {} : { request }),
|
|
250
|
+
responses,
|
|
251
|
+
...(route.middlewares === undefined
|
|
252
|
+
? {}
|
|
253
|
+
: { middlewares: Object.freeze([...route.middlewares]) }),
|
|
254
|
+
}), route);
|
|
255
|
+
}
|
|
256
|
+
function snapshotHttpResponseHeaders(headers) {
|
|
257
|
+
if (isStandardSchema(headers))
|
|
258
|
+
return headers;
|
|
259
|
+
if (isResponseHeadersWithDefaults(headers)) {
|
|
260
|
+
return Object.freeze({
|
|
261
|
+
schema: headers.schema,
|
|
262
|
+
defaults: snapshotHttpHeaders(headers.defaults),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return snapshotHttpHeaders(headers);
|
|
266
|
+
}
|
|
267
|
+
function snapshotHttpHeaders(headers) {
|
|
268
|
+
return Object.freeze(Object.fromEntries(Object.entries(headers).map(([name, value]) => [
|
|
269
|
+
name,
|
|
270
|
+
Array.isArray(value) ? Object.freeze([...value]) : value,
|
|
271
|
+
])));
|
|
272
|
+
}
|
|
273
|
+
function compileHttpMiddlewares(route) {
|
|
274
|
+
const declared = route.middlewares ?? [];
|
|
275
|
+
const compiled = declared.map((middleware) => isHttpValidationMiddleware(middleware)
|
|
276
|
+
? compileHttpValidationMiddleware(middleware, route)
|
|
277
|
+
: middleware);
|
|
278
|
+
if (route.request?.body && !hasBodyValidation(declared)) {
|
|
279
|
+
compiled.push(createBodyValidationMiddleware(route.request.body));
|
|
280
|
+
}
|
|
281
|
+
return Object.freeze(compiled);
|
|
282
|
+
}
|
|
283
|
+
function compileHttpValidationMiddleware(middleware, route) {
|
|
284
|
+
switch (middleware.part) {
|
|
285
|
+
case 'body': {
|
|
286
|
+
const schema = route.request?.body;
|
|
287
|
+
if (!schema) {
|
|
288
|
+
throw new TypeError(`HTTP ${route.method} ${route.path} uses validate.body but declares no request body.`);
|
|
289
|
+
}
|
|
290
|
+
return createBodyValidationMiddleware(schema);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function createBodyValidationMiddleware(schema) {
|
|
295
|
+
return defineLayer({
|
|
296
|
+
name: 'validate.body',
|
|
297
|
+
factory: () => async (context, next) => {
|
|
298
|
+
const rawBody = await decodeBody(context.request, validatedContentType(context.input.headers));
|
|
299
|
+
try {
|
|
300
|
+
const body = await validateSchema(schema, rawBody);
|
|
301
|
+
context.input.body = body;
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
if (error instanceof SchemaValidationError) {
|
|
305
|
+
throw new HttpInputValidationError(error);
|
|
306
|
+
}
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
await next();
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function isHttpValidationMiddleware(middleware) {
|
|
314
|
+
return middleware.kind === 'http-validation';
|
|
315
|
+
}
|
|
316
|
+
function hasBodyValidation(middlewares) {
|
|
317
|
+
return middlewares.some((middleware) => isHttpValidationMiddleware(middleware) && middleware.part === 'body');
|
|
318
|
+
}
|
|
319
|
+
function countBodyValidations(middlewares) {
|
|
320
|
+
return middlewares.filter((middleware) => isHttpValidationMiddleware(middleware) && middleware.part === 'body').length;
|
|
321
|
+
}
|
|
322
|
+
function assertValidHttpRouteDefinition(route, segments) {
|
|
323
|
+
if (route.request?.body && !route.request.headers) {
|
|
324
|
+
throw new TypeError(`HTTP ${route.method} ${route.path} declares a body but no request headers schema.`);
|
|
325
|
+
}
|
|
326
|
+
const bodyValidationCount = countBodyValidations(route.middlewares ?? []);
|
|
327
|
+
if (!route.request?.body && bodyValidationCount > 0) {
|
|
328
|
+
throw new TypeError(`HTTP ${route.method} ${route.path} uses validate.body but declares no request body.`);
|
|
329
|
+
}
|
|
330
|
+
if (bodyValidationCount > 1) {
|
|
331
|
+
throw new TypeError(`HTTP ${route.method} ${route.path} declares validate.body more than once.`);
|
|
332
|
+
}
|
|
333
|
+
if (route.request?.params) {
|
|
334
|
+
const pathParams = segments
|
|
335
|
+
.filter((segment) => segment.kind === 'param')
|
|
336
|
+
.map((segment) => segment.name)
|
|
337
|
+
.toSorted();
|
|
338
|
+
const schemaParams = Object.keys(route.request.params).toSorted();
|
|
339
|
+
if (pathParams.length !== schemaParams.length ||
|
|
340
|
+
pathParams.some((name, index) => name !== schemaParams[index])) {
|
|
341
|
+
throw new TypeError(`HTTP ${route.method} ${route.path} request params must exactly match path parameters.`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for (const [name, response] of Object.entries(route.responses)) {
|
|
345
|
+
if (!Number.isInteger(response.status) ||
|
|
346
|
+
response.status < 200 ||
|
|
347
|
+
response.status > 599) {
|
|
348
|
+
throw new TypeError(`HTTP response ${name} has invalid status ${response.status}.`);
|
|
349
|
+
}
|
|
350
|
+
if ((response.status === 204 ||
|
|
351
|
+
response.status === 205 ||
|
|
352
|
+
response.status === 304) &&
|
|
353
|
+
(response.body || response.stream === 'server')) {
|
|
354
|
+
throw new TypeError(`HTTP response ${name} with status ${response.status} cannot declare a body or stream.`);
|
|
355
|
+
}
|
|
356
|
+
if (response.stream === 'server' && !response.body) {
|
|
357
|
+
throw new TypeError(`HTTP server-stream response ${name} must declare a body schema.`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function createHttpExtensionRuntime(executions, applicationRuntime) {
|
|
362
|
+
const ingress = new IngressGate();
|
|
363
|
+
const activeResponseStreams = new Set();
|
|
364
|
+
const handlers = new Map();
|
|
365
|
+
for (const execution of executions) {
|
|
366
|
+
handlers.set(execution.id, runInInjectionContext({
|
|
367
|
+
consumer: {
|
|
368
|
+
kind: 'execution',
|
|
369
|
+
id: `http:${execution.id}`,
|
|
370
|
+
name: execution.id,
|
|
371
|
+
},
|
|
372
|
+
resolve: (token) => applicationRuntime.resolve(token, execution.id),
|
|
373
|
+
}, () => execution.compiled.factory()));
|
|
374
|
+
}
|
|
375
|
+
const routes = executions
|
|
376
|
+
.flatMap((execution) => execution.compiled.routes.map((route) => ({
|
|
377
|
+
executionId: execution.id,
|
|
378
|
+
route,
|
|
379
|
+
})))
|
|
380
|
+
.toSorted((left, right) => compareHttpPathSpecificity(left.route.segments, right.route.segments));
|
|
381
|
+
return {
|
|
382
|
+
async drain({ timeoutMs }) {
|
|
383
|
+
ingress.stopAccepting();
|
|
384
|
+
const deadline = Date.now() + timeoutMs;
|
|
385
|
+
await ingress.waitForIdle();
|
|
386
|
+
const reason = new Error('LUTRE_HTTP_SERVER_STREAM_DRAIN: Application is shutting down.');
|
|
387
|
+
const results = await Promise.allSettled([...activeResponseStreams].map((stream) => stream.abort(reason, deadline)));
|
|
388
|
+
const errors = results.flatMap((result) => result.status === 'rejected' ? [result.reason] : []);
|
|
389
|
+
if (errors.length > 0) {
|
|
390
|
+
throw new AggregateError(errors, 'HTTP response stream drain failed.');
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
async fetch(request) {
|
|
394
|
+
if (!ingress.isAccepting) {
|
|
395
|
+
return Response.json({ error: 'Service Unavailable' }, { status: 503 });
|
|
396
|
+
}
|
|
397
|
+
const url = new URL(request.url);
|
|
398
|
+
const method = request.method.toUpperCase();
|
|
399
|
+
if (isCorsPreflightRequest(request)) {
|
|
400
|
+
try {
|
|
401
|
+
const requestedMethod = request.headers
|
|
402
|
+
.get('access-control-request-method')
|
|
403
|
+
.toUpperCase();
|
|
404
|
+
const match = findRuntimeHttpRoute(routes, requestedMethod, url.pathname);
|
|
405
|
+
if (match) {
|
|
406
|
+
const completePendingIngress = ingress.enter();
|
|
407
|
+
if (!completePendingIngress) {
|
|
408
|
+
return Response.json({ error: 'Service Unavailable' }, { status: 503 });
|
|
409
|
+
}
|
|
410
|
+
let lease;
|
|
411
|
+
try {
|
|
412
|
+
lease = applicationRuntime.beginExecution();
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
completePendingIngress();
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
const headers = await createCorsPreflightResponseHeaders(match.route.middlewares, request, match.route.method);
|
|
420
|
+
if (headers) {
|
|
421
|
+
return new Response(null, { status: 204, headers });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
lease.complete();
|
|
426
|
+
completePendingIngress();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
if (error instanceof HttpPathDecodeError) {
|
|
432
|
+
return Response.json({ error: 'Invalid request' }, { status: 400 });
|
|
433
|
+
}
|
|
434
|
+
return Response.json({ error: 'Internal Server Error' }, { status: 500 });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
let match;
|
|
438
|
+
try {
|
|
439
|
+
match = findRuntimeHttpRoute(routes, method, url.pathname);
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
if (error instanceof HttpPathDecodeError) {
|
|
443
|
+
return Response.json({ error: 'Invalid request' }, { status: 400 });
|
|
444
|
+
}
|
|
445
|
+
return Response.json({ error: 'Internal Server Error' }, { status: 500 });
|
|
446
|
+
}
|
|
447
|
+
if (!match) {
|
|
448
|
+
return Response.json({ error: 'Not Found' }, { status: 404 });
|
|
449
|
+
}
|
|
450
|
+
const completePendingIngress = ingress.enter();
|
|
451
|
+
if (!completePendingIngress) {
|
|
452
|
+
return Response.json({ error: 'Service Unavailable' }, { status: 503 });
|
|
453
|
+
}
|
|
454
|
+
let lease;
|
|
455
|
+
try {
|
|
456
|
+
lease = applicationRuntime.beginExecution();
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
completePendingIngress();
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
const abortRequest = () => lease.abort(request.signal.reason);
|
|
463
|
+
let executionOwnedByStream = false;
|
|
464
|
+
let executionFinished = false;
|
|
465
|
+
const finishExecution = () => {
|
|
466
|
+
if (executionFinished)
|
|
467
|
+
return;
|
|
468
|
+
executionFinished = true;
|
|
469
|
+
request.signal.removeEventListener('abort', abortRequest);
|
|
470
|
+
lease.complete();
|
|
471
|
+
};
|
|
472
|
+
const adoptResponseStream = (stream) => {
|
|
473
|
+
executionOwnedByStream = true;
|
|
474
|
+
const activeStream = {
|
|
475
|
+
async abort(reason, deadline) {
|
|
476
|
+
stream.setDrainDeadline?.(deadline);
|
|
477
|
+
lease.abort(reason);
|
|
478
|
+
await stream.abort(reason, deadline);
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
activeResponseStreams.add(activeStream);
|
|
482
|
+
void stream.finished.then(() => {
|
|
483
|
+
activeResponseStreams.delete(activeStream);
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
request.signal.addEventListener('abort', abortRequest, { once: true });
|
|
487
|
+
if (request.signal.aborted)
|
|
488
|
+
abortRequest();
|
|
489
|
+
try {
|
|
490
|
+
const corsHeaders = await createCorsActualResponseHeaders(match.route.middlewares, request);
|
|
491
|
+
const complete = (response) => applyFrameworkHeadersToResponse(response, corsHeaders);
|
|
492
|
+
let context;
|
|
493
|
+
try {
|
|
494
|
+
context = await createHttpContext(request, url, match.params, match.route, lease.signal);
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
if (error instanceof HttpUnsupportedMediaTypeError) {
|
|
498
|
+
return complete(Response.json({ error: 'Unsupported Media Type' }, { status: 415 }));
|
|
499
|
+
}
|
|
500
|
+
if (error instanceof HttpInputDecodeError) {
|
|
501
|
+
return complete(Response.json({ error: 'Invalid request' }, { status: 400 }));
|
|
502
|
+
}
|
|
503
|
+
if (error instanceof SchemaValidationError) {
|
|
504
|
+
return complete(Response.json({ error: 'Validation failed' }, { status: 400 }));
|
|
505
|
+
}
|
|
506
|
+
throw error;
|
|
507
|
+
}
|
|
508
|
+
const handler = handlers.get(match.executionId)?.[match.route.name];
|
|
509
|
+
if (!handler) {
|
|
510
|
+
throw new Error(`LUTRE_HTTP_HANDLER_MISSING: ${match.executionId}.${match.route.name}`);
|
|
511
|
+
}
|
|
512
|
+
const result = await composeLayers({
|
|
513
|
+
context,
|
|
514
|
+
layers: match.route.middlewares,
|
|
515
|
+
resolve: (token, source) => applicationRuntime.resolve(token, source),
|
|
516
|
+
terminal: async (middlewareContext) => handler({
|
|
517
|
+
input: middlewareContext.input,
|
|
518
|
+
response: middlewareContext.response,
|
|
519
|
+
signal: middlewareContext.signal,
|
|
520
|
+
state: middlewareContext.state,
|
|
521
|
+
}),
|
|
522
|
+
});
|
|
523
|
+
const finalized = await finalizeHttpResult(match.route.definition, result, lease.signal, finishExecution);
|
|
524
|
+
if (finalized.streamControl)
|
|
525
|
+
adoptResponseStream(finalized.streamControl);
|
|
526
|
+
return complete(finalized.response);
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
if (error instanceof HttpInputDecodeError) {
|
|
530
|
+
return applyFrameworkHeadersToResponse(Response.json({ error: 'Invalid request' }, { status: 400 }), await safeCorsHeaders(match.route.middlewares, request));
|
|
531
|
+
}
|
|
532
|
+
if (error instanceof HttpInputValidationError) {
|
|
533
|
+
return applyFrameworkHeadersToResponse(Response.json({ error: 'Validation failed' }, { status: 400 }), await safeCorsHeaders(match.route.middlewares, request));
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
const mapped = await mapDeclaredError(match.route.definition, error);
|
|
537
|
+
if (mapped) {
|
|
538
|
+
const finalized = await finalizeHttpResult(match.route.definition, mapped, lease.signal, finishExecution);
|
|
539
|
+
if (finalized.streamControl) {
|
|
540
|
+
adoptResponseStream(finalized.streamControl);
|
|
541
|
+
}
|
|
542
|
+
return applyFrameworkHeadersToResponse(finalized.response, await safeCorsHeaders(match.route.middlewares, request));
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
// 内部のmapping/finalization errorをclientへ公開しない。
|
|
547
|
+
}
|
|
548
|
+
return applyFrameworkHeadersToResponse(Response.json({ error: 'Internal Server Error' }, { status: 500 }), await safeCorsHeaders(match.route.middlewares, request));
|
|
549
|
+
}
|
|
550
|
+
finally {
|
|
551
|
+
if (!executionOwnedByStream)
|
|
552
|
+
finishExecution();
|
|
553
|
+
completePendingIngress();
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function findRuntimeHttpRoute(routes, method, pathname) {
|
|
559
|
+
for (const candidate of routes) {
|
|
560
|
+
if (candidate.route.method !== method)
|
|
561
|
+
continue;
|
|
562
|
+
const params = matchHttpPath(candidate.route.segments, pathname);
|
|
563
|
+
if (!params)
|
|
564
|
+
continue;
|
|
565
|
+
return { ...candidate, params };
|
|
566
|
+
}
|
|
567
|
+
return undefined;
|
|
568
|
+
}
|
|
569
|
+
async function safeCorsHeaders(middlewares, request) {
|
|
570
|
+
try {
|
|
571
|
+
return await createCorsActualResponseHeaders(middlewares, request);
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
return undefined;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function isCorsPreflightRequest(request) {
|
|
578
|
+
return (request.method.toUpperCase() === 'OPTIONS' &&
|
|
579
|
+
request.headers.has('origin') &&
|
|
580
|
+
request.headers.has('access-control-request-method'));
|
|
581
|
+
}
|
|
582
|
+
async function createHttpContext(request, url, rawParams, route, signal) {
|
|
583
|
+
const definition = route.definition.request;
|
|
584
|
+
const params = definition?.params
|
|
585
|
+
? Object.fromEntries(await Promise.all(Object.entries(definition.params).map(async ([name, schema]) => [
|
|
586
|
+
name,
|
|
587
|
+
await validateSchema(schema, rawParams[name]),
|
|
588
|
+
])))
|
|
589
|
+
: rawParams;
|
|
590
|
+
const rawQuery = decodeQuery(url.searchParams);
|
|
591
|
+
const query = definition?.query
|
|
592
|
+
? await validateSchema(definition.query, rawQuery)
|
|
593
|
+
: rawQuery;
|
|
594
|
+
const headers = definition?.headers
|
|
595
|
+
? await validateRequestHeaders(definition.headers, definition.body !== undefined, request.headers)
|
|
596
|
+
: Object.fromEntries(request.headers.entries());
|
|
597
|
+
const body = definition?.body ? request.body : undefined;
|
|
598
|
+
const response = Object.fromEntries(Object.keys(route.definition.responses).map((name) => [
|
|
599
|
+
name,
|
|
600
|
+
(value = {}) => ({
|
|
601
|
+
kind: 'http-result',
|
|
602
|
+
response: name,
|
|
603
|
+
body: value.body,
|
|
604
|
+
...(value.headers === undefined ? {} : { headers: value.headers }),
|
|
605
|
+
}),
|
|
606
|
+
]));
|
|
607
|
+
return {
|
|
608
|
+
request,
|
|
609
|
+
input: { params, query, headers, body },
|
|
610
|
+
response,
|
|
611
|
+
signal,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
async function finalizeHttpResult(route, result, signal, completeExecution) {
|
|
615
|
+
const response = route.responses[result.response];
|
|
616
|
+
if (!response) {
|
|
617
|
+
throw new Error(`LUTRE_HTTP_RESPONSE_UNDECLARED: ${result.response}`);
|
|
618
|
+
}
|
|
619
|
+
const responseHeaders = await validateResponseHeaders(responseHeadersSchema(response.headers), result.headers);
|
|
620
|
+
const headers = mergeResponseHeaders(responseHeadersDefaults(response.headers), responseHeaders);
|
|
621
|
+
applyFrameworkResponseHeaders(headers, result[httpFrameworkHeaders]);
|
|
622
|
+
if (response.stream === 'server') {
|
|
623
|
+
if (!response.body || !isAsyncIterable(result.body)) {
|
|
624
|
+
throw new Error('Server-stream response requires an AsyncIterable body');
|
|
625
|
+
}
|
|
626
|
+
applyResponseHeaders(headers, {
|
|
627
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
628
|
+
'cache-control': 'no-cache',
|
|
629
|
+
});
|
|
630
|
+
const serverStream = createServerSentEventStream(result.body, response.body, signal, completeExecution);
|
|
631
|
+
return {
|
|
632
|
+
response: new Response(serverStream.stream, {
|
|
633
|
+
status: response.status,
|
|
634
|
+
headers,
|
|
635
|
+
}),
|
|
636
|
+
streamControl: serverStream,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
const body = response.body
|
|
640
|
+
? await validateSchema(response.body, result.body)
|
|
641
|
+
: undefined;
|
|
642
|
+
if (body === undefined) {
|
|
643
|
+
return {
|
|
644
|
+
response: new Response(null, { status: response.status, headers }),
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
if (body instanceof ReadableStream) {
|
|
648
|
+
const responseStream = createLeasedReadableStream(body, signal, completeExecution);
|
|
649
|
+
return {
|
|
650
|
+
response: new Response(responseStream.stream, {
|
|
651
|
+
status: response.status,
|
|
652
|
+
headers,
|
|
653
|
+
}),
|
|
654
|
+
streamControl: responseStream,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
if (typeof body === 'string' ||
|
|
658
|
+
body instanceof ArrayBuffer ||
|
|
659
|
+
ArrayBuffer.isView(body) ||
|
|
660
|
+
body instanceof Blob ||
|
|
661
|
+
body instanceof FormData ||
|
|
662
|
+
body instanceof URLSearchParams) {
|
|
663
|
+
return {
|
|
664
|
+
response: new Response(body, {
|
|
665
|
+
status: response.status,
|
|
666
|
+
headers,
|
|
667
|
+
}),
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
if (!headers.has('content-type')) {
|
|
671
|
+
headers.set('content-type', 'application/json; charset=utf-8');
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
response: new Response(JSON.stringify(body), {
|
|
675
|
+
status: response.status,
|
|
676
|
+
headers,
|
|
677
|
+
}),
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
function createLeasedReadableStream(source, signal, completeExecution) {
|
|
681
|
+
const reader = source.getReader();
|
|
682
|
+
let finished = false;
|
|
683
|
+
let completed = false;
|
|
684
|
+
let inFlightOperations = 0;
|
|
685
|
+
let cleanupSettled = true;
|
|
686
|
+
let controller;
|
|
687
|
+
let cleanup;
|
|
688
|
+
let resolveFinished;
|
|
689
|
+
const finishedPromise = new Promise((resolve) => {
|
|
690
|
+
resolveFinished = resolve;
|
|
691
|
+
});
|
|
692
|
+
const markFinished = () => {
|
|
693
|
+
if (finished)
|
|
694
|
+
return false;
|
|
695
|
+
finished = true;
|
|
696
|
+
signal.removeEventListener('abort', abort);
|
|
697
|
+
return true;
|
|
698
|
+
};
|
|
699
|
+
const completeIfSafe = () => {
|
|
700
|
+
if (completed || !finished || !cleanupSettled || inFlightOperations > 0) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
completed = true;
|
|
704
|
+
try {
|
|
705
|
+
reader.releaseLock();
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
// pending中のreader操作がsettleするまではlockを解放できない。
|
|
709
|
+
}
|
|
710
|
+
try {
|
|
711
|
+
completeExecution();
|
|
712
|
+
}
|
|
713
|
+
finally {
|
|
714
|
+
resolveFinished();
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
const stop = (reason, errorStream) => {
|
|
718
|
+
if (cleanup)
|
|
719
|
+
return cleanup;
|
|
720
|
+
if (!markFinished())
|
|
721
|
+
return finishedPromise;
|
|
722
|
+
cleanupSettled = false;
|
|
723
|
+
cleanup = (async () => {
|
|
724
|
+
let cleanupError;
|
|
725
|
+
try {
|
|
726
|
+
await reader.cancel(reason);
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
cleanupError = error;
|
|
730
|
+
}
|
|
731
|
+
finally {
|
|
732
|
+
cleanupSettled = true;
|
|
733
|
+
if (errorStream) {
|
|
734
|
+
controller?.error(cleanupError ?? reason ?? new Error('HTTP request was aborted'));
|
|
735
|
+
}
|
|
736
|
+
completeIfSafe();
|
|
737
|
+
}
|
|
738
|
+
await finishedPromise;
|
|
739
|
+
if (cleanupError !== undefined)
|
|
740
|
+
throw cleanupError;
|
|
741
|
+
})();
|
|
742
|
+
return cleanup;
|
|
743
|
+
};
|
|
744
|
+
const abort = () => {
|
|
745
|
+
void stop(signal.reason, true).catch(() => undefined);
|
|
746
|
+
};
|
|
747
|
+
const stream = new ReadableStream({
|
|
748
|
+
start(value) {
|
|
749
|
+
controller = value;
|
|
750
|
+
if (signal.aborted)
|
|
751
|
+
abort();
|
|
752
|
+
else
|
|
753
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
754
|
+
},
|
|
755
|
+
async pull(value) {
|
|
756
|
+
if (finished)
|
|
757
|
+
return;
|
|
758
|
+
inFlightOperations += 1;
|
|
759
|
+
try {
|
|
760
|
+
const next = await reader.read();
|
|
761
|
+
if (finished)
|
|
762
|
+
return;
|
|
763
|
+
if (next.done) {
|
|
764
|
+
if (markFinished())
|
|
765
|
+
value.close();
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
value.enqueue(next.value);
|
|
769
|
+
}
|
|
770
|
+
catch (error) {
|
|
771
|
+
if (markFinished())
|
|
772
|
+
value.error(error);
|
|
773
|
+
}
|
|
774
|
+
finally {
|
|
775
|
+
inFlightOperations -= 1;
|
|
776
|
+
completeIfSafe();
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
cancel(reason) {
|
|
780
|
+
return stop(reason, false);
|
|
781
|
+
},
|
|
782
|
+
});
|
|
783
|
+
return {
|
|
784
|
+
stream,
|
|
785
|
+
finished: finishedPromise,
|
|
786
|
+
abort: (reason) => stop(reason, true),
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
function createServerSentEventStream(source, schema, signal, completeExecution) {
|
|
790
|
+
const encoder = new TextEncoder();
|
|
791
|
+
const iterator = new AsyncIteratorLifecycle(source[Symbol.asyncIterator]());
|
|
792
|
+
let finished = false;
|
|
793
|
+
let completed = false;
|
|
794
|
+
let inFlightOperations = 0;
|
|
795
|
+
let cleanupComplete = false;
|
|
796
|
+
let cleanupSettled = true;
|
|
797
|
+
let controller;
|
|
798
|
+
let cleanupAttempt;
|
|
799
|
+
let cleanupDeadline;
|
|
800
|
+
let resolveFinished;
|
|
801
|
+
const finishedPromise = new Promise((resolve) => {
|
|
802
|
+
resolveFinished = resolve;
|
|
803
|
+
});
|
|
804
|
+
const markFinished = () => {
|
|
805
|
+
if (finished)
|
|
806
|
+
return false;
|
|
807
|
+
finished = true;
|
|
808
|
+
signal.removeEventListener('abort', abort);
|
|
809
|
+
return true;
|
|
810
|
+
};
|
|
811
|
+
const completeIfSafe = () => {
|
|
812
|
+
if (completed || !finished || !cleanupSettled || inFlightOperations > 0) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
completed = true;
|
|
816
|
+
try {
|
|
817
|
+
completeExecution();
|
|
818
|
+
}
|
|
819
|
+
finally {
|
|
820
|
+
resolveFinished();
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
const stop = (reason, errorStream, deadline) => {
|
|
824
|
+
if (cleanupAttempt) {
|
|
825
|
+
if (deadline !== undefined) {
|
|
826
|
+
cleanupDeadline = Math.min(cleanupDeadline ?? deadline, deadline);
|
|
827
|
+
}
|
|
828
|
+
return cleanupAttempt;
|
|
829
|
+
}
|
|
830
|
+
if (deadline !== undefined)
|
|
831
|
+
cleanupDeadline = deadline;
|
|
832
|
+
markFinished();
|
|
833
|
+
if (cleanupComplete)
|
|
834
|
+
return finishedPromise;
|
|
835
|
+
cleanupSettled = false;
|
|
836
|
+
const operation = (async () => {
|
|
837
|
+
let cleanupError;
|
|
838
|
+
try {
|
|
839
|
+
await iterator.drain(reason, () => cleanupDeadline, 'HTTP iterator cleanup exceeded the drain deadline.');
|
|
840
|
+
}
|
|
841
|
+
catch (error) {
|
|
842
|
+
if (error instanceof AsyncIteratorCleanupDeadlineError)
|
|
843
|
+
throw error;
|
|
844
|
+
cleanupError = error;
|
|
845
|
+
}
|
|
846
|
+
finally {
|
|
847
|
+
if (iterator.done || cleanupError !== undefined) {
|
|
848
|
+
cleanupComplete = true;
|
|
849
|
+
cleanupSettled = true;
|
|
850
|
+
if (errorStream) {
|
|
851
|
+
controller?.error(cleanupError ?? reason ?? new Error('HTTP request was aborted'));
|
|
852
|
+
}
|
|
853
|
+
completeIfSafe();
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
await finishedPromise;
|
|
857
|
+
if (cleanupError !== undefined)
|
|
858
|
+
throw cleanupError;
|
|
859
|
+
})();
|
|
860
|
+
cleanupAttempt = operation;
|
|
861
|
+
void operation.then(() => {
|
|
862
|
+
if (cleanupAttempt === operation)
|
|
863
|
+
cleanupAttempt = undefined;
|
|
864
|
+
}, () => {
|
|
865
|
+
if (cleanupAttempt === operation)
|
|
866
|
+
cleanupAttempt = undefined;
|
|
867
|
+
});
|
|
868
|
+
return operation;
|
|
869
|
+
};
|
|
870
|
+
const abort = () => {
|
|
871
|
+
void stop(signal.reason, true).catch(() => undefined);
|
|
872
|
+
};
|
|
873
|
+
const stream = new ReadableStream({
|
|
874
|
+
start(value) {
|
|
875
|
+
controller = value;
|
|
876
|
+
if (signal.aborted)
|
|
877
|
+
abort();
|
|
878
|
+
else
|
|
879
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
880
|
+
},
|
|
881
|
+
async pull(value) {
|
|
882
|
+
if (finished)
|
|
883
|
+
return;
|
|
884
|
+
inFlightOperations += 1;
|
|
885
|
+
let stopPromise;
|
|
886
|
+
try {
|
|
887
|
+
const next = await iterator.next();
|
|
888
|
+
if (finished)
|
|
889
|
+
return;
|
|
890
|
+
if (next.done) {
|
|
891
|
+
if (markFinished())
|
|
892
|
+
value.close();
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const item = await validateSchema(schema, next.value);
|
|
896
|
+
if (finished)
|
|
897
|
+
return;
|
|
898
|
+
value.enqueue(encoder.encode(`data:${JSON.stringify(item)}\n\n`));
|
|
899
|
+
}
|
|
900
|
+
catch (error) {
|
|
901
|
+
stopPromise = stop(error, true);
|
|
902
|
+
}
|
|
903
|
+
finally {
|
|
904
|
+
inFlightOperations -= 1;
|
|
905
|
+
completeIfSafe();
|
|
906
|
+
}
|
|
907
|
+
await stopPromise;
|
|
908
|
+
},
|
|
909
|
+
cancel(reason) {
|
|
910
|
+
return stop(reason, false);
|
|
911
|
+
},
|
|
912
|
+
});
|
|
913
|
+
return {
|
|
914
|
+
stream,
|
|
915
|
+
finished: finishedPromise,
|
|
916
|
+
setDrainDeadline(deadline) {
|
|
917
|
+
cleanupDeadline = deadline;
|
|
918
|
+
},
|
|
919
|
+
abort: (reason, deadline) => stop(reason, true, deadline),
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
async function mapDeclaredError(route, error) {
|
|
923
|
+
for (const [responseName, response] of Object.entries(route.responses)) {
|
|
924
|
+
const mapping = response.error;
|
|
925
|
+
if (!mapping?.definition.is(error))
|
|
926
|
+
continue;
|
|
927
|
+
const mapped = await mapping.map(error);
|
|
928
|
+
if (typeof mapped !== 'object' || mapped === null || !('body' in mapped)) {
|
|
929
|
+
throw new Error('HTTP error mapping must return a body');
|
|
930
|
+
}
|
|
931
|
+
return {
|
|
932
|
+
kind: 'http-result',
|
|
933
|
+
response: responseName,
|
|
934
|
+
body: mapped.body,
|
|
935
|
+
...('headers' in mapped
|
|
936
|
+
? { headers: mapped.headers }
|
|
937
|
+
: {}),
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
return undefined;
|
|
941
|
+
}
|
|
942
|
+
function isAsyncIterable(value) {
|
|
943
|
+
return (typeof value === 'object' &&
|
|
944
|
+
value !== null &&
|
|
945
|
+
Symbol.asyncIterator in value &&
|
|
946
|
+
typeof value[Symbol.asyncIterator] === 'function');
|
|
947
|
+
}
|