@loutrejs/loutre 0.1.0 → 0.3.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 (43) hide show
  1. package/README.md +69 -0
  2. package/dist/adapters/aws-lambda.js +2 -0
  3. package/dist/adapters/bun.d.ts +3 -1
  4. package/dist/adapters/bun.js +90 -33
  5. package/dist/adapters/cloudflare-workers.js +2 -0
  6. package/dist/adapters/deno.d.ts +3 -1
  7. package/dist/adapters/deno.js +99 -33
  8. package/dist/adapters/electron.js +2 -0
  9. package/dist/application/openapi.d.ts +6 -0
  10. package/dist/application/openapi.js +14 -19
  11. package/dist/core/contract.d.ts +47 -7
  12. package/dist/core/contract.js +59 -10
  13. package/dist/generated/version.d.ts +1 -0
  14. package/dist/generated/version.js +2 -0
  15. package/dist/graph/compiler.d.ts +4 -0
  16. package/dist/graph/compiler.js +228 -0
  17. package/dist/graph/graph.js +65 -40
  18. package/dist/graph/index.d.ts +2 -1
  19. package/dist/graph/index.js +2 -1
  20. package/dist/graph/ir.d.ts +24 -8
  21. package/dist/http/application.js +5 -5
  22. package/dist/http/basic-auth.js +1 -1
  23. package/dist/http/client.d.ts +72 -0
  24. package/dist/http/client.js +258 -0
  25. package/dist/http/cors.js +4 -4
  26. package/dist/http/definitions.d.ts +13 -5
  27. package/dist/http/definitions.js +6 -2
  28. package/dist/http/index.d.ts +1 -0
  29. package/dist/http/index.js +1 -0
  30. package/dist/message-port/index.d.ts +12 -4
  31. package/dist/message-port/index.js +13 -6
  32. package/dist/presentation.d.ts +33 -0
  33. package/dist/presentation.js +233 -0
  34. package/dist/runtime/capability.js +1 -1
  35. package/dist/runtime/di.js +2 -2
  36. package/dist/runtime/engine.d.ts +3 -0
  37. package/dist/runtime/engine.js +38 -0
  38. package/dist/runtime/index.d.ts +2 -0
  39. package/dist/runtime/index.js +2 -0
  40. package/dist/runtime/pipeline.js +13 -13
  41. package/dist/runtime/server-url.d.ts +1 -0
  42. package/dist/runtime/server-url.js +5 -0
  43. package/package.json +5 -1
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @loutrejs/loutre
2
+
3
+ LoutreのCore packageです。Application Definition、Contract、DI、Pipeline、Task、Trigger、Environment、Arguments、Runtime bindingなど、Loutre Applicationを構成するpublic APIを提供します。
4
+
5
+ > Loutreは現在v0.xです。Public APIには破壊的変更が入る可能性があります。
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install @loutrejs/loutre
11
+ ```
12
+
13
+ 新しいApplicationを始める場合はinitializerも利用できます。
14
+
15
+ ```sh
16
+ npm create loutre@latest my-app
17
+ ```
18
+
19
+ ## Application model
20
+
21
+ LoutreはHTTP endpointだけを中心にせず、Applicationが実行する処理を同じmodelへ載せます。
22
+
23
+ - **Contract / Implementation** — protocolごとのinterfaceと実装
24
+ - **Module / DI** — dependencyとApplication構成
25
+ - **Pipeline / Layer** — requestやexecution contextの処理
26
+ - **Task / Trigger / Queue** — 明示実行、schedule、queue consumer
27
+ - **Environment / Arguments** — Hostから渡されるtyped input
28
+ - **Application Graph** — Application構造をRuntimeとToolingで共有
29
+
30
+ ## Example
31
+
32
+ ```ts
33
+ import { defineApplication, task } from '@loutrejs/loutre'
34
+
35
+ export const hello = task<void, string>({
36
+ name: 'hello',
37
+ factory: () => () => 'Hello, Loutre!',
38
+ })
39
+
40
+ export default defineApplication({
41
+ modules: [],
42
+ tasks: [hello],
43
+ })
44
+ ```
45
+
46
+ ## Entry points
47
+
48
+ 主なsubpath exportは次のとおりです。
49
+
50
+ | Entry point | Role |
51
+ | ------------------------------- | ------------------------ |
52
+ | `@loutrejs/loutre/http` | HTTP Contract / Pipeline |
53
+ | `@loutrejs/loutre/host` | Application bootstrap |
54
+ | `@loutrejs/loutre/binding` | Host / Queue binding |
55
+ | `@loutrejs/loutre/runtime` | Runtime capability |
56
+ | `@loutrejs/loutre/graph` | Application Graph |
57
+ | `@loutrejs/loutre/openapi` | OpenAPI generation |
58
+ | `@loutrejs/loutre/presentation` | startup presentation |
59
+ | `@loutrejs/loutre/message-port` | MessagePort protocol |
60
+
61
+ ## Documentation
62
+
63
+ - [Getting Started](https://github.com/come25136/loutrejs/blob/main/docs/getting-started.md)
64
+ - [Architecture](https://github.com/come25136/loutrejs/blob/main/docs/architecture.md)
65
+ - [Examples](https://github.com/come25136/loutrejs/tree/main/examples)
66
+
67
+ ## License
68
+
69
+ [MIT](https://github.com/come25136/loutrejs/blob/main/LICENSE)
@@ -1,4 +1,5 @@
1
1
  import { binding, } from '../application/index.js';
2
+ import { assertRuntimeEngine } from '../runtime/engine.js';
2
3
  export const awsLambdaRuntime = {
3
4
  runtime: 'aws-lambda',
4
5
  capabilities: new Set([
@@ -12,6 +13,7 @@ export const awsLambdaRuntime = {
12
13
  bind,
13
14
  };
14
15
  function bind(options) {
16
+ assertRuntimeEngine('aws-lambda');
15
17
  const invocation = binding.invocation({
16
18
  application: options.application,
17
19
  environment: 'environment' in options ? options.environment : process.env,
@@ -3,12 +3,14 @@ type IsAny<TValue> = 0 extends 1 & TValue ? true : false;
3
3
  type HttpApplication<TDefinition extends ApplicationDefinition> = IsAny<TDefinition> extends true ? TDefinition : HasHttp<TDefinition> extends true ? TDefinition : never;
4
4
  export type BunServeOptions<TDefinition extends ApplicationDefinition> = {
5
5
  readonly application: HttpApplication<TDefinition>;
6
- readonly port: number;
6
+ readonly port?: number;
7
7
  readonly hostname?: string;
8
+ readonly shutdownHooks?: boolean;
8
9
  readonly environment?: unknown;
9
10
  } & BootstrapArguments<TDefinition>;
10
11
  export interface BunServeHandle<TDefinition extends ApplicationDefinition = ApplicationDefinition> {
11
12
  readonly application: HostBindingApplication<TDefinition>;
13
+ readonly port: number;
12
14
  close(signal?: string): Promise<void>;
13
15
  }
14
16
  export declare const bunRuntime: {
@@ -1,4 +1,7 @@
1
1
  import { binding, } from '../application/index.js';
2
+ import { LOUTRE_VERSION, detectPresentationTerminal, startStartupPresentation, } from '../presentation.js';
3
+ import { assertRuntimeEngine } from '../runtime/engine.js';
4
+ import { serverUrl } from '../runtime/server-url.js';
2
5
  export const bunRuntime = {
3
6
  runtime: 'bun',
4
7
  capabilities: new Set([
@@ -15,13 +18,20 @@ export const bunRuntime = {
15
18
  serve,
16
19
  };
17
20
  async function serve(options) {
21
+ assertRuntimeEngine('bun');
18
22
  const bun = bunGlobal();
19
23
  if (!bun?.serve) {
20
24
  throw new Error('LUTRE_BUN_UNAVAILABLE: Bun.serve() is not available.');
21
25
  }
26
+ const startedAt = performance.now();
27
+ const environment = bun.env ?? {};
28
+ const presentation = startStartupPresentation({ version: LOUTRE_VERSION }, {
29
+ terminal: detectPresentationTerminal(process.stdout, environment),
30
+ write: (value) => console.log(value),
31
+ });
22
32
  const host = binding.host({
23
33
  application: options.application,
24
- environment: 'environment' in options ? options.environment : (bun.env ?? undefined),
34
+ environment: 'environment' in options ? options.environment : environment,
25
35
  ...('arguments' in options ? { arguments: options.arguments } : {}),
26
36
  });
27
37
  const http = 'http' in host ? host.http : undefined;
@@ -33,41 +43,88 @@ async function serve(options) {
33
43
  if ('triggers' in host.application)
34
44
  await host.application.triggers.start();
35
45
  let server;
36
- try {
37
- server = bun.serve({
38
- port: options.port,
39
- ...(options.hostname === undefined ? {} : { hostname: options.hostname }),
40
- fetch: createBunFetchDriver(http),
41
- });
42
- }
43
- catch (error) {
44
- await host.application.close().catch(() => undefined);
45
- throw error;
46
+ const requestedPort = options.port;
47
+ let port = requestedPort ?? 3000;
48
+ while (true) {
49
+ try {
50
+ server = bun.serve({
51
+ port,
52
+ ...(options.hostname === undefined
53
+ ? {}
54
+ : { hostname: options.hostname }),
55
+ fetch: createBunFetchDriver(http),
56
+ });
57
+ break;
58
+ }
59
+ catch (error) {
60
+ if (requestedPort !== undefined || !canRetryOnNextPort(error, port)) {
61
+ await host.application.close().catch(() => undefined);
62
+ throw error;
63
+ }
64
+ port += 1;
65
+ }
46
66
  }
67
+ presentation.ready({
68
+ server: serverUrl(options.hostname, port),
69
+ runtime: `Bun ${bun.version ?? 'unknown'}`,
70
+ environment: environment.NODE_ENV ?? 'development',
71
+ startupDurationMs: performance.now() - startedAt,
72
+ });
47
73
  let closed = false;
48
- return {
49
- application: host.application,
50
- async close(signal) {
51
- if (closed)
52
- return;
53
- closed = true;
54
- const errors = [];
55
- try {
56
- await server.stop(true);
57
- }
58
- catch (error) {
59
- errors.push(error);
60
- }
61
- try {
62
- await host.application.close(signal);
63
- }
64
- catch (error) {
65
- errors.push(error);
66
- }
67
- if (errors.length > 0)
68
- throw new AggregateError(errors, 'Bun runtime shutdown failed');
69
- },
74
+ const closeRuntime = async (signal) => {
75
+ if (closed)
76
+ return;
77
+ closed = true;
78
+ const errors = [];
79
+ try {
80
+ await server.stop(true);
81
+ }
82
+ catch (error) {
83
+ errors.push(error);
84
+ }
85
+ try {
86
+ await host.application.close(signal);
87
+ }
88
+ catch (error) {
89
+ errors.push(error);
90
+ }
91
+ if (errors.length > 0)
92
+ throw new AggregateError(errors, 'Bun runtime shutdown failed');
93
+ };
94
+ const removeShutdownHooks = options.shutdownHooks === false
95
+ ? undefined
96
+ : registerBunShutdownHooks(closeRuntime);
97
+ const close = async (signal) => {
98
+ removeShutdownHooks?.();
99
+ await closeRuntime(signal);
100
+ };
101
+ return { application: host.application, port, close };
102
+ }
103
+ function registerBunShutdownHooks(close) {
104
+ const handlers = new Map();
105
+ const remove = () => {
106
+ for (const [signal, handler] of handlers)
107
+ process.off(signal, handler);
108
+ handlers.clear();
70
109
  };
110
+ for (const signal of ['SIGINT', 'SIGTERM']) {
111
+ const handler = () => {
112
+ remove();
113
+ void close(signal);
114
+ };
115
+ handlers.set(signal, handler);
116
+ process.once(signal, handler);
117
+ }
118
+ return remove;
119
+ }
120
+ function canRetryOnNextPort(error, port) {
121
+ return port < 65_535 && isAddressInUseError(error);
122
+ }
123
+ function isAddressInUseError(error) {
124
+ return (typeof error === 'object' &&
125
+ error !== null &&
126
+ 'code' in error &&
127
+ error.code === 'EADDRINUSE');
71
128
  }
72
129
  function createBunFetchDriver(application) {
73
130
  let initialization;
@@ -1,4 +1,5 @@
1
1
  import { binding, } from '../application/index.js';
2
+ import { assertRuntimeEngine } from '../runtime/engine.js';
2
3
  export const cloudflareWorkersRuntime = {
3
4
  runtime: 'cloudflare-workers',
4
5
  compatibilityDateMinimum: '2026-08-04',
@@ -15,6 +16,7 @@ export const cloudflareWorkersRuntime = {
15
16
  bind,
16
17
  };
17
18
  function bind(options) {
19
+ assertRuntimeEngine('cloudflare-workers');
18
20
  let invocation;
19
21
  let fetch;
20
22
  const resolve = (environment) => {
@@ -6,8 +6,9 @@ export type DenoRuntimeOptions<TDefinition extends ApplicationDefinition> = {
6
6
  readonly environment?: unknown;
7
7
  } & BootstrapArguments<TDefinition>;
8
8
  export type DenoServeOptions<TDefinition extends ApplicationDefinition> = DenoRuntimeOptions<TDefinition> & {
9
- readonly port: number;
9
+ readonly port?: number;
10
10
  readonly hostname?: string;
11
+ readonly shutdownHooks?: boolean;
11
12
  };
12
13
  export interface DenoBinding {
13
14
  fetch(request: Request): Promise<Response>;
@@ -15,6 +16,7 @@ export interface DenoBinding {
15
16
  }
16
17
  export interface DenoServeHandle<TDefinition extends ApplicationDefinition = ApplicationDefinition> {
17
18
  readonly application: HostBindingApplication<TDefinition>;
19
+ readonly port: number;
18
20
  close(signal?: string): Promise<void>;
19
21
  }
20
22
  export declare const denoRuntime: {
@@ -1,4 +1,7 @@
1
1
  import { binding, } from '../application/index.js';
2
+ import { LOUTRE_VERSION, startStartupPresentation } from '../presentation.js';
3
+ import { assertRuntimeEngine } from '../runtime/engine.js';
4
+ import { serverUrl } from '../runtime/server-url.js';
2
5
  export const denoRuntime = {
3
6
  runtime: 'deno',
4
7
  capabilities: new Set([
@@ -16,6 +19,7 @@ export const denoRuntime = {
16
19
  serve,
17
20
  };
18
21
  function bind(options) {
22
+ assertRuntimeEngine('deno');
19
23
  const invocation = binding.invocation(bindingOptions(options, denoEnvironment()));
20
24
  const http = 'http' in invocation
21
25
  ? invocation.http
@@ -31,10 +35,20 @@ function bind(options) {
31
35
  };
32
36
  }
33
37
  async function serve(options) {
38
+ assertRuntimeEngine('deno');
34
39
  const deno = denoGlobal();
35
40
  if (!deno?.serve) {
36
41
  throw new Error('LUTRE_DENO_UNAVAILABLE: Deno.serve() is not available.');
37
42
  }
43
+ const startedAt = performance.now();
44
+ const isTTY = deno.stdout?.isTerminal?.() === true;
45
+ const presentation = startStartupPresentation({ version: LOUTRE_VERSION }, {
46
+ terminal: {
47
+ isTTY,
48
+ color: isTTY && deno.env?.get?.('NO_COLOR') === undefined,
49
+ },
50
+ write: (value) => console.log(value),
51
+ });
38
52
  const host = binding.host(bindingOptions(options, denoEnvironment()));
39
53
  const http = 'http' in host ? host.http : undefined;
40
54
  if (!http) {
@@ -45,42 +59,94 @@ async function serve(options) {
45
59
  if ('triggers' in host.application)
46
60
  await host.application.triggers.start();
47
61
  let server;
48
- try {
49
- server = deno.serve({
50
- port: options.port,
51
- ...(options.hostname === undefined
52
- ? {}
53
- : { hostname: options.hostname }),
54
- }, createDenoFetchDriver(http));
55
- }
56
- catch (error) {
57
- await host.application.close().catch(() => undefined);
58
- throw error;
62
+ const requestedPort = options.port;
63
+ let port = requestedPort ?? 3000;
64
+ while (true) {
65
+ try {
66
+ server = deno.serve({
67
+ port,
68
+ ...(options.hostname === undefined
69
+ ? {}
70
+ : { hostname: options.hostname }),
71
+ onListen: () => undefined,
72
+ }, createDenoFetchDriver(http));
73
+ break;
74
+ }
75
+ catch (error) {
76
+ if (requestedPort !== undefined || !canRetryOnNextPort(error, port)) {
77
+ await host.application.close().catch(() => undefined);
78
+ throw error;
79
+ }
80
+ port += 1;
81
+ }
59
82
  }
83
+ presentation.ready({
84
+ server: serverUrl(options.hostname, port),
85
+ runtime: `Deno ${deno.version?.deno ?? 'unknown'}`,
86
+ environment: deno.env?.get?.('DENO_ENV') ??
87
+ deno.env?.get?.('NODE_ENV') ??
88
+ 'development',
89
+ startupDurationMs: performance.now() - startedAt,
90
+ });
60
91
  let closed = false;
61
- return {
62
- application: host.application,
63
- async close(signal) {
64
- if (closed)
65
- return;
66
- closed = true;
67
- const errors = [];
68
- try {
69
- await server.shutdown();
70
- }
71
- catch (error) {
72
- errors.push(error);
73
- }
74
- try {
75
- await host.application.close(signal);
76
- }
77
- catch (error) {
78
- errors.push(error);
79
- }
80
- if (errors.length > 0)
81
- throw new AggregateError(errors, 'Deno runtime shutdown failed');
82
- },
92
+ const closeRuntime = async (signal) => {
93
+ if (closed)
94
+ return;
95
+ closed = true;
96
+ const errors = [];
97
+ try {
98
+ await server.shutdown();
99
+ }
100
+ catch (error) {
101
+ errors.push(error);
102
+ }
103
+ try {
104
+ await host.application.close(signal);
105
+ }
106
+ catch (error) {
107
+ errors.push(error);
108
+ }
109
+ if (errors.length > 0)
110
+ throw new AggregateError(errors, 'Deno runtime shutdown failed');
111
+ };
112
+ const removeShutdownHooks = options.shutdownHooks === false
113
+ ? undefined
114
+ : registerDenoShutdownHooks(deno, closeRuntime);
115
+ const close = async (signal) => {
116
+ removeShutdownHooks?.();
117
+ await closeRuntime(signal);
118
+ };
119
+ return { application: host.application, port, close };
120
+ }
121
+ function registerDenoShutdownHooks(deno, close) {
122
+ if (!deno.addSignalListener || !deno.removeSignalListener) {
123
+ return undefined;
124
+ }
125
+ const handlers = new Map();
126
+ const remove = () => {
127
+ for (const [signal, handler] of handlers) {
128
+ deno.removeSignalListener?.(signal, handler);
129
+ }
130
+ handlers.clear();
83
131
  };
132
+ for (const signal of ['SIGINT', 'SIGTERM']) {
133
+ const handler = () => {
134
+ remove();
135
+ void close(signal);
136
+ };
137
+ handlers.set(signal, handler);
138
+ deno.addSignalListener(signal, handler);
139
+ }
140
+ return remove;
141
+ }
142
+ function canRetryOnNextPort(error, port) {
143
+ return port < 65_535 && isAddressInUseError(error);
144
+ }
145
+ function isAddressInUseError(error) {
146
+ return (typeof error === 'object' &&
147
+ error !== null &&
148
+ 'code' in error &&
149
+ error.code === 'EADDRINUSE');
84
150
  }
85
151
  function createDenoFetchDriver(application) {
86
152
  let initialization;
@@ -1,5 +1,6 @@
1
1
  import { binding, } from '../application/index.js';
2
2
  import { attachMessagePort, } from '../message-port/index.js';
3
+ import { assertRuntimeEngine } from '../runtime/engine.js';
3
4
  export const electronRuntime = {
4
5
  runtime: 'electron',
5
6
  capabilities: new Set([
@@ -15,6 +16,7 @@ export const electronRuntime = {
15
16
  attach,
16
17
  };
17
18
  function attach(options) {
19
+ assertRuntimeEngine('electron');
18
20
  const invocation = binding.invocation({
19
21
  application: options.application,
20
22
  environment: 'environment' in options
@@ -8,9 +8,15 @@ export interface OpenApiServer {
8
8
  readonly url: string;
9
9
  readonly description?: string;
10
10
  }
11
+ export interface OpenApiOperationIdContext {
12
+ readonly procedure: string;
13
+ readonly method: string;
14
+ readonly path: string;
15
+ }
11
16
  export interface GenerateOpenApiOptions {
12
17
  readonly info: OpenApiInfo;
13
18
  readonly servers?: readonly OpenApiServer[];
19
+ readonly operationId?: (context: OpenApiOperationIdContext) => string | undefined;
14
20
  }
15
21
  export interface OpenApiDocument {
16
22
  readonly openapi: '3.2.0';
@@ -13,7 +13,7 @@ const FIXED_METHODS = new Set([
13
13
  'query',
14
14
  ]);
15
15
  export function generateOpenApi(application, options) {
16
- const graph = assertValidCompilation(compileApplication({
16
+ assertValidCompilation(compileApplication({
17
17
  modules: application.modules,
18
18
  ...(application.arguments === undefined
19
19
  ? {}
@@ -21,11 +21,6 @@ export function generateOpenApi(application, options) {
21
21
  tasks: application.tasks,
22
22
  triggers: application.triggers,
23
23
  }));
24
- const executable = new Set(graph.executions
25
- .filter((execution) => execution.kind === 'protocol' && execution.protocol === 'http')
26
- .map((execution) => execution.kind === 'protocol'
27
- ? `${execution.implementation}\u0000${execution.procedure}`
28
- : ''));
29
24
  const registry = new SchemaRegistry();
30
25
  const paths = {};
31
26
  const operationIds = new Set();
@@ -34,20 +29,15 @@ export function generateOpenApi(application, options) {
34
29
  if (implementation.protocol !== 'http')
35
30
  continue;
36
31
  for (const procedure of implementation.procedures) {
37
- if (!executable.has(`${implementation.name}\u0000${procedure}`))
38
- continue;
39
32
  const protocol = implementation.contract.procedures[procedure]?.protocols.http;
40
33
  if (!protocol || protocol.protocol !== 'http')
41
34
  continue;
42
35
  const typed = protocol;
43
36
  const target = {
44
37
  definition: typed.definition,
45
- ...(implementation.contract.name === undefined
46
- ? {}
47
- : { contractName: implementation.contract.name }),
48
38
  procedure,
49
39
  };
50
- const operation = createOperation(target, registry, operationIds);
40
+ const operation = createOperation(target, registry, operationIds, options.operationId);
51
41
  attachOperation(paths, typed.definition, operation);
52
42
  }
53
43
  }
@@ -62,12 +52,17 @@ export function generateOpenApi(application, options) {
62
52
  ...(Object.keys(schemas).length === 0 ? {} : { components: { schemas } }),
63
53
  };
64
54
  }
65
- function createOperation(target, registry, operationIds) {
55
+ function createOperation(target, registry, operationIds, resolveOperationId) {
66
56
  const definition = target.definition;
67
- const operationId = target.contractName
68
- ? `${target.contractName}.${target.procedure}`
69
- : undefined;
70
- if (operationId) {
57
+ const operationId = resolveOperationId?.({
58
+ procedure: target.procedure,
59
+ method: definition.method,
60
+ path: definition.path,
61
+ });
62
+ if (operationId !== undefined) {
63
+ if (operationId.length === 0) {
64
+ throw openApiError('LUTRE_OPENAPI_OPERATION_ID_002', `operationId must not be empty for ${describeTarget(target)}`);
65
+ }
71
66
  if (operationIds.has(operationId)) {
72
67
  throw openApiError('LUTRE_OPENAPI_OPERATION_ID_001', `Duplicate operationId: ${operationId}`);
73
68
  }
@@ -322,7 +317,7 @@ function rebaseLocalDefinitions(schema, schemaComponentName) {
322
317
  return visit(schema);
323
318
  }
324
319
  function componentName(target, suffix) {
325
- return sanitizeComponentName(`${target.contractName ?? 'AnonymousContract'}_${target.procedure}_${suffix}`);
320
+ return sanitizeComponentName(`${target.definition.method} ${target.definition.path} ${target.procedure} ${suffix}`);
326
321
  }
327
322
  function sanitizeComponentName(value) {
328
323
  const sanitized = value.replace(/[^A-Za-z0-9_.-]+/g, '_');
@@ -340,7 +335,7 @@ function escapeJsonPointer(value) {
340
335
  return value.replace(/~/g, '~0').replace(/\//g, '~1');
341
336
  }
342
337
  function describeTarget(target) {
343
- return `${target.contractName ?? 'AnonymousContract'}.${target.procedure}`;
338
+ return `${target.definition.method.toUpperCase()} ${target.definition.path} (${target.procedure})`;
344
339
  }
345
340
  function dedupeJsonSchemas(schemas) {
346
341
  const seen = new Set();
@@ -12,17 +12,57 @@ export interface ProcedureDefinition<TProtocols extends Record<string, ProtocolD
12
12
  readonly kind: 'procedure';
13
13
  readonly protocols: TProtocols;
14
14
  }
15
- export declare function procedure<const TProtocols extends Record<string, ProtocolDescriptor>>(definition: {
16
- readonly protocols: TProtocols;
17
- }): ProcedureDefinition<TProtocols>;
15
+ export interface ProtocolGroup<TName extends string = string, TProcedures extends Record<string, ProtocolDescriptor<TName>> = Record<string, ProtocolDescriptor<TName>>> {
16
+ readonly kind: 'protocol-group';
17
+ readonly protocol: TName;
18
+ readonly procedures: TProcedures;
19
+ }
20
+ export declare function protocolGroup<const TName extends string, const TProcedures extends Record<string, ProtocolDescriptor<TName>>>(protocol: TName, procedures: TProcedures): ProtocolGroup<TName, TProcedures>;
18
21
  export interface ContractDefinition<TProcedures extends Record<string, ProcedureDefinition> = Record<string, ProcedureDefinition>> {
19
22
  readonly kind: 'contract';
20
- readonly name?: string;
21
23
  readonly procedures: TProcedures;
22
24
  }
23
- export declare function contract<const TProcedures extends Record<string, ProcedureDefinition>>(procedures: TProcedures & DispatchKeyUniquenessConstraint<TProcedures>, options?: {
24
- readonly name?: string;
25
- }): ContractDefinition<TProcedures>;
25
+ type ProcedureNamesOfGroup<TGroup> = TGroup extends ProtocolGroup<string, infer TProcedures> ? keyof TProcedures & string : never;
26
+ type ProcedureNamesOfGroups<TGroups extends readonly ProtocolGroup[]> = ProcedureNamesOfGroup<TGroups[number]>;
27
+ type ProtocolOfGroupProcedure<TGroup, TProcedure extends string> = TGroup extends ProtocolGroup<infer TProtocol, infer TProcedures> ? TProcedure extends keyof TProcedures ? {
28
+ readonly [K in TProtocol]: TProcedures[TProcedure];
29
+ } : never : never;
30
+ type ProceduresFromGroups<TGroups extends readonly ProtocolGroup[]> = {
31
+ [TProcedure in ProcedureNamesOfGroups<TGroups>]: ProcedureDefinition<UnionToIntersection<ProtocolOfGroupProcedure<TGroups[number], TProcedure>> extends infer TProtocols extends Record<string, ProtocolDescriptor> ? TProtocols : never>;
32
+ };
33
+ type NonEmptyArrayConstraint<TValues extends readonly unknown[]> = TValues extends readonly [] ? {
34
+ readonly __requiresAtLeastOneEntry__: never;
35
+ } : unknown;
36
+ type ProcedureProtocolKeysOfGroup<TGroup> = TGroup extends ProtocolGroup<infer TProtocol, infer TProcedures> ? `${keyof TProcedures & string}.${TProtocol}` : never;
37
+ type DuplicateGroupProcedureProtocolKeys<TGroups extends readonly ProtocolGroup[], TSeen extends string = never> = number extends TGroups['length'] ? never : TGroups extends readonly [
38
+ infer THead extends ProtocolGroup,
39
+ ...infer TTail extends readonly ProtocolGroup[]
40
+ ] ? Extract<ProcedureProtocolKeysOfGroup<THead>, TSeen> | DuplicateGroupProcedureProtocolKeys<TTail, TSeen | ProcedureProtocolKeysOfGroup<THead>> : never;
41
+ type GroupProcedureProtocolUniquenessConstraint<TGroups extends readonly ProtocolGroup[]> = [DuplicateGroupProcedureProtocolKeys<TGroups>] extends [never] ? unknown : {
42
+ readonly __duplicateProcedureProtocol__: DuplicateGroupProcedureProtocolKeys<TGroups>;
43
+ };
44
+ declare function defineContract<const TGroups extends readonly ProtocolGroup[]>(groups: TGroups & NonEmptyArrayConstraint<TGroups> & GroupProcedureProtocolUniquenessConstraint<TGroups> & DispatchKeyUniquenessConstraint<ProceduresFromGroups<TGroups>>): ContractDefinition<ProceduresFromGroups<TGroups>>;
45
+ type ProcedureNamesOfContract<TContract> = TContract extends ContractDefinition<infer TProcedures> ? keyof TProcedures & string : never;
46
+ type ProcedureNamesOfContracts<TContracts extends readonly ContractDefinition[]> = ProcedureNamesOfContract<TContracts[number]>;
47
+ type ProtocolsOfContractProcedure<TContract, TProcedure extends string> = TContract extends ContractDefinition<infer TProcedures> ? TProcedure extends keyof TProcedures ? TProcedures[TProcedure]['protocols'] : never : never;
48
+ type UnionToIntersection<T> = (T extends unknown ? (value: T) => void : never) extends (value: infer TIntersection) => void ? TIntersection : never;
49
+ type MergedProcedures<TContracts extends readonly ContractDefinition[]> = {
50
+ [TProcedure in ProcedureNamesOfContracts<TContracts>]: ProcedureDefinition<UnionToIntersection<ProtocolsOfContractProcedure<TContracts[number], TProcedure>> extends infer TProtocols extends Record<string, ProtocolDescriptor> ? TProtocols : never>;
51
+ };
52
+ type ProcedureProtocolKeysOfContract<TContract> = TContract extends ContractDefinition<infer TProcedures> ? {
53
+ [TProcedure in keyof TProcedures & string]: `${TProcedure}.${keyof TProcedures[TProcedure]['protocols'] & string}`;
54
+ }[keyof TProcedures & string] : never;
55
+ type DuplicateContractProcedureProtocolKeys<TContracts extends readonly ContractDefinition[], TSeen extends string = never> = number extends TContracts['length'] ? never : TContracts extends readonly [
56
+ infer THead extends ContractDefinition,
57
+ ...infer TTail extends readonly ContractDefinition[]
58
+ ] ? Extract<ProcedureProtocolKeysOfContract<THead>, TSeen> | DuplicateContractProcedureProtocolKeys<TTail, TSeen | ProcedureProtocolKeysOfContract<THead>> : never;
59
+ type ContractProcedureProtocolUniquenessConstraint<TContracts extends readonly ContractDefinition[]> = [DuplicateContractProcedureProtocolKeys<TContracts>] extends [never] ? unknown : {
60
+ readonly __duplicateProcedureProtocol__: DuplicateContractProcedureProtocolKeys<TContracts>;
61
+ };
62
+ declare function mergeContracts<const TContracts extends readonly ContractDefinition[]>(contracts: TContracts & NonEmptyArrayConstraint<TContracts> & ContractProcedureProtocolUniquenessConstraint<TContracts> & DispatchKeyUniquenessConstraint<MergedProcedures<TContracts>>): ContractDefinition<MergedProcedures<TContracts>>;
63
+ export declare const contract: typeof defineContract & {
64
+ merge: typeof mergeContracts;
65
+ };
26
66
  type ProtocolDispatchEntries<TProcedures extends Record<string, ProcedureDefinition>> = {
27
67
  [TProcedure in keyof TProcedures & string]: {
28
68
  [TProtocol in keyof TProcedures[TProcedure]['protocols'] & string]: TProcedures[TProcedure]['protocols'][TProtocol] extends {