@loutrejs/loutre 0.9.2 → 0.10.1

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 (145) hide show
  1. package/dist/adapters/aws-lambda.d.ts +1 -1
  2. package/dist/adapters/aws-lambda.js +8 -2
  3. package/dist/adapters/bun.d.ts +3 -2
  4. package/dist/adapters/bun.js +12 -6
  5. package/dist/adapters/cloudflare-workers.d.ts +1 -1
  6. package/dist/adapters/cloudflare-workers.js +2 -1
  7. package/dist/adapters/electron.d.ts +1 -1
  8. package/dist/adapters/electron.js +8 -51
  9. package/dist/adapters/server-transports.js +2 -2
  10. package/dist/adapters/websocket-native.js +4 -1
  11. package/dist/application/definition.d.ts +4 -7
  12. package/dist/application/definition.js +3 -1
  13. package/dist/application/kernel.d.ts +0 -2
  14. package/dist/application/kernel.js +1 -3
  15. package/dist/core/args.d.ts +1 -0
  16. package/dist/core/args.js +2 -1
  17. package/dist/core/env.d.ts +1 -0
  18. package/dist/core/env.js +2 -1
  19. package/dist/core/extension.d.ts +23 -13
  20. package/dist/core/extension.js +3 -1
  21. package/dist/core/generic-layer.d.ts +9 -2
  22. package/dist/core/generic-layer.js +2 -1
  23. package/dist/core/identity.d.ts +18 -0
  24. package/dist/core/identity.js +5 -0
  25. package/dist/core/index.d.ts +13 -5
  26. package/dist/core/index.js +7 -1
  27. package/dist/core/injection.d.ts +2 -1
  28. package/dist/core/instrumentation.d.ts +7 -5
  29. package/dist/core/lifecycle.d.ts +4 -0
  30. package/dist/core/logger-identity.d.ts +4 -0
  31. package/dist/core/logger-identity.js +9 -0
  32. package/dist/core/logger-internal.d.ts +5 -0
  33. package/dist/core/logger-internal.js +27 -0
  34. package/dist/core/logger-module.d.ts +5 -0
  35. package/dist/core/logger-module.js +8 -0
  36. package/dist/core/logger.d.ts +42 -0
  37. package/dist/core/logger.js +221 -0
  38. package/dist/core/model/context.d.ts +6 -3
  39. package/dist/core/model/context.js +17 -17
  40. package/dist/core/model/identity.d.ts +19 -0
  41. package/dist/core/model/identity.js +8 -0
  42. package/dist/core/model/lifecycle.d.ts +3 -2
  43. package/dist/core/model/lifecycle.js +7 -3
  44. package/dist/core/model/logger.d.ts +3 -0
  45. package/dist/core/model/logger.js +59 -0
  46. package/dist/core/model/modules.d.ts +4 -1
  47. package/dist/core/model/modules.js +6 -4
  48. package/dist/core/model/providers.d.ts +1 -1
  49. package/dist/core/model/providers.js +8 -8
  50. package/dist/core/model/types.d.ts +25 -18
  51. package/dist/core/model/validation.js +3 -2
  52. package/dist/core/model/visibility.js +7 -2
  53. package/dist/core/model.js +15 -7
  54. package/dist/core/module.d.ts +7 -7
  55. package/dist/core/module.js +0 -3
  56. package/dist/core/runtime-input.d.ts +2 -1
  57. package/dist/core/source-location.d.ts +14 -2
  58. package/dist/core/source-location.js +8 -3
  59. package/dist/core/token-internal.js +3 -1
  60. package/dist/core/token.d.ts +4 -2
  61. package/dist/core/token.js +3 -0
  62. package/dist/core/units.d.ts +22 -0
  63. package/dist/core/units.js +14 -0
  64. package/dist/devtools/capsule.d.ts +17 -13
  65. package/dist/devtools/capsule.js +14 -10
  66. package/dist/devtools/decoder.d.ts +15 -0
  67. package/dist/devtools/decoder.js +179 -0
  68. package/dist/devtools/identity-internal.d.ts +6 -0
  69. package/dist/devtools/identity-internal.js +5 -0
  70. package/dist/devtools/identity.d.ts +21 -0
  71. package/dist/devtools/identity.js +7 -0
  72. package/dist/devtools/index.d.ts +3 -0
  73. package/dist/devtools/index.js +2 -0
  74. package/dist/devtools/inspector.d.ts +18 -16
  75. package/dist/devtools/observer.d.ts +32 -28
  76. package/dist/devtools/runtime.js +34 -30
  77. package/dist/devtools/values.js +3 -5
  78. package/dist/generated/version.d.ts +1 -1
  79. package/dist/generated/version.js +1 -1
  80. package/dist/graph/model.d.ts +25 -14
  81. package/dist/http/auth.d.ts +4 -2
  82. package/dist/http/client.d.ts +6 -3
  83. package/dist/http/client.js +8 -9
  84. package/dist/http/cors-internal.d.ts +7 -5
  85. package/dist/http/cors-internal.js +8 -6
  86. package/dist/http/extension.d.ts +60 -21
  87. package/dist/http/extension.js +91 -20
  88. package/dist/http/identity.d.ts +20 -0
  89. package/dist/http/identity.js +10 -0
  90. package/dist/http/index.d.ts +6 -2
  91. package/dist/http/index.js +3 -1
  92. package/dist/http/openapi-identity.d.ts +9 -0
  93. package/dist/http/openapi-identity.js +4 -0
  94. package/dist/http/openapi.d.ts +1 -1
  95. package/dist/http/openapi.js +10 -7
  96. package/dist/http/path.d.ts +8 -5
  97. package/dist/http/route-validation.js +2 -1
  98. package/dist/http/runtime/request.d.ts +3 -2
  99. package/dist/http/runtime/request.js +1 -4
  100. package/dist/http/websocket-upgrade.d.ts +2 -0
  101. package/dist/http/websocket-upgrade.js +4 -0
  102. package/dist/internal/brand.d.ts +8 -0
  103. package/dist/internal/brand.js +1 -0
  104. package/dist/message-port/extension.d.ts +4 -2
  105. package/dist/message-port/extension.js +15 -8
  106. package/dist/message-port/identity.d.ts +9 -0
  107. package/dist/message-port/identity.js +16 -0
  108. package/dist/message-port/index.d.ts +2 -0
  109. package/dist/message-port/index.js +1 -0
  110. package/dist/message-port/transport.d.ts +4 -2
  111. package/dist/message-port/transport.js +6 -1
  112. package/dist/runtime/async-iterator-lifecycle.d.ts +2 -1
  113. package/dist/runtime/async-iterator-lifecycle.js +2 -1
  114. package/dist/runtime/capability.d.ts +6 -2
  115. package/dist/runtime/capability.js +3 -1
  116. package/dist/runtime/di.d.ts +1 -3
  117. package/dist/runtime/di.js +62 -35
  118. package/dist/runtime/error.d.ts +3 -1
  119. package/dist/runtime/index.d.ts +6 -2
  120. package/dist/runtime/index.js +3 -1
  121. package/dist/runtime/kernel.d.ts +0 -2
  122. package/dist/runtime/kernel.js +68 -30
  123. package/dist/runtime/logger-source.d.ts +4 -0
  124. package/dist/runtime/logger-source.js +47 -0
  125. package/dist/runtime/logger.d.ts +2 -38
  126. package/dist/runtime/logger.js +1 -209
  127. package/dist/runtime/node-capabilities.d.ts +1 -1
  128. package/dist/runtime/node-capabilities.js +2 -1
  129. package/dist/runtime/server-port.d.ts +5 -1
  130. package/dist/runtime/server-port.js +2 -1
  131. package/dist/runtime/shutdown-hooks.d.ts +1 -1
  132. package/dist/runtime/shutdown-hooks.js +5 -4
  133. package/dist/tasks/extension.d.ts +18 -16
  134. package/dist/tasks/extension.js +28 -23
  135. package/dist/tasks/identity.d.ts +9 -0
  136. package/dist/tasks/identity.js +4 -0
  137. package/dist/tasks/index.d.ts +1 -0
  138. package/dist/tasks/runtime/cron.d.ts +6 -2
  139. package/dist/tasks/runtime/cron.js +9 -0
  140. package/dist/websocket/extension.d.ts +19 -9
  141. package/dist/websocket/extension.js +103 -30
  142. package/dist/websocket/identity.d.ts +9 -0
  143. package/dist/websocket/identity.js +4 -0
  144. package/dist/websocket/index.d.ts +1 -0
  145. package/package.json +1 -1
@@ -1,3 +1,7 @@
1
+ import { dependencyConsumerId } from '../core/identity.js';
2
+ import { epochNow, expiresAt, epochMilliseconds, } from '../core/units.js';
3
+ import { normalizeHttpMethod, requestPathname, httpProcedureName, } from './identity.js';
4
+ import { syntheticExecutionId } from '../core/identity.js';
1
5
  import { collectInjectedDependencies, getSourceLocation, inheritSourceLocation, composeLayers, defineExecution, defineExecutionExtension, defineLayer, runInInjectionContext, runtimeCapability, SchemaValidationError, validateSchema, } from '../core/index.js';
2
6
  import { createCorsActualResponseHeaders, createCorsPreflightResponseHeaders, } from './cors-internal.js';
3
7
  import { assertValidHttpMethod, compareHttpPathSpecificity, createHttpDispatchKey, HttpPathDecodeError, matchHttpPath, parseHttpPath, } from './path.js';
@@ -6,8 +10,15 @@ import { IngressGate } from '../runtime/ingress-gate.js';
6
10
  import { AsyncIteratorCleanupDeadlineError, AsyncIteratorLifecycle, } from '../runtime/async-iterator-lifecycle.js';
7
11
  import { decodeBody, decodeQuery, HttpInputDecodeError, HttpInputValidationError, HttpUnsupportedMediaTypeError, validatedContentType, validateRequestHeaders, } from './runtime/request.js';
8
12
  import { applyFrameworkHeadersToResponse, applyFrameworkResponseHeaders, applyResponseHeaders, isResponseHeadersWithDefaults, isStandardSchema, mergeResponseHeaders, responseHeadersDefaults, responseHeadersSchema, validateResponseHeaders, } from './runtime/response.js';
13
+ import { isWebSocketUpgrade } from './websocket-upgrade.js';
9
14
  import { match as patternMatch } from 'ts-pattern';
10
15
  export const HTTP_SERVER = runtimeCapability('http.server');
16
+ const httpContractRawRouteBrand = Symbol.for('loutre.http-contract-raw-route');
17
+ export function isHttpRawContractRoute(value) {
18
+ return (typeof value === 'object' &&
19
+ value !== null &&
20
+ value[httpContractRawRouteBrand] === true);
21
+ }
11
22
  const httpContractBrand = Symbol.for('loutre.http-contract');
12
23
  export function isHttpContract(value) {
13
24
  if (typeof value !== 'object' || value === null)
@@ -29,18 +40,22 @@ export const httpExecutionExtension = defineExecutionExtension({
29
40
  const raw = 'route' in definition;
30
41
  const routes = raw
31
42
  ? [compileRawRoute(definition.route)]
32
- : Object.entries(definition.contract.routes).map(([name, route]) => compileHttpRoute(name, route, getSourceLocation(definition.contract)));
43
+ : Object.entries(definition.contract.routes).flatMap(([name, route]) => route.raw === true
44
+ ? []
45
+ : [
46
+ compileHttpRoute(name, route, getSourceLocation(definition.contract)),
47
+ ]);
33
48
  const id = definition.name || `${context.moduleId}.http.${context.definitionIndex}`;
34
49
  const dependencies = new Set(collectInjectedDependencies({
35
50
  kind: 'execution',
36
- id: `http:${id}`,
51
+ id: dependencyConsumerId(`http:${id}`),
37
52
  name: id,
38
53
  }, () => definition.factory()));
39
54
  routes.forEach((route) => {
40
55
  route.middlewares.forEach((middleware, index) => {
41
56
  for (const dependency of collectInjectedDependencies({
42
57
  kind: 'layer',
43
- id: `http:${id}:${route.name}:${index}`,
58
+ id: dependencyConsumerId(`http:${id}:${route.name}:${index}`),
44
59
  name: middleware.name,
45
60
  }, () => middleware.factory())) {
46
61
  dependencies.add(dependency);
@@ -123,7 +138,9 @@ export function defineHttpContract(routes) {
123
138
  const resolvedRoutes = resolveHttpContractRoutes(routes);
124
139
  const canonicalRoutes = Object.fromEntries(Object.entries(resolvedRoutes).map(([name, route]) => [
125
140
  name,
126
- compileHttpRoute(name, route).definition,
141
+ route.raw === true
142
+ ? snapshotHttpContractRawRoute(route)
143
+ : compileHttpRoute(name, route).definition,
127
144
  ]));
128
145
  return Object.freeze({
129
146
  kind: 'http-contract',
@@ -142,7 +159,28 @@ function resolveHttpContractRoutes(tree) {
142
159
  if (resolved[name]) {
143
160
  throw new TypeError(`Duplicate nested HTTP route name: ${name}`);
144
161
  }
145
- assertNoInheritedResponseCollision(name, inheritedResponses, node.responses);
162
+ if (node.raw === true) {
163
+ if (node.responses !== undefined ||
164
+ node.request !== undefined ||
165
+ Object.keys(inheritedResponses).length > 0)
166
+ throw new TypeError('Raw routes cannot declare or inherit HTTP request schemas or responses.');
167
+ resolved[name] = inheritSourceLocation({
168
+ ...node,
169
+ path: joinHttpPath(pathPrefix, node.path),
170
+ ...(inheritedMiddlewares.length === 0 && !node.middlewares
171
+ ? {}
172
+ : {
173
+ middlewares: [
174
+ ...inheritedMiddlewares,
175
+ ...(node.middlewares ?? []),
176
+ ],
177
+ }),
178
+ }, node);
179
+ continue;
180
+ }
181
+ else {
182
+ assertNoInheritedResponseCollision(name, inheritedResponses, node.responses);
183
+ }
146
184
  resolved[name] = inheritSourceLocation({
147
185
  ...node,
148
186
  path: joinHttpPath(pathPrefix, node.path),
@@ -208,6 +246,7 @@ export const validate = Object.freeze({
208
246
  body: bodyValidation,
209
247
  });
210
248
  export const executionHttp = Object.freeze({
249
+ isWebSocketUpgrade,
211
250
  raw: defineHttpRaw,
212
251
  contract: defineHttpContract,
213
252
  implementation: defineHttpImplementation,
@@ -226,40 +265,55 @@ export function withHttpFrameworkHeaders(result, headers) {
226
265
  [httpFrameworkHeaders]: { ...current, ...headers },
227
266
  };
228
267
  }
268
+ function snapshotHttpContractRawRoute(route) {
269
+ if (route.method !== '*')
270
+ assertValidHttpMethod(route.method);
271
+ parseHttpPath(route.path);
272
+ return inheritSourceLocation(Object.freeze({
273
+ ...route,
274
+ method: route.method.toUpperCase(),
275
+ [httpContractRawRouteBrand]: true,
276
+ ...(route.middlewares === undefined
277
+ ? {}
278
+ : { middlewares: Object.freeze([...route.middlewares]) }),
279
+ }), route);
280
+ }
229
281
  function compileRawRoute(route) {
230
282
  if (route.method !== '*')
231
283
  assertValidHttpMethod(route.method);
232
284
  const segments = parseHttpPath(route.path);
285
+ const source = getSourceLocation(route);
233
286
  return Object.freeze({
234
- name: 'raw',
235
- method: route.method.toUpperCase(),
287
+ name: httpProcedureName('raw'),
288
+ method: route.method === '*' ? '*' : normalizeHttpMethod(route.method),
236
289
  path: route.path,
237
290
  segments,
238
- dispatch: createHttpDispatchKey(route.method, segments),
291
+ dispatch: createHttpDispatchKey(normalizeHttpMethod(route.method), segments),
239
292
  definition: Object.freeze({
240
293
  method: route.method,
241
294
  path: route.path,
242
295
  responses: Object.freeze({}),
243
296
  }),
244
297
  middlewares: Object.freeze([...(route.middlewares ?? [])]),
298
+ ...(source === undefined ? {} : { source }),
245
299
  raw: true,
246
300
  ...(route.protocol === undefined ? {} : { protocol: route.protocol }),
247
301
  });
248
302
  }
249
303
  function compileHttpRoute(name, route, routeSourceFallback) {
250
304
  if (route.method === '*')
251
- throw new TypeError('method: *はhttp.raw()でのみ利用できます。');
305
+ throw new TypeError('The wildcard method (*) is only supported by http.raw().');
252
306
  assertValidHttpMethod(route.method);
253
307
  const segments = parseHttpPath(route.path);
254
308
  assertValidHttpRouteDefinition(route, segments);
255
309
  const definition = snapshotHttpRouteDefinition(route);
256
310
  const routeSource = getSourceLocation(route) ?? routeSourceFallback;
257
311
  return Object.freeze({
258
- name,
259
- method: route.method.toUpperCase(),
312
+ name: httpProcedureName(name),
313
+ method: route.method === '*' ? '*' : normalizeHttpMethod(route.method),
260
314
  path: route.path,
261
315
  segments,
262
- dispatch: createHttpDispatchKey(route.method, segments),
316
+ dispatch: createHttpDispatchKey(normalizeHttpMethod(route.method), segments),
263
317
  definition,
264
318
  middlewares: compileHttpMiddlewares(definition),
265
319
  ...(routeSource === undefined ? {} : { source: routeSource }),
@@ -278,6 +332,7 @@ function snapshotHttpRouteDefinition(route) {
278
332
  name,
279
333
  Object.freeze({
280
334
  ...response,
335
+ status: response.status,
281
336
  ...(response.headers === undefined
282
337
  ? {}
283
338
  : { headers: snapshotHttpResponseHeaders(response.headers) }),
@@ -411,7 +466,7 @@ function createHttpExtensionRuntime(bindings, applicationRuntime) {
411
466
  handlers.set(execution.id, runInInjectionContext({
412
467
  consumer: {
413
468
  kind: 'execution',
414
- id: `http:${execution.id}`,
469
+ id: dependencyConsumerId(`http:${execution.id}`),
415
470
  name: execution.id,
416
471
  },
417
472
  resolve: (token, consumer) => resolve(token, { consumer }),
@@ -427,7 +482,7 @@ function createHttpExtensionRuntime(bindings, applicationRuntime) {
427
482
  return {
428
483
  async drain({ timeoutMs }) {
429
484
  ingress.stopAccepting();
430
- const deadline = Date.now() + timeoutMs;
485
+ const deadline = expiresAt(epochNow(), timeoutMs);
431
486
  await ingress.waitForIdle();
432
487
  const reason = new Error('LUTRE_HTTP_SERVER_STREAM_DRAIN: Application is shutting down.');
433
488
  const results = await Promise.allSettled([...activeResponseStreams].map((stream) => stream.abort(reason, deadline)));
@@ -440,6 +495,13 @@ function createHttpExtensionRuntime(bindings, applicationRuntime) {
440
495
  if (!ingress.isAccepting) {
441
496
  return Response.json({ error: 'Service Unavailable' }, { status: 503 });
442
497
  }
498
+ if (request.headers.has('upgrade') ||
499
+ request.headers
500
+ .get('connection')
501
+ ?.split(',')
502
+ .some((value) => value.trim().toLowerCase() === 'upgrade')) {
503
+ return new Response('Upgrade requests must use the WebSocket entry point.', { status: 400 });
504
+ }
443
505
  const url = new URL(request.url);
444
506
  const method = request.method.toUpperCase();
445
507
  if (isCorsPreflightRequest(request)) {
@@ -504,7 +566,7 @@ function createHttpExtensionRuntime(bindings, applicationRuntime) {
504
566
  let lease;
505
567
  try {
506
568
  lease = applicationRuntime.beginExecution({
507
- executionId: 'http.not-found',
569
+ executionId: syntheticExecutionId('http.not-found'),
508
570
  executionKind: 'http.request',
509
571
  name: `${method} ${url.pathname}`,
510
572
  });
@@ -674,7 +736,7 @@ function createHttpExtensionRuntime(bindings, applicationRuntime) {
674
736
  return complete(result);
675
737
  }
676
738
  if (match.route.raw)
677
- throw new TypeError('Raw handlerとmiddlewareはResponseを返す必要があります。');
739
+ throw new TypeError('Raw handlers and middleware must return a Response.');
678
740
  const finalized = await finalizeHttpResult(match.route.definition, result, lease.signal, finishExecution);
679
741
  if (finalized.streamControl)
680
742
  adoptResponseStream(finalized.streamControl);
@@ -715,7 +777,7 @@ function findRuntimeHttpRoute(routes, method, pathname) {
715
777
  for (const candidate of routes) {
716
778
  if (candidate.route.method !== method && candidate.route.method !== '*')
717
779
  continue;
718
- const params = matchHttpPath(candidate.route.segments, pathname);
780
+ const params = matchHttpPath(candidate.route.segments, requestPathname(pathname));
719
781
  if (!params)
720
782
  continue;
721
783
  return { ...candidate, params };
@@ -770,10 +832,14 @@ function createHttpResponseHelpers(responses) {
770
832
  }),
771
833
  ]));
772
834
  }
835
+ function isHttpInputRecord(value) {
836
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
837
+ }
773
838
  async function replayHttpHandler(applicationRuntime, handler, match, input, state) {
774
- if (typeof input !== 'object' || input === null || Array.isArray(input)) {
839
+ if (!isHttpInputRecord(input) || !isHttpInputRecord(input.params)) {
775
840
  throw new TypeError('LUTRE_DEVTOOLS_HTTP_HANDLER_INPUT_REQUIRED');
776
841
  }
842
+ const params = input.params;
777
843
  const lease = applicationRuntime.beginExecution({
778
844
  executionId: match.executionId,
779
845
  executionKind: 'http.handler',
@@ -781,7 +847,12 @@ async function replayHttpHandler(applicationRuntime, handler, match, input, stat
781
847
  name: `${match.executionId}.${match.route.name}`,
782
848
  });
783
849
  const invoke = () => handler({
784
- input,
850
+ input: {
851
+ params,
852
+ query: input.query,
853
+ headers: input.headers,
854
+ body: input.body,
855
+ },
785
856
  response: createHttpResponseHelpers(match.route.definition.responses),
786
857
  signal: lease.signal,
787
858
  state,
@@ -1016,7 +1087,7 @@ function createServerSentEventStream(source, schema, signal, completeExecution)
1016
1087
  const stop = (reason, errorStream, deadline) => {
1017
1088
  if (cleanupAttempt) {
1018
1089
  if (deadline !== undefined) {
1019
- cleanupDeadline = Math.min(cleanupDeadline ?? deadline, deadline);
1090
+ cleanupDeadline = epochMilliseconds(Math.min(cleanupDeadline ?? deadline, deadline));
1020
1091
  }
1021
1092
  return cleanupAttempt;
1022
1093
  }
@@ -0,0 +1,20 @@
1
+ import type { Brand } from '../internal/brand.js';
2
+ export type HttpPathTemplate = Brand<string, 'http.path-template'>;
3
+ export type HttpNormalizedPath = Brand<string, 'http.normalized-path'>;
4
+ export type RequestPathname = Brand<string, 'http.request-pathname'>;
5
+ export type HttpMethod = Brand<string, 'http.method'>;
6
+ export type HttpDispatchKey = Brand<string, 'http.dispatch-key'>;
7
+ export type HttpProcedureName = Brand<string, 'http.procedure'>;
8
+ export type HttpHeaderName = Brand<string, 'http.header-name'>;
9
+ export type CorsOriginValue = Brand<string, 'http.cors-origin'>;
10
+ export type ContentTypeValue = Brand<string, 'http.content-type'>;
11
+ export type NormalizedMediaType = Brand<string, 'http.media-type'>;
12
+ export type HttpResponseStatus = Brand<number, 'http.response-status'>;
13
+ export type HttpBaseUrl = Brand<string, 'http.base-url'>;
14
+ export declare const normalizeHttpMethod: (value: string) => HttpMethod;
15
+ export declare const requestPathname: (value: string) => RequestPathname;
16
+ export declare const httpProcedureName: (value: string) => HttpProcedureName;
17
+ export declare const httpHeaderName: (value: string) => HttpHeaderName;
18
+ export declare const corsOriginValue: (value: string) => CorsOriginValue;
19
+ export declare const httpBaseUrl: (value: string | URL) => HttpBaseUrl;
20
+ export declare function normalizeMediaType(value: string | null | undefined): NormalizedMediaType | undefined;
@@ -0,0 +1,10 @@
1
+ export const normalizeHttpMethod = (value) => value.toUpperCase();
2
+ export const requestPathname = (value) => value;
3
+ export const httpProcedureName = (value) => value;
4
+ export const httpHeaderName = (value) => value;
5
+ export const corsOriginValue = (value) => value;
6
+ export const httpBaseUrl = (value) => String(value);
7
+ export function normalizeMediaType(value) {
8
+ const normalized = value?.split(';', 1)[0]?.trim().toLowerCase();
9
+ return normalized ? normalized : undefined;
10
+ }
@@ -1,11 +1,15 @@
1
1
  export { createHttpClient, fetchHttpTransport, HttpClientResponseError, } from './client.js';
2
2
  export type { FetchHttpTransportOptions, HttpClient, HttpClientRequest, HttpClientResponse, HttpClientTransport, HttpClientTransportRequest, HttpClientTransportResponse, } from './client.js';
3
3
  export { cors } from './cors.js';
4
+ export { isWebSocketUpgrade } from './websocket-upgrade.js';
4
5
  export type { CorsLayerDescriptor, CorsOptions, CorsOrigin } from './cors.js';
5
6
  export { basicAuth, bearerAuth } from './auth.js';
6
7
  export type { BasicAuthContext, BasicAuthCredentials, BasicAuthDefinition, BasicAuthUnauthorized, BasicAuthRuntime, BearerAuthContext, BearerAuthDefinition, BearerAuthRuntime, BearerAuthUnauthorized, HttpAuthenticationFailure, } from './auth.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';
8
+ export { bindHttpServer, defineHttpContract, defineHttpImplementation, defineHttpMiddleware, defineHttpRaw, httpError, executionHttp as http, httpExecutionExtension, isHttpContract, isHttpRawContractRoute, HTTP_SERVER, validate, } from './extension.js';
9
+ export type { HttpRawDefinition, HttpRawRouteDefinition, HttpRawExecutionContext, HttpRawExecutionDefinition, HttpContract, HttpContractBranchDefinition, HttpContractNodeDefinition, HttpContractRouteTree, HttpContractRouteDefinition, HttpContractRawRouteDefinition, HttpContractRawRoute, 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
10
  export { compareHttpPathSpecificity, HttpPathDecodeError, matchHttpPath, normalizeHttpPath, parseHttpPath, } from './path.js';
10
11
  export type { HttpPathSegment, PathParamNames } from './path.js';
11
12
  export type { HttpRequestHeadDefinition } from './request-head.js';
13
+ export * from './identity.js';
14
+ export type { ParsedHttpPath } from './path.js';
15
+ export type { BearerCredential } from './auth.js';
@@ -1,5 +1,7 @@
1
1
  export { createHttpClient, fetchHttpTransport, HttpClientResponseError, } from './client.js';
2
2
  export { cors } from './cors.js';
3
+ export { isWebSocketUpgrade } from './websocket-upgrade.js';
3
4
  export { basicAuth, bearerAuth } from './auth.js';
4
- export { bindHttpServer, defineHttpContract, defineHttpImplementation, defineHttpMiddleware, defineHttpRaw, httpError, executionHttp as http, httpExecutionExtension, isHttpContract, HTTP_SERVER, validate, } from './extension.js';
5
+ export { bindHttpServer, defineHttpContract, defineHttpImplementation, defineHttpMiddleware, defineHttpRaw, httpError, executionHttp as http, httpExecutionExtension, isHttpContract, isHttpRawContractRoute, HTTP_SERVER, validate, } from './extension.js';
5
6
  export { compareHttpPathSpecificity, HttpPathDecodeError, matchHttpPath, normalizeHttpPath, parseHttpPath, } from './path.js';
7
+ export * from './identity.js';
@@ -0,0 +1,9 @@
1
+ import type { Brand } from '../internal/brand.js';
2
+ export type OpenApiOperationId = Brand<string, 'openapi.operation-id'>;
3
+ export type OpenApiSchemaName = Brand<string, 'openapi.schema-name'>;
4
+ export type JsonSchemaReference = Brand<string, 'openapi.schema-reference'>;
5
+ export type SchemaComparisonKey = Brand<string, 'openapi.schema-comparison-key'>;
6
+ export declare const openApiOperationId: (value: string) => OpenApiOperationId;
7
+ export declare const openApiSchemaName: (value: string) => OpenApiSchemaName;
8
+ export declare const jsonSchemaReference: (value: string) => JsonSchemaReference;
9
+ export declare const schemaComparisonKey: (value: unknown) => SchemaComparisonKey;
@@ -0,0 +1,4 @@
1
+ export const openApiOperationId = (value) => value;
2
+ export const openApiSchemaName = (value) => value;
3
+ export const jsonSchemaReference = (value) => value;
4
+ export const schemaComparisonKey = (value) => JSON.stringify(value);
@@ -1,3 +1,4 @@
1
+ export type { OpenApiOperationId, OpenApiSchemaName, JsonSchemaReference, } from './openapi-identity.js';
1
2
  import { type ApplicationModel } from '../core/index.js';
2
3
  import { type HttpContract, type HttpExecutionResponseDefinition, type HttpExecutionRouteDefinition } from './extension.js';
3
4
  export interface OpenApiInfo {
@@ -39,4 +40,3 @@ export interface HttpOperationTarget {
39
40
  }
40
41
  export type OpenApiSource = ApplicationModel | HttpContract;
41
42
  export declare function generateOpenApi(source: OpenApiSource, options: GenerateOpenApiOptions): OpenApiDocument;
42
- export {};
@@ -1,3 +1,4 @@
1
+ import { openApiOperationId, openApiSchemaName, jsonSchemaReference, schemaComparisonKey, } from './openapi-identity.js';
1
2
  import { supportsJsonSchema, assertValidApplicationModel, } from '../core/index.js';
2
3
  import { httpExecutionExtension, isHttpContract, } from './extension.js';
3
4
  import { findHttpRouteConflicts } from './route-validation.js';
@@ -34,7 +35,7 @@ function httpOperationTargets(source) {
34
35
  if (!isHttpContract(source)) {
35
36
  throw openApiError('LUTRE_OPENAPI_SOURCE_001', 'HttpContract source must be created by http.contract().');
36
37
  }
37
- const routes = Object.entries(source.routes).map(([procedure, definition]) => ({ procedure, definition }));
38
+ const routes = Object.entries(source.routes).flatMap(([procedure, definition]) => definition.raw === true ? [] : [{ procedure, definition }]);
38
39
  const [conflict] = findHttpRouteConflicts(routes.map(({ definition }) => definition));
39
40
  if (conflict) {
40
41
  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}`);
@@ -71,10 +72,10 @@ function createOperation(target, registry, operationIds, resolveOperationId) {
71
72
  if (operationId.length === 0) {
72
73
  throw openApiError('LUTRE_OPENAPI_OPERATION_ID_002', `operationId must not be empty for ${describeTarget(target)}`);
73
74
  }
74
- if (operationIds.has(operationId)) {
75
+ if (operationIds.has(openApiOperationId(operationId))) {
75
76
  throw openApiError('LUTRE_OPENAPI_OPERATION_ID_001', `Duplicate operationId: ${operationId}`);
76
77
  }
77
- operationIds.add(operationId);
78
+ operationIds.add(openApiOperationId(operationId));
78
79
  }
79
80
  const parameters = [];
80
81
  const request = definition.request;
@@ -387,7 +388,9 @@ class SchemaRegistry {
387
388
  const materialized = {
388
389
  name,
389
390
  schema: rebased,
390
- reference: { $ref: `#/components/schemas/${escapeJsonPointer(name)}` },
391
+ reference: {
392
+ $ref: jsonSchemaReference(`#/components/schemas/${escapeJsonPointer(name)}`),
393
+ },
391
394
  };
392
395
  const directions = this.bySchema.get(schema) ?? {};
393
396
  directions[direction] = materialized;
@@ -407,11 +410,11 @@ function sanitizeComponentName(value) {
407
410
  }
408
411
  function uniqueComponentName(preferred, schemas) {
409
412
  if (!(preferred in schemas))
410
- return preferred;
413
+ return openApiSchemaName(preferred);
411
414
  let index = 2;
412
415
  while (`${preferred}_${index}` in schemas)
413
416
  index += 1;
414
- return `${preferred}_${index}`;
417
+ return openApiSchemaName(`${preferred}_${index}`);
415
418
  }
416
419
  function escapeJsonPointer(value) {
417
420
  return value.replace(/~/g, '~0').replace(/\//g, '~1');
@@ -423,7 +426,7 @@ function dedupeJsonSchemas(schemas) {
423
426
  const seen = new Set();
424
427
  const result = [];
425
428
  for (const schema of schemas) {
426
- const serialized = JSON.stringify(schema);
429
+ const serialized = schemaComparisonKey(schema);
427
430
  if (seen.has(serialized))
428
431
  continue;
429
432
  seen.add(serialized);
@@ -1,3 +1,5 @@
1
+ import type { Brand } from '../internal/brand.js';
2
+ import type { HttpNormalizedPath, HttpDispatchKey, HttpMethod, RequestPathname } from './identity.js';
1
3
  export type HttpPathSegment = {
2
4
  readonly kind: 'static';
3
5
  readonly value: string;
@@ -6,12 +8,13 @@ export type HttpPathSegment = {
6
8
  readonly name: string;
7
9
  };
8
10
  export type PathParamNames<TPath extends string> = TPath extends `${string}{${infer TName}}${infer TRest}` ? TName | PathParamNames<TRest> : never;
11
+ export type ParsedHttpPath = Brand<readonly HttpPathSegment[], 'http.parsed-path'>;
9
12
  export declare function assertValidHttpMethod(method: string): void;
10
- export declare function parseHttpPath(path: string): readonly HttpPathSegment[];
11
- export declare function normalizeHttpPath(segments: readonly HttpPathSegment[]): string;
12
- export declare function createHttpDispatchKey(method: string, segments: readonly HttpPathSegment[]): string;
13
- export declare function matchHttpPath(segments: readonly HttpPathSegment[], pathname: string): Record<string, string> | undefined;
14
- export declare function compareHttpPathSpecificity(left: readonly HttpPathSegment[], right: readonly HttpPathSegment[]): number;
13
+ export declare function parseHttpPath(path: string): ParsedHttpPath;
14
+ export declare function normalizeHttpPath(segments: ParsedHttpPath): HttpNormalizedPath;
15
+ export declare function createHttpDispatchKey(method: HttpMethod | '*', segments: ParsedHttpPath): HttpDispatchKey;
16
+ export declare function matchHttpPath(segments: ParsedHttpPath, pathname: RequestPathname): Record<string, string> | undefined;
17
+ export declare function compareHttpPathSpecificity(left: ParsedHttpPath, right: ParsedHttpPath): number;
15
18
  export declare class HttpPathDecodeError extends Error {
16
19
  readonly cause: unknown;
17
20
  constructor(cause: unknown);
@@ -1,9 +1,10 @@
1
+ import { normalizeHttpMethod } from './identity.js';
1
2
  import { createHttpDispatchKey, parseHttpPath } from './path.js';
2
3
  export function findHttpRouteConflicts(routes) {
3
4
  const dispatches = new Map();
4
5
  const conflicts = [];
5
6
  for (const route of routes) {
6
- const dispatch = createHttpDispatchKey(route.method, parseHttpPath(route.path));
7
+ const dispatch = createHttpDispatchKey(normalizeHttpMethod(route.method), parseHttpPath(route.path));
7
8
  const existing = dispatches.get(dispatch) ??
8
9
  [...dispatches.values()].find((candidate) => (route.method === '*' || candidate.method === '*') &&
9
10
  createHttpDispatchKey('*', parseHttpPath(candidate.path)) ===
@@ -1,8 +1,9 @@
1
+ import { type ContentTypeValue } from '../identity.js';
1
2
  import { type StandardSchemaV1 } from '../../core/index.js';
2
3
  export declare function decodeQuery(searchParams: URLSearchParams): Readonly<Record<string, string | string[]>>;
3
4
  export declare function validateRequestHeaders(schema: StandardSchemaV1, bodyDeclared: boolean, headers: Headers): Promise<unknown>;
4
- export declare function validatedContentType(headers: unknown): string;
5
- export declare function decodeBody(request: Request, contentType: string): Promise<unknown>;
5
+ export declare function validatedContentType(headers: unknown): ContentTypeValue;
6
+ export declare function decodeBody(request: Request, contentType: ContentTypeValue): Promise<unknown>;
6
7
  export declare class HttpInputDecodeError extends Error {
7
8
  readonly cause: unknown;
8
9
  constructor(cause: unknown);
@@ -1,3 +1,4 @@
1
+ import { normalizeMediaType } from '../identity.js';
1
2
  import { SchemaValidationError, validateSchema, } from '../../core/index.js';
2
3
  export function decodeQuery(searchParams) {
3
4
  const query = {};
@@ -79,10 +80,6 @@ function hasContentTypeIssue(error) {
79
80
  return key === 'content-type';
80
81
  });
81
82
  }
82
- function normalizeMediaType(value) {
83
- const normalized = value?.split(';', 1)[0]?.trim().toLowerCase();
84
- return normalized ? normalized : undefined;
85
- }
86
83
  export class HttpInputDecodeError extends Error {
87
84
  cause;
88
85
  constructor(cause) {
@@ -0,0 +1,2 @@
1
+ /** WebSocketへのUpgrade要求かを判定する。handshake全体の検証は行わない。 */
2
+ export declare function isWebSocketUpgrade(request: Request): boolean;
@@ -0,0 +1,4 @@
1
+ /** WebSocketへのUpgrade要求かを判定する。handshake全体の検証は行わない。 */
2
+ export function isWebSocketUpgrade(request) {
3
+ return request.headers.get('upgrade')?.toLowerCase() === 'websocket';
4
+ }
@@ -0,0 +1,8 @@
1
+ export type Brand<T, Tag extends string> = T & {
2
+ readonly '~loutre.brand': {
3
+ readonly [Key in Tag]: true;
4
+ };
5
+ };
6
+ export interface Descriptor<Tag extends string> {
7
+ readonly '~loutre.descriptor': Tag;
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,5 @@
1
+ import { type MessagePortProcedureName } from './identity.js';
2
+ import type { Descriptor } from '../internal/brand.js';
1
3
  import { type ExecutionDefinition, type ExecutionExtension, type ExecutionExtensionDrainContext, type SchemaOutput, type StandardSchemaV1 } from '../core/index.js';
2
4
  export interface MessagePortServerStreamResponseDefinition {
3
5
  readonly body: StandardSchemaV1;
@@ -8,7 +10,7 @@ export interface MessagePortRouteDefinition {
8
10
  readonly input?: StandardSchemaV1;
9
11
  readonly responses: Readonly<Record<string, MessagePortResponseDefinition>>;
10
12
  }
11
- export interface MessagePortContract<TRoutes extends Readonly<Record<string, MessagePortRouteDefinition>> = Readonly<Record<string, MessagePortRouteDefinition>>> {
13
+ export interface MessagePortContract<TRoutes extends Readonly<Record<string, MessagePortRouteDefinition>> = Readonly<Record<string, MessagePortRouteDefinition>>> extends Descriptor<'message-port-contract'> {
12
14
  readonly kind: 'message-port-contract';
13
15
  readonly routes: TRoutes;
14
16
  }
@@ -39,7 +41,7 @@ interface CompiledMessagePortExecution {
39
41
  readonly factory: () => Readonly<Record<string, (context: MessagePortContext) => MessagePortResult | Promise<MessagePortResult>>>;
40
42
  }
41
43
  export interface MessagePortExtensionRuntime {
42
- invoke(method: string, input?: unknown): Promise<MessagePortResult>;
44
+ invoke(method: MessagePortProcedureName, input?: unknown): Promise<MessagePortResult>;
43
45
  drain(context: ExecutionExtensionDrainContext): Promise<void>;
44
46
  }
45
47
  export interface MessagePortHostApi {
@@ -1,3 +1,6 @@
1
+ import { dependencyConsumerId } from '../core/identity.js';
2
+ import { epochNow, expiresAt, epochMilliseconds, } from '../core/units.js';
3
+ import { messagePortProcedureName, } from './identity.js';
1
4
  import { collectInjectedDependencies, defineExecution, defineExecutionExtension, runInInjectionContext, validateSchema, } from '../core/index.js';
2
5
  import { IngressGate } from '../runtime/ingress-gate.js';
3
6
  import { AsyncIteratorCleanupDeadlineError, AsyncIteratorLifecycle, } from '../runtime/async-iterator-lifecycle.js';
@@ -13,7 +16,7 @@ export const messagePortExtension = defineExecutionExtension({
13
16
  executionKind: 'message-port.invocation',
14
17
  dependencies: collectInjectedDependencies({
15
18
  kind: 'execution',
16
- id: `message-port:${definition.name || `${context.moduleId}.message-port.${context.definitionIndex}`}`,
19
+ id: dependencyConsumerId(`message-port:${definition.name || `${context.moduleId}.message-port.${context.definitionIndex}`}`),
17
20
  name: definition.name ||
18
21
  `${context.moduleId}.message-port.${context.definitionIndex}`,
19
22
  }, () => definition.factory()),
@@ -26,7 +29,8 @@ export const messagePortExtension = defineExecutionExtension({
26
29
  },
27
30
  validate({ executions }) {
28
31
  const methods = new Set();
29
- return executions.flatMap((execution) => Object.keys(execution.compiled.routes).flatMap((method) => {
32
+ return executions.flatMap((execution) => Object.keys(execution.compiled.routes).flatMap((name) => {
33
+ const method = messagePortProcedureName(name);
30
34
  if (!methods.has(method)) {
31
35
  methods.add(method);
32
36
  return [];
@@ -49,12 +53,15 @@ export const messagePortExtension = defineExecutionExtension({
49
53
  host: {
50
54
  namespace: 'messagePort',
51
55
  create: ({ runtime }) => ({
52
- invoke: (method, input) => runtime.invoke(method, input),
56
+ invoke: (method, input) => runtime.invoke(messagePortProcedureName(method), input),
53
57
  }),
54
58
  },
55
59
  });
56
60
  export function defineMessagePortContract(routes) {
57
- return Object.freeze({ kind: 'message-port-contract', routes });
61
+ return Object.freeze({
62
+ kind: 'message-port-contract',
63
+ routes,
64
+ });
58
65
  }
59
66
  export function defineMessagePortImplementation(definition) {
60
67
  return defineExecution(messagePortExtension, {
@@ -90,7 +97,7 @@ function createMessagePortRuntime(bindings, applicationRuntime) {
90
97
  const handlers = runInInjectionContext({
91
98
  consumer: {
92
99
  kind: 'execution',
93
- id: `message-port:${execution.id}`,
100
+ id: dependencyConsumerId(`message-port:${execution.id}`),
94
101
  name: execution.id,
95
102
  },
96
103
  resolve: (token, consumer) => resolve(token, { consumer }),
@@ -98,7 +105,7 @@ function createMessagePortRuntime(bindings, applicationRuntime) {
98
105
  for (const [method, definition] of Object.entries(execution.compiled.routes)) {
99
106
  const handler = handlers[method];
100
107
  if (handler) {
101
- routes.set(method, {
108
+ routes.set(messagePortProcedureName(method), {
102
109
  executionId: execution.id,
103
110
  route: definition,
104
111
  handler,
@@ -178,7 +185,7 @@ function createMessagePortRuntime(bindings, applicationRuntime) {
178
185
  },
179
186
  async drain({ timeoutMs }) {
180
187
  ingress.stopAccepting();
181
- const deadline = Date.now() + timeoutMs;
188
+ const deadline = expiresAt(epochNow(), timeoutMs);
182
189
  await ingress.waitForIdle();
183
190
  const reason = new Error('LUTRE_MESSAGE_PORT_DRAINING');
184
191
  const results = await Promise.allSettled([...activeStreams].map((stream) => stream.abort(reason, deadline)));
@@ -230,7 +237,7 @@ function createLeasedMessagePortStream(schema, source, lease, activeStreams) {
230
237
  const startIteratorCleanup = (reason, deadline) => {
231
238
  if (iteratorCleanup) {
232
239
  if (deadline !== undefined) {
233
- iteratorCleanupDeadline = Math.min(iteratorCleanupDeadline ?? deadline, deadline);
240
+ iteratorCleanupDeadline = epochMilliseconds(Math.min(iteratorCleanupDeadline ?? deadline, deadline));
234
241
  }
235
242
  return iteratorCleanup;
236
243
  }
@@ -0,0 +1,9 @@
1
+ import type { Brand } from '../internal/brand.js';
2
+ export type MessagePortRequestId = Brand<string, 'message-port.request-id'>;
3
+ export type MessagePortProcedureName = Brand<string, 'message-port.procedure'>;
4
+ export declare const messagePortProcedureName: (value: string) => MessagePortProcedureName;
5
+ export declare function decodeMessagePortRequest(value: unknown): {
6
+ readonly id: MessagePortRequestId;
7
+ readonly procedure: MessagePortProcedureName;
8
+ readonly input?: unknown;
9
+ } | undefined;
@@ -0,0 +1,16 @@
1
+ export const messagePortProcedureName = (value) => value;
2
+ export function decodeMessagePortRequest(value) {
3
+ if (typeof value !== 'object' ||
4
+ value === null ||
5
+ Array.isArray(value) ||
6
+ !('id' in value) ||
7
+ !('procedure' in value) ||
8
+ typeof value.id !== 'string' ||
9
+ typeof value.procedure !== 'string')
10
+ return undefined;
11
+ return {
12
+ id: value.id,
13
+ procedure: messagePortProcedureName(value.procedure),
14
+ ...('input' in value ? { input: value.input } : {}),
15
+ };
16
+ }