@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.
Files changed (168) hide show
  1. package/README.md +63 -32
  2. package/dist/adapters/aws-lambda.d.ts +5 -3
  3. package/dist/adapters/aws-lambda.js +53 -23
  4. package/dist/adapters/bun.d.ts +7 -4
  5. package/dist/adapters/bun.js +52 -42
  6. package/dist/adapters/cloudflare-workers.d.ts +5 -3
  7. package/dist/adapters/cloudflare-workers.js +23 -28
  8. package/dist/adapters/deno.d.ts +7 -4
  9. package/dist/adapters/deno.js +94 -57
  10. package/dist/adapters/electron.d.ts +13 -5
  11. package/dist/adapters/electron.js +76 -28
  12. package/dist/application/definition.d.ts +31 -0
  13. package/dist/application/definition.js +16 -0
  14. package/dist/application/index.d.ts +4 -63
  15. package/dist/application/index.js +2 -11
  16. package/dist/application/kernel-internal.d.ts +2 -0
  17. package/dist/application/kernel-internal.js +10 -0
  18. package/dist/application/kernel.d.ts +21 -0
  19. package/dist/application/kernel.js +65 -0
  20. package/dist/core/diagnostic.d.ts +9 -0
  21. package/dist/core/diagnostic.js +9 -0
  22. package/dist/core/extension.d.ts +107 -0
  23. package/dist/core/extension.js +73 -0
  24. package/dist/core/generic-layer.d.ts +26 -0
  25. package/dist/core/generic-layer.js +116 -0
  26. package/dist/core/index.d.ts +31 -15
  27. package/dist/core/index.js +15 -15
  28. package/dist/core/injection.d.ts +4 -13
  29. package/dist/core/injection.js +63 -0
  30. package/dist/core/model/context.d.ts +18 -0
  31. package/dist/core/model/context.js +34 -0
  32. package/dist/core/model/lifecycle.d.ts +5 -0
  33. package/dist/core/model/lifecycle.js +44 -0
  34. package/dist/core/model/modules.d.ts +2 -0
  35. package/dist/core/model/modules.js +101 -0
  36. package/dist/core/model/providers.d.ts +4 -0
  37. package/dist/core/model/providers.js +74 -0
  38. package/dist/core/model/snapshot.d.ts +6 -0
  39. package/dist/core/model/snapshot.js +31 -0
  40. package/dist/core/model/types.d.ts +78 -0
  41. package/dist/core/model/validation.d.ts +4 -0
  42. package/dist/core/model/validation.js +40 -0
  43. package/dist/core/model/visibility.d.ts +4 -0
  44. package/dist/core/model/visibility.js +20 -0
  45. package/dist/core/model.d.ts +10 -0
  46. package/dist/core/model.js +346 -0
  47. package/dist/core/module.d.ts +8 -13
  48. package/dist/core/source-location.d.ts +8 -0
  49. package/dist/core/source-location.js +31 -0
  50. package/dist/core/token-internal.d.ts +3 -0
  51. package/dist/core/token-internal.js +4 -0
  52. package/dist/core/type.d.ts +7 -0
  53. package/dist/core/type.js +4 -0
  54. package/dist/generated/version.d.ts +1 -1
  55. package/dist/generated/version.js +1 -1
  56. package/dist/graph/index.d.ts +2 -3
  57. package/dist/graph/index.js +1 -3
  58. package/dist/graph/model.d.ts +34 -0
  59. package/dist/graph/model.js +127 -0
  60. package/dist/http/auth.d.ts +46 -0
  61. package/dist/http/auth.js +96 -0
  62. package/dist/http/client.d.ts +28 -31
  63. package/dist/http/client.js +58 -44
  64. package/dist/http/cors-internal.d.ts +29 -0
  65. package/dist/http/cors-internal.js +160 -0
  66. package/dist/http/cors.d.ts +5 -8
  67. package/dist/http/cors.js +7 -165
  68. package/dist/http/extension.d.ts +348 -0
  69. package/dist/http/extension.js +947 -0
  70. package/dist/http/index.d.ts +9 -7
  71. package/dist/http/index.js +5 -7
  72. package/dist/{application → http}/openapi.d.ts +12 -3
  73. package/dist/{application → http}/openapi.js +164 -102
  74. package/dist/http/path.d.ts +1 -42
  75. package/dist/http/path.js +1 -2
  76. package/dist/http/runtime/request.d.ts +17 -0
  77. package/dist/http/runtime/request.js +109 -0
  78. package/dist/http/runtime/response.d.ts +18 -0
  79. package/dist/http/runtime/response.js +108 -0
  80. package/dist/index.d.ts +4 -0
  81. package/dist/index.js +2 -0
  82. package/dist/message-port/extension.d.ts +66 -0
  83. package/dist/message-port/extension.js +395 -0
  84. package/dist/message-port/index.d.ts +4 -73
  85. package/dist/message-port/index.js +2 -189
  86. package/dist/message-port/transport.d.ts +11 -0
  87. package/dist/message-port/transport.js +45 -0
  88. package/dist/runtime/async-iterator-lifecycle.d.ts +12 -0
  89. package/dist/runtime/async-iterator-lifecycle.js +95 -0
  90. package/dist/runtime/capability.d.ts +11 -11
  91. package/dist/runtime/capability.js +3 -3
  92. package/dist/runtime/di.d.ts +2 -22
  93. package/dist/runtime/di.js +8 -285
  94. package/dist/runtime/execution-tracker.d.ts +8 -0
  95. package/dist/runtime/execution-tracker.js +54 -0
  96. package/dist/runtime/index.d.ts +15 -9
  97. package/dist/runtime/index.js +9 -9
  98. package/dist/runtime/ingress-gate.d.ts +7 -0
  99. package/dist/runtime/ingress-gate.js +33 -0
  100. package/dist/runtime/kernel.d.ts +23 -0
  101. package/dist/runtime/kernel.js +354 -0
  102. package/dist/runtime/logger.d.ts +2 -0
  103. package/dist/runtime/logger.js +2 -0
  104. package/dist/runtime/node-capabilities.d.ts +1 -1
  105. package/dist/runtime/node-capabilities.js +1 -1
  106. package/dist/runtime/server-port.d.ts +2 -0
  107. package/dist/runtime/server-port.js +11 -0
  108. package/dist/tasks/extension.d.ts +144 -0
  109. package/dist/tasks/extension.js +499 -0
  110. package/dist/tasks/index.d.ts +2 -0
  111. package/dist/tasks/index.js +1 -0
  112. package/dist/tasks/runtime/cron.d.ts +6 -0
  113. package/dist/tasks/runtime/cron.js +89 -0
  114. package/dist/websocket/errors.d.ts +9 -0
  115. package/dist/websocket/errors.js +18 -0
  116. package/dist/websocket/extension.d.ts +154 -0
  117. package/dist/websocket/extension.js +524 -0
  118. package/dist/websocket/index.d.ts +3 -0
  119. package/dist/websocket/index.js +2 -0
  120. package/package.json +12 -16
  121. package/dist/application/binding.d.ts +0 -33
  122. package/dist/application/binding.js +0 -295
  123. package/dist/application/host.d.ts +0 -8
  124. package/dist/application/host.js +0 -14
  125. package/dist/application/queue.d.ts +0 -3
  126. package/dist/application/queue.js +0 -4
  127. package/dist/application/scheduler.d.ts +0 -2
  128. package/dist/application/scheduler.js +0 -45
  129. package/dist/core/context-key.d.ts +0 -16
  130. package/dist/core/context-key.js +0 -17
  131. package/dist/core/contract-internal.d.ts +0 -22
  132. package/dist/core/contract-internal.js +0 -61
  133. package/dist/core/contract.d.ts +0 -96
  134. package/dist/core/contract.js +0 -88
  135. package/dist/core/execution.d.ts +0 -78
  136. package/dist/core/execution.js +0 -61
  137. package/dist/core/implementation.d.ts +0 -44
  138. package/dist/core/implementation.js +0 -35
  139. package/dist/core/layer.d.ts +0 -137
  140. package/dist/core/layer.js +0 -81
  141. package/dist/graph/compiler.d.ts +0 -4
  142. package/dist/graph/compiler.js +0 -228
  143. package/dist/graph/graph.d.ts +0 -18
  144. package/dist/graph/graph.js +0 -1118
  145. package/dist/graph/ir.d.ts +0 -175
  146. package/dist/http/application.d.ts +0 -18
  147. package/dist/http/application.js +0 -515
  148. package/dist/http/basic-auth.d.ts +0 -37
  149. package/dist/http/basic-auth.js +0 -67
  150. package/dist/http/definitions.d.ts +0 -363
  151. package/dist/http/definitions.js +0 -310
  152. package/dist/http/hosted-protocol.d.ts +0 -4
  153. package/dist/http/hosted-protocol.js +0 -4
  154. package/dist/http/internal.d.ts +0 -8
  155. package/dist/http/internal.js +0 -1
  156. package/dist/http/params.d.ts +0 -2
  157. package/dist/http/params.js +0 -19
  158. package/dist/http/request-body.d.ts +0 -4
  159. package/dist/http/request-body.js +0 -6
  160. package/dist/http/validate.d.ts +0 -20
  161. package/dist/http/validate.js +0 -6
  162. package/dist/message-port/environment.d.ts +0 -2
  163. package/dist/message-port/environment.js +0 -4
  164. package/dist/runtime/application.d.ts +0 -38
  165. package/dist/runtime/application.js +0 -339
  166. package/dist/runtime/pipeline.d.ts +0 -11
  167. package/dist/runtime/pipeline.js +0 -161
  168. /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
+ }