@loutrejs/loutre 0.6.0 → 0.8.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 (71) hide show
  1. package/dist/adapters/bun.d.ts +2 -4
  2. package/dist/adapters/bun.js +36 -28
  3. package/dist/adapters/cloudflare-workers.d.ts +2 -4
  4. package/dist/adapters/cloudflare-workers.js +6 -11
  5. package/dist/adapters/server-transports.d.ts +4 -0
  6. package/dist/adapters/server-transports.js +18 -0
  7. package/dist/adapters/websocket-native.d.ts +24 -0
  8. package/dist/adapters/websocket-native.js +150 -0
  9. package/dist/application/kernel.d.ts +2 -1
  10. package/dist/core/extension.d.ts +26 -3
  11. package/dist/core/extension.js +19 -0
  12. package/dist/core/index.d.ts +2 -1
  13. package/dist/core/instrumentation.d.ts +36 -0
  14. package/dist/core/instrumentation.js +1 -0
  15. package/dist/core/model/modules.js +13 -17
  16. package/dist/core/model.js +14 -4
  17. package/dist/core/module.d.ts +3 -3
  18. package/dist/devtools/capsule.d.ts +51 -0
  19. package/dist/devtools/capsule.js +247 -0
  20. package/dist/devtools/index.d.ts +11 -0
  21. package/dist/devtools/index.js +6 -0
  22. package/dist/devtools/inspector.d.ts +57 -0
  23. package/dist/devtools/inspector.js +1 -0
  24. package/dist/devtools/module.d.ts +18 -0
  25. package/dist/devtools/module.js +26 -0
  26. package/dist/devtools/observer.d.ts +77 -0
  27. package/dist/devtools/observer.js +10 -0
  28. package/dist/devtools/protocol.d.ts +1 -0
  29. package/dist/devtools/protocol.js +1 -0
  30. package/dist/devtools/provider-instrumentation.d.ts +14 -0
  31. package/dist/devtools/provider-instrumentation.js +117 -0
  32. package/dist/devtools/runtime.d.ts +20 -0
  33. package/dist/devtools/runtime.js +519 -0
  34. package/dist/devtools/values.d.ts +35 -0
  35. package/dist/devtools/values.js +172 -0
  36. package/dist/generated/version.d.ts +1 -1
  37. package/dist/generated/version.js +1 -1
  38. package/dist/graph/model.js +37 -40
  39. package/dist/http/auth.d.ts +2 -2
  40. package/dist/http/auth.js +9 -0
  41. package/dist/http/client.js +5 -5
  42. package/dist/http/extension.d.ts +37 -10
  43. package/dist/http/extension.js +249 -57
  44. package/dist/http/index.d.ts +3 -2
  45. package/dist/http/index.js +1 -1
  46. package/dist/http/openapi.d.ts +3 -4
  47. package/dist/http/openapi.js +45 -26
  48. package/dist/http/request-head.d.ts +6 -0
  49. package/dist/http/request-head.js +1 -0
  50. package/dist/http/route-validation.d.ts +9 -0
  51. package/dist/http/route-validation.js +18 -0
  52. package/dist/http/runtime/response.js +10 -10
  53. package/dist/message-port/extension.js +20 -4
  54. package/dist/runtime/di.d.ts +4 -0
  55. package/dist/runtime/di.js +42 -31
  56. package/dist/runtime/engine.d.ts +1 -1
  57. package/dist/runtime/engine.js +1 -2
  58. package/dist/runtime/kernel.d.ts +4 -2
  59. package/dist/runtime/kernel.js +96 -3
  60. package/dist/runtime/node-capabilities.d.ts +1 -1
  61. package/dist/runtime/node-capabilities.js +2 -1
  62. package/dist/tasks/extension.js +133 -118
  63. package/dist/websocket/driver.d.ts +30 -0
  64. package/dist/websocket/driver.js +96 -0
  65. package/dist/websocket/extension.d.ts +33 -12
  66. package/dist/websocket/extension.js +72 -47
  67. package/dist/websocket/index.d.ts +3 -1
  68. package/dist/websocket/index.js +1 -0
  69. package/package.json +7 -6
  70. package/dist/adapters/deno.d.ts +0 -35
  71. package/dist/adapters/deno.js +0 -231
@@ -1,18 +1,35 @@
1
1
  import { collectInjectedDependencies, getSourceLocation, inheritSourceLocation, composeLayers, defineExecution, defineExecutionExtension, defineLayer, runInInjectionContext, runtimeCapability, SchemaValidationError, validateSchema, } from '../core/index.js';
2
2
  import { createCorsActualResponseHeaders, createCorsPreflightResponseHeaders, } from './cors-internal.js';
3
3
  import { assertValidHttpMethod, compareHttpPathSpecificity, createHttpDispatchKey, HttpPathDecodeError, matchHttpPath, parseHttpPath, } from './path.js';
4
+ import { findHttpRouteConflicts } from './route-validation.js';
4
5
  import { IngressGate } from '../runtime/ingress-gate.js';
5
6
  import { AsyncIteratorCleanupDeadlineError, AsyncIteratorLifecycle, } from '../runtime/async-iterator-lifecycle.js';
6
7
  import { decodeBody, decodeQuery, HttpInputDecodeError, HttpInputValidationError, HttpUnsupportedMediaTypeError, validatedContentType, validateRequestHeaders, } from './runtime/request.js';
7
8
  import { applyFrameworkHeadersToResponse, applyFrameworkResponseHeaders, applyResponseHeaders, isResponseHeadersWithDefaults, isStandardSchema, mergeResponseHeaders, responseHeadersDefaults, responseHeadersSchema, validateResponseHeaders, } from './runtime/response.js';
9
+ import { match as patternMatch } from 'ts-pattern';
8
10
  export const HTTP_SERVER = runtimeCapability('http.server');
11
+ const httpContractBrand = Symbol.for('loutre.http-contract');
12
+ export function isHttpContract(value) {
13
+ if (typeof value !== 'object' || value === null)
14
+ return false;
15
+ const candidate = value;
16
+ return (candidate.kind === 'http-contract' &&
17
+ candidate[httpContractBrand] === true &&
18
+ typeof candidate.routes === 'object' &&
19
+ candidate.routes !== null);
20
+ }
21
+ export function defineHttpRaw(definition) {
22
+ return defineExecution(httpExecutionExtension, { ...definition, raw: true });
23
+ }
9
24
  export const httpExecutionExtension = defineExecutionExtension({
10
25
  kind: 'execution-extension',
11
26
  abiVersion: '1',
12
27
  name: 'loutre:http',
13
28
  compile(definition, context) {
14
- const contractSource = getSourceLocation(definition.contract);
15
- const routes = Object.entries(definition.contract.routes).map(([name, route]) => compileHttpRoute(name, route, contractSource));
29
+ const raw = 'route' in definition;
30
+ const routes = raw
31
+ ? [compileRawRoute(definition.route)]
32
+ : Object.entries(definition.contract.routes).map(([name, route]) => compileHttpRoute(name, route, getSourceLocation(definition.contract)));
16
33
  const id = definition.name || `${context.moduleId}.http.${context.definitionIndex}`;
17
34
  const dependencies = new Set(collectInjectedDependencies({
18
35
  kind: 'execution',
@@ -43,25 +60,22 @@ export const httpExecutionExtension = defineExecutionExtension({
43
60
  ],
44
61
  compiled: Object.freeze({
45
62
  routes: Object.freeze(routes),
46
- factory: definition.factory,
63
+ factory: (raw
64
+ ? () => ({ raw: definition.factory() })
65
+ : definition.factory),
47
66
  }),
48
67
  };
49
68
  },
50
69
  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 [];
70
+ const routes = executions.flatMap((execution) => execution.compiled.routes.map((route) => ({
71
+ method: route.method,
72
+ path: route.path,
73
+ owner: execution.id,
74
+ })));
75
+ return findHttpRouteConflicts(routes).map(({ existing, route }) => ({
76
+ code: 'LUTRE_HTTP_DUPLICATE_ROUTE',
77
+ message: `${route.method} ${route.path} conflicts with ${existing.owner}.`,
78
+ path: route.owner,
65
79
  }));
66
80
  },
67
81
  createRuntime(context) {
@@ -81,10 +95,16 @@ export const httpExecutionExtension = defineExecutionExtension({
81
95
  ? {}
82
96
  : { source: getSourceLocation(middleware) }),
83
97
  })),
84
- responses: Object.fromEntries(Object.entries(route.definition.responses).map(([name, response]) => [
85
- name,
86
- { status: response.status },
87
- ])),
98
+ ...(route.raw
99
+ ? {
100
+ raw: true,
101
+ ...(route.protocol === undefined
102
+ ? {}
103
+ : { protocol: route.protocol }),
104
+ }
105
+ : {
106
+ responses: Object.fromEntries(Object.entries(route.definition.responses).map(([name, response]) => [name, { status: response.status }])),
107
+ }),
88
108
  })),
89
109
  }),
90
110
  host: {
@@ -101,12 +121,14 @@ export function httpError(definition, map) {
101
121
  }
102
122
  export function defineHttpContract(routes) {
103
123
  const resolvedRoutes = resolveHttpContractRoutes(routes);
104
- for (const [name, route] of Object.entries(resolvedRoutes)) {
105
- compileHttpRoute(name, route);
106
- }
124
+ const canonicalRoutes = Object.fromEntries(Object.entries(resolvedRoutes).map(([name, route]) => [
125
+ name,
126
+ compileHttpRoute(name, route).definition,
127
+ ]));
107
128
  return Object.freeze({
108
129
  kind: 'http-contract',
109
- routes: Object.freeze(resolvedRoutes),
130
+ [httpContractBrand]: true,
131
+ routes: Object.freeze(canonicalRoutes),
110
132
  });
111
133
  }
112
134
  function resolveHttpContractRoutes(tree) {
@@ -186,6 +208,7 @@ export const validate = Object.freeze({
186
208
  body: bodyValidation,
187
209
  });
188
210
  export const executionHttp = Object.freeze({
211
+ raw: defineHttpRaw,
189
212
  contract: defineHttpContract,
190
213
  implementation: defineHttpImplementation,
191
214
  middleware: defineHttpMiddleware,
@@ -203,7 +226,29 @@ export function withHttpFrameworkHeaders(result, headers) {
203
226
  [httpFrameworkHeaders]: { ...current, ...headers },
204
227
  };
205
228
  }
229
+ function compileRawRoute(route) {
230
+ if (route.method !== '*')
231
+ assertValidHttpMethod(route.method);
232
+ const segments = parseHttpPath(route.path);
233
+ return Object.freeze({
234
+ name: 'raw',
235
+ method: route.method.toUpperCase(),
236
+ path: route.path,
237
+ segments,
238
+ dispatch: createHttpDispatchKey(route.method, segments),
239
+ definition: Object.freeze({
240
+ method: route.method,
241
+ path: route.path,
242
+ responses: Object.freeze({}),
243
+ }),
244
+ middlewares: Object.freeze([...(route.middlewares ?? [])]),
245
+ raw: true,
246
+ ...(route.protocol === undefined ? {} : { protocol: route.protocol }),
247
+ });
248
+ }
206
249
  function compileHttpRoute(name, route, routeSourceFallback) {
250
+ if (route.method === '*')
251
+ throw new TypeError('method: *はhttp.raw()でのみ利用できます。');
207
252
  assertValidHttpMethod(route.method);
208
253
  const segments = parseHttpPath(route.path);
209
254
  assertValidHttpRouteDefinition(route, segments);
@@ -281,15 +326,15 @@ function compileHttpMiddlewares(route) {
281
326
  return Object.freeze(compiled);
282
327
  }
283
328
  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);
329
+ return patternMatch(middleware)
330
+ .with({ part: 'body' }, () => {
331
+ const schema = route.request?.body;
332
+ if (!schema) {
333
+ throw new TypeError(`HTTP ${route.method} ${route.path} uses validate.body but declares no request body.`);
291
334
  }
292
- }
335
+ return createBodyValidationMiddleware(schema);
336
+ })
337
+ .exhaustive();
293
338
  }
294
339
  function createBodyValidationMiddleware(schema) {
295
340
  return defineLayer({
@@ -409,7 +454,12 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
409
454
  }
410
455
  let lease;
411
456
  try {
412
- lease = applicationRuntime.beginExecution();
457
+ lease = applicationRuntime.beginExecution({
458
+ executionId: match.executionId,
459
+ executionKind: 'http.request',
460
+ graphNodeId: match.executionId,
461
+ name: `OPTIONS ${match.route.path}`,
462
+ });
413
463
  }
414
464
  catch (error) {
415
465
  completePendingIngress();
@@ -418,6 +468,7 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
418
468
  try {
419
469
  const headers = await createCorsPreflightResponseHeaders(match.route.middlewares, request, match.route.method);
420
470
  if (headers) {
471
+ lease.annotate?.({ 'http.status_code': 204 });
421
472
  return new Response(null, { status: 204, headers });
422
473
  }
423
474
  }
@@ -445,7 +496,30 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
445
496
  return Response.json({ error: 'Internal Server Error' }, { status: 500 });
446
497
  }
447
498
  if (!match) {
448
- return Response.json({ error: 'Not Found' }, { status: 404 });
499
+ const completePendingIngress = ingress.enter();
500
+ if (!completePendingIngress) {
501
+ return Response.json({ error: 'Service Unavailable' }, { status: 503 });
502
+ }
503
+ let lease;
504
+ try {
505
+ lease = applicationRuntime.beginExecution({
506
+ executionId: 'http.not-found',
507
+ executionKind: 'http.request',
508
+ name: `${method} ${url.pathname}`,
509
+ });
510
+ }
511
+ catch (error) {
512
+ completePendingIngress();
513
+ throw error;
514
+ }
515
+ try {
516
+ lease.annotate?.({ 'http.status_code': 404 });
517
+ return Response.json({ error: 'Not Found' }, { status: 404 });
518
+ }
519
+ finally {
520
+ lease.complete();
521
+ completePendingIngress();
522
+ }
449
523
  }
450
524
  const completePendingIngress = ingress.enter();
451
525
  if (!completePendingIngress) {
@@ -453,7 +527,12 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
453
527
  }
454
528
  let lease;
455
529
  try {
456
- lease = applicationRuntime.beginExecution();
530
+ lease = applicationRuntime.beginExecution({
531
+ executionId: match.executionId,
532
+ executionKind: 'http.request',
533
+ graphNodeId: match.executionId,
534
+ name: `${match.route.method} ${match.route.path}`,
535
+ });
457
536
  }
458
537
  catch (error) {
459
538
  completePendingIngress();
@@ -462,12 +541,17 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
462
541
  const abortRequest = () => lease.abort(request.signal.reason);
463
542
  let executionOwnedByStream = false;
464
543
  let executionFinished = false;
544
+ let executionResult;
545
+ let hasExecutionResult = false;
465
546
  const finishExecution = () => {
466
547
  if (executionFinished)
467
548
  return;
468
549
  executionFinished = true;
469
550
  request.signal.removeEventListener('abort', abortRequest);
470
- lease.complete();
551
+ if (hasExecutionResult)
552
+ lease.complete(executionResult);
553
+ else
554
+ lease.complete();
471
555
  };
472
556
  const adoptResponseStream = (stream) => {
473
557
  executionOwnedByStream = true;
@@ -486,9 +570,13 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
486
570
  request.signal.addEventListener('abort', abortRequest, { once: true });
487
571
  if (request.signal.aborted)
488
572
  abortRequest();
573
+ const annotateResponse = (response) => {
574
+ lease.annotate?.({ 'http.status_code': response.status });
575
+ return response;
576
+ };
489
577
  try {
490
578
  const corsHeaders = await createCorsActualResponseHeaders(match.route.middlewares, request);
491
- const complete = (response) => applyFrameworkHeadersToResponse(response, corsHeaders);
579
+ const complete = (response) => annotateResponse(applyFrameworkHeadersToResponse(response, corsHeaders));
492
580
  let context;
493
581
  try {
494
582
  context = await createHttpContext(request, url, match.params, match.route, lease.signal);
@@ -509,17 +597,83 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
509
597
  if (!handler) {
510
598
  throw new Error(`LUTRE_HTTP_HANDLER_MISSING: ${match.executionId}.${match.route.name}`);
511
599
  }
512
- const result = await composeLayers({
600
+ const dispatch = () => composeLayers({
513
601
  context,
514
602
  layers: match.route.middlewares,
515
603
  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
- }),
604
+ terminal: async (middlewareContext) => {
605
+ const handlerName = `${match.executionId}.${match.route.name}`;
606
+ const handlerOperation = applicationRuntime.beginOperation?.({
607
+ kind: 'http.handler',
608
+ name: handlerName,
609
+ graphNodeId: match.executionId,
610
+ }, match.route.raw
611
+ ? undefined
612
+ : {
613
+ input: middlewareContext.input,
614
+ invoke: (input) => replayHttpHandler(applicationRuntime, handler, match, input, middlewareContext.state),
615
+ });
616
+ const invoke = () => handler({
617
+ ...(match.route.raw
618
+ ? {
619
+ request,
620
+ execution: {
621
+ signal: lease.signal,
622
+ annotate: (attributes) => lease.annotate?.(attributes),
623
+ beginOperation: (metadata) => {
624
+ const begin = () => applicationRuntime.beginOperation?.(metadata) ?? {
625
+ complete() { },
626
+ };
627
+ return lease.run ? lease.run(begin) : begin();
628
+ },
629
+ },
630
+ }
631
+ : {}),
632
+ input: middlewareContext.input,
633
+ response: middlewareContext.response,
634
+ signal: match.route.raw
635
+ ? request.signal
636
+ : middlewareContext.signal,
637
+ state: middlewareContext.state,
638
+ });
639
+ let result;
640
+ let hasResult = false;
641
+ try {
642
+ result = await (handlerOperation?.run
643
+ ? handlerOperation.run(invoke)
644
+ : invoke());
645
+ hasResult = true;
646
+ return result;
647
+ }
648
+ catch (error) {
649
+ handlerOperation?.fail?.(error);
650
+ throw error;
651
+ }
652
+ finally {
653
+ if (hasResult)
654
+ handlerOperation?.complete(result);
655
+ else
656
+ handlerOperation?.complete();
657
+ }
658
+ },
522
659
  });
660
+ const result = await (lease.run ? lease.run(dispatch) : dispatch());
661
+ executionResult = result;
662
+ hasExecutionResult = true;
663
+ if (result instanceof Response) {
664
+ if (result.body) {
665
+ const stream = createLeasedReadableStream(result.body, lease.signal, finishExecution);
666
+ adoptResponseStream(stream);
667
+ return complete(new Response(stream.stream, {
668
+ status: result.status,
669
+ statusText: result.statusText,
670
+ headers: result.headers,
671
+ }));
672
+ }
673
+ return complete(result);
674
+ }
675
+ if (match.route.raw)
676
+ throw new TypeError('Raw handlerとmiddlewareはResponseを返す必要があります。');
523
677
  const finalized = await finalizeHttpResult(match.route.definition, result, lease.signal, finishExecution);
524
678
  if (finalized.streamControl)
525
679
  adoptResponseStream(finalized.streamControl);
@@ -527,11 +681,12 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
527
681
  }
528
682
  catch (error) {
529
683
  if (error instanceof HttpInputDecodeError) {
530
- return applyFrameworkHeadersToResponse(Response.json({ error: 'Invalid request' }, { status: 400 }), await safeCorsHeaders(match.route.middlewares, request));
684
+ return annotateResponse(applyFrameworkHeadersToResponse(Response.json({ error: 'Invalid request' }, { status: 400 }), await safeCorsHeaders(match.route.middlewares, request)));
531
685
  }
532
686
  if (error instanceof HttpInputValidationError) {
533
- return applyFrameworkHeadersToResponse(Response.json({ error: 'Validation failed' }, { status: 400 }), await safeCorsHeaders(match.route.middlewares, request));
687
+ return annotateResponse(applyFrameworkHeadersToResponse(Response.json({ error: 'Validation failed' }, { status: 400 }), await safeCorsHeaders(match.route.middlewares, request)));
534
688
  }
689
+ lease.fail?.(error);
535
690
  try {
536
691
  const mapped = await mapDeclaredError(match.route.definition, error);
537
692
  if (mapped) {
@@ -539,13 +694,13 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
539
694
  if (finalized.streamControl) {
540
695
  adoptResponseStream(finalized.streamControl);
541
696
  }
542
- return applyFrameworkHeadersToResponse(finalized.response, await safeCorsHeaders(match.route.middlewares, request));
697
+ return annotateResponse(applyFrameworkHeadersToResponse(finalized.response, await safeCorsHeaders(match.route.middlewares, request)));
543
698
  }
544
699
  }
545
700
  catch {
546
701
  // 内部のmapping/finalization errorをclientへ公開しない。
547
702
  }
548
- return applyFrameworkHeadersToResponse(Response.json({ error: 'Internal Server Error' }, { status: 500 }), await safeCorsHeaders(match.route.middlewares, request));
703
+ return annotateResponse(applyFrameworkHeadersToResponse(Response.json({ error: 'Internal Server Error' }, { status: 500 }), await safeCorsHeaders(match.route.middlewares, request)));
549
704
  }
550
705
  finally {
551
706
  if (!executionOwnedByStream)
@@ -557,7 +712,7 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
557
712
  }
558
713
  function findRuntimeHttpRoute(routes, method, pathname) {
559
714
  for (const candidate of routes) {
560
- if (candidate.route.method !== method)
715
+ if (candidate.route.method !== method && candidate.route.method !== '*')
561
716
  continue;
562
717
  const params = matchHttpPath(candidate.route.segments, pathname);
563
718
  if (!params)
@@ -595,7 +750,16 @@ async function createHttpContext(request, url, rawParams, route, signal) {
595
750
  ? await validateRequestHeaders(definition.headers, definition.body !== undefined, request.headers)
596
751
  : Object.fromEntries(request.headers.entries());
597
752
  const body = definition?.body ? request.body : undefined;
598
- const response = Object.fromEntries(Object.keys(route.definition.responses).map((name) => [
753
+ const response = createHttpResponseHelpers(route.definition.responses);
754
+ return {
755
+ request,
756
+ input: { params, query, headers, body },
757
+ response,
758
+ signal,
759
+ };
760
+ }
761
+ function createHttpResponseHelpers(responses) {
762
+ return Object.fromEntries(Object.keys(responses).map((name) => [
599
763
  name,
600
764
  (value = {}) => ({
601
765
  kind: 'http-result',
@@ -604,12 +768,40 @@ async function createHttpContext(request, url, rawParams, route, signal) {
604
768
  ...(value.headers === undefined ? {} : { headers: value.headers }),
605
769
  }),
606
770
  ]));
607
- return {
608
- request,
609
- input: { params, query, headers, body },
610
- response,
611
- signal,
612
- };
771
+ }
772
+ async function replayHttpHandler(applicationRuntime, handler, match, input, state) {
773
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
774
+ throw new TypeError('LUTRE_DEVTOOLS_HTTP_HANDLER_INPUT_REQUIRED');
775
+ }
776
+ const lease = applicationRuntime.beginExecution({
777
+ executionId: match.executionId,
778
+ executionKind: 'http.handler',
779
+ graphNodeId: match.executionId,
780
+ name: `${match.executionId}.${match.route.name}`,
781
+ });
782
+ const invoke = () => handler({
783
+ input,
784
+ response: createHttpResponseHelpers(match.route.definition.responses),
785
+ signal: lease.signal,
786
+ state,
787
+ });
788
+ let result;
789
+ let hasResult = false;
790
+ try {
791
+ result = await (lease.run ? lease.run(invoke) : invoke());
792
+ hasResult = true;
793
+ return result;
794
+ }
795
+ catch (error) {
796
+ lease.fail?.(error);
797
+ throw error;
798
+ }
799
+ finally {
800
+ if (hasResult)
801
+ lease.complete(result);
802
+ else
803
+ lease.complete();
804
+ }
613
805
  }
614
806
  async function finalizeHttpResult(route, result, signal, completeExecution) {
615
807
  const response = route.responses[result.response];
@@ -4,7 +4,8 @@ export { cors } from './cors.js';
4
4
  export type { CorsLayerDescriptor, CorsOptions, CorsOrigin } from './cors.js';
5
5
  export { basicAuth, bearerAuth } from './auth.js';
6
6
  export type { BasicAuthContext, BasicAuthCredentials, BasicAuthDefinition, BasicAuthUnauthorized, BasicAuthRuntime, BearerAuthContext, BearerAuthDefinition, BearerAuthRuntime, BearerAuthUnauthorized, HttpAuthenticationFailure, } from './auth.js';
7
- export { bindHttpServer, defineHttpContract, defineHttpImplementation, defineHttpMiddleware, httpError, executionHttp as http, httpExecutionExtension, HTTP_SERVER, validate, } from './extension.js';
8
- export type { HttpContract, HttpContractBranchDefinition, HttpContractNodeDefinition, HttpContractRouteTree, HttpExecutionContext, HttpExecutionDefinition, HttpExecutionRequestDefinition, HttpExecutionResponseDefinition, HttpExecutionResult, HttpExecutionRouteDefinition, HttpErrorMapping, HttpErrorMatcher, HttpExtensionRuntime, HttpHandlers, HttpHeaderValue, HttpHeaders, HttpHostApi, HttpImplementationDefinition, HttpMiddleware, HttpMiddlewareContext, HttpResponseHeadersDefinition, HttpResponseResult, HttpResponseHeadersWithDefaults, HttpServerDriver, HttpValidationMiddleware, HttpValidationPart, ResolvedHttpContractRoutes, } from './extension.js';
7
+ export { bindHttpServer, defineHttpContract, defineHttpImplementation, defineHttpMiddleware, defineHttpRaw, httpError, executionHttp as http, httpExecutionExtension, isHttpContract, HTTP_SERVER, validate, } from './extension.js';
8
+ export type { HttpRawDefinition, HttpRawRouteDefinition, HttpRawExecutionContext, HttpRawExecutionDefinition, HttpContract, HttpContractBranchDefinition, HttpContractNodeDefinition, HttpContractRouteTree, HttpExecutionContext, HttpExecutionDefinition, HttpExecutionRequestDefinition, HttpExecutionResponseDefinition, HttpExecutionResult, HttpExecutionRouteDefinition, HttpErrorMapping, HttpErrorMatcher, HttpExtensionRuntime, HttpHandlers, HttpHeaderValue, HttpHeaders, HttpHostApi, HttpImplementationDefinition, HttpMiddleware, HttpMiddlewareContext, HttpResponseHeadersDefinition, HttpResponseResult, HttpResponseHeadersWithDefaults, HttpServerDriver, HttpValidationMiddleware, HttpValidationPart, ResolvedHttpContractRoutes, } from './extension.js';
9
9
  export { compareHttpPathSpecificity, HttpPathDecodeError, matchHttpPath, normalizeHttpPath, parseHttpPath, } from './path.js';
10
10
  export type { HttpPathSegment, PathParamNames } from './path.js';
11
+ export type { HttpRequestHeadDefinition } from './request-head.js';
@@ -1,5 +1,5 @@
1
1
  export { createHttpClient, fetchHttpTransport, HttpClientResponseError, } from './client.js';
2
2
  export { cors } from './cors.js';
3
3
  export { basicAuth, bearerAuth } from './auth.js';
4
- export { bindHttpServer, defineHttpContract, defineHttpImplementation, defineHttpMiddleware, httpError, executionHttp as http, httpExecutionExtension, HTTP_SERVER, validate, } from './extension.js';
4
+ export { bindHttpServer, defineHttpContract, defineHttpImplementation, defineHttpMiddleware, defineHttpRaw, httpError, executionHttp as http, httpExecutionExtension, isHttpContract, HTTP_SERVER, validate, } from './extension.js';
5
5
  export { compareHttpPathSpecificity, HttpPathDecodeError, matchHttpPath, normalizeHttpPath, parseHttpPath, } from './path.js';
@@ -1,5 +1,5 @@
1
1
  import { type ApplicationModel } from '../core/index.js';
2
- import { type HttpExecutionResponseDefinition, type HttpExecutionRouteDefinition } from './extension.js';
2
+ import { type HttpContract, type HttpExecutionResponseDefinition, type HttpExecutionRouteDefinition } from './extension.js';
3
3
  export interface OpenApiInfo {
4
4
  readonly title: string;
5
5
  readonly version: string;
@@ -37,7 +37,6 @@ export interface HttpOperationTarget {
37
37
  };
38
38
  readonly procedure: string;
39
39
  }
40
- export declare function generateOpenApi(source: ApplicationModel | {
41
- readonly model: ApplicationModel;
42
- }, options: GenerateOpenApiOptions): OpenApiDocument;
40
+ export type OpenApiSource = ApplicationModel | HttpContract;
41
+ export declare function generateOpenApi(source: OpenApiSource, options: GenerateOpenApiOptions): OpenApiDocument;
43
42
  export {};
@@ -1,5 +1,7 @@
1
1
  import { supportsJsonSchema, assertValidApplicationModel, } from '../core/index.js';
2
- import { httpExecutionExtension, } from './extension.js';
2
+ import { httpExecutionExtension, isHttpContract, } from './extension.js';
3
+ import { findHttpRouteConflicts } from './route-validation.js';
4
+ import { match } from 'ts-pattern';
3
5
  const FIXED_METHODS = new Set([
4
6
  'get',
5
7
  'put',
@@ -12,24 +14,11 @@ const FIXED_METHODS = new Set([
12
14
  'query',
13
15
  ]);
14
16
  export function generateOpenApi(source, options) {
15
- const model = 'model' in source ? source.model : source;
16
- assertValidApplicationModel(model);
17
17
  const registry = new SchemaRegistry();
18
18
  const paths = {};
19
19
  const operationIds = new Set();
20
- const http = model.extensions.get(httpExecutionExtension);
21
- for (const execution of http?.executions ?? []) {
22
- for (const route of execution.compiled.routes) {
23
- const target = {
24
- procedure: route.name,
25
- definition: {
26
- ...route.definition,
27
- method: route.method,
28
- path: route.path,
29
- },
30
- };
31
- attachOperation(paths, target.definition, createOperation(target, registry, operationIds, options.operationId));
32
- }
20
+ for (const target of httpOperationTargets(source)) {
21
+ attachOperation(paths, target.definition, createOperation(target, registry, operationIds, options.operationId));
33
22
  }
34
23
  const schemas = registry.components();
35
24
  return {
@@ -40,6 +29,37 @@ export function generateOpenApi(source, options) {
40
29
  ...(Object.keys(schemas).length === 0 ? {} : { components: { schemas } }),
41
30
  };
42
31
  }
32
+ function httpOperationTargets(source) {
33
+ if (source.kind === 'http-contract') {
34
+ if (!isHttpContract(source)) {
35
+ throw openApiError('LUTRE_OPENAPI_SOURCE_001', 'HttpContract source must be created by http.contract().');
36
+ }
37
+ const routes = Object.entries(source.routes).map(([procedure, definition]) => ({ procedure, definition }));
38
+ const [conflict] = findHttpRouteConflicts(routes.map(({ definition }) => definition));
39
+ if (conflict) {
40
+ throw openApiError('LUTRE_OPENAPI_OPERATION_001', `Duplicate OpenAPI operation: ${conflict.route.method.toUpperCase()} ${conflict.route.path} conflicts with ${conflict.existing.method.toUpperCase()} ${conflict.existing.path}`);
41
+ }
42
+ return routes.map(({ procedure, definition }) => ({
43
+ procedure,
44
+ definition: {
45
+ ...definition,
46
+ method: definition.method.toUpperCase(),
47
+ },
48
+ }));
49
+ }
50
+ assertValidApplicationModel(source);
51
+ const http = source.extensions.get(httpExecutionExtension);
52
+ return (http?.executions ?? []).flatMap((execution) => execution.compiled.routes
53
+ .filter((route) => !route.raw)
54
+ .map((route) => ({
55
+ procedure: route.name,
56
+ definition: {
57
+ ...route.definition,
58
+ method: route.method,
59
+ path: route.path,
60
+ },
61
+ })));
62
+ }
43
63
  function createOperation(target, registry, operationIds, resolveOperationId) {
44
64
  const definition = target.definition;
45
65
  const operationId = resolveOperationId?.({
@@ -244,18 +264,17 @@ function mergeResponseHeaders(entries, registry, target) {
244
264
  ]));
245
265
  }
246
266
  function responseHeadersSchema(headers) {
247
- if (isStandardSchema(headers))
248
- return headers;
249
- if (isResponseHeadersWithDefaults(headers))
250
- return headers.schema;
251
- return undefined;
267
+ return match(headers)
268
+ .when(isStandardSchema, (schema) => schema)
269
+ .when(isResponseHeadersWithDefaults, (definition) => definition.schema)
270
+ .otherwise(() => undefined);
252
271
  }
253
272
  function responseHeadersDefaults(headers) {
254
- if (headers === undefined || isStandardSchema(headers))
255
- return undefined;
256
- if (isResponseHeadersWithDefaults(headers))
257
- return headers.defaults;
258
- return headers;
273
+ return match(headers)
274
+ .with(undefined, () => undefined)
275
+ .when(isStandardSchema, () => undefined)
276
+ .when(isResponseHeadersWithDefaults, (definition) => definition.defaults)
277
+ .otherwise((definition) => definition);
259
278
  }
260
279
  function isStandardSchema(value) {
261
280
  return typeof value === 'object' && value !== null && '~standard' in value;
@@ -0,0 +1,6 @@
1
+ import type { StandardSchemaV1 } from '../core/index.js';
2
+ export interface HttpRequestHeadDefinition {
3
+ readonly params?: Readonly<Record<string, StandardSchemaV1>>;
4
+ readonly query?: StandardSchemaV1;
5
+ readonly headers?: StandardSchemaV1;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ export interface HttpRouteDescriptor {
2
+ readonly method: string;
3
+ readonly path: string;
4
+ }
5
+ export interface HttpRouteConflict<TRoute extends HttpRouteDescriptor> {
6
+ readonly existing: TRoute;
7
+ readonly route: TRoute;
8
+ }
9
+ export declare function findHttpRouteConflicts<TRoute extends HttpRouteDescriptor>(routes: readonly TRoute[]): readonly HttpRouteConflict<TRoute>[];
@@ -0,0 +1,18 @@
1
+ import { createHttpDispatchKey, parseHttpPath } from './path.js';
2
+ export function findHttpRouteConflicts(routes) {
3
+ const dispatches = new Map();
4
+ const conflicts = [];
5
+ for (const route of routes) {
6
+ const dispatch = createHttpDispatchKey(route.method, parseHttpPath(route.path));
7
+ const existing = dispatches.get(dispatch) ??
8
+ [...dispatches.values()].find((candidate) => (route.method === '*' || candidate.method === '*') &&
9
+ createHttpDispatchKey('*', parseHttpPath(candidate.path)) ===
10
+ createHttpDispatchKey('*', parseHttpPath(route.path)));
11
+ if (existing) {
12
+ conflicts.push({ existing, route });
13
+ continue;
14
+ }
15
+ dispatches.set(dispatch, route);
16
+ }
17
+ return conflicts;
18
+ }