@loutrejs/loutre 0.2.0 → 0.4.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.
@@ -1,22 +1,25 @@
1
1
  import { type ApplicationDefinition, type BootstrapArguments, type HasHttp, type HostBindingApplication } from '../application/index.js';
2
2
  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
- export type BunServeOptions<TDefinition extends ApplicationDefinition> = {
4
+ export type BunCreateOptions<TDefinition extends ApplicationDefinition> = {
5
5
  readonly application: HttpApplication<TDefinition>;
6
+ readonly environment?: unknown;
7
+ } & BootstrapArguments<TDefinition>;
8
+ export interface BunServeOptions {
6
9
  readonly port?: number;
7
10
  readonly hostname?: string;
8
11
  readonly shutdownHooks?: boolean;
9
- readonly environment?: unknown;
10
- } & BootstrapArguments<TDefinition>;
11
- export interface BunServeHandle<TDefinition extends ApplicationDefinition = ApplicationDefinition> {
12
- readonly application: HostBindingApplication<TDefinition>;
12
+ }
13
+ export interface BunListenerHandle {
13
14
  readonly port: number;
14
- close(signal?: string): Promise<void>;
15
15
  }
16
+ export type BunRuntimeApplication<TDefinition extends ApplicationDefinition = ApplicationDefinition> = HostBindingApplication<TDefinition> & {
17
+ serve(options?: BunServeOptions): Promise<BunListenerHandle>;
18
+ };
16
19
  export declare const bunRuntime: {
17
20
  readonly runtime: 'bun';
18
21
  readonly capabilities: Set<string>;
19
- readonly serve: typeof serve;
22
+ readonly create: typeof create;
20
23
  };
21
- declare function serve<const TDefinition extends ApplicationDefinition>(options: BunServeOptions<TDefinition>): Promise<BunServeHandle<TDefinition>>;
24
+ declare function create<const TDefinition extends ApplicationDefinition>(options: BunCreateOptions<TDefinition>): Promise<BunRuntimeApplication<TDefinition>>;
22
25
  export {};
@@ -15,16 +15,13 @@ export const bunRuntime = {
15
15
  'env.runtime',
16
16
  'crypto.random',
17
17
  ]),
18
- serve,
18
+ create,
19
19
  };
20
- async function serve(options) {
20
+ async function create(options) {
21
21
  assertRuntimeEngine('bun');
22
22
  const bun = bunGlobal();
23
- if (!bun?.serve) {
24
- throw new Error('LUTRE_BUN_UNAVAILABLE: Bun.serve() is not available.');
25
- }
26
23
  const startedAt = performance.now();
27
- const environment = bun.env ?? {};
24
+ const environment = bun?.env ?? {};
28
25
  const presentation = startStartupPresentation({ version: LOUTRE_VERSION }, {
29
26
  terminal: detectPresentationTerminal(process.stdout, environment),
30
27
  write: (value) => console.log(value),
@@ -37,53 +34,32 @@ async function serve(options) {
37
34
  const http = 'http' in host ? host.http : undefined;
38
35
  if (!http) {
39
36
  await host.application.close();
40
- throw new Error('LUTRE_RUNTIME_HTTP_REQUIRED: bunRuntime.serve() requires an HTTP-capable Application.');
37
+ throw new Error('LUTRE_RUNTIME_HTTP_REQUIRED: bunRuntime.create() requires an HTTP-capable Application.');
41
38
  }
42
39
  await host.application.init();
43
- if ('triggers' in host.application)
44
- await host.application.triggers.start();
40
+ const application = host.application;
41
+ const closeApplication = host.application.close.bind(host.application);
45
42
  let server;
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
- }
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
- });
43
+ let removeShutdownHooks;
44
+ let serving = false;
73
45
  let closed = false;
74
- const closeRuntime = async (signal) => {
46
+ const close = async (signal) => {
75
47
  if (closed)
76
48
  return;
77
49
  closed = true;
50
+ removeShutdownHooks?.();
51
+ removeShutdownHooks = undefined;
78
52
  const errors = [];
79
- try {
80
- await server.stop(true);
81
- }
82
- catch (error) {
83
- errors.push(error);
53
+ if (server) {
54
+ try {
55
+ await server.stop(true);
56
+ }
57
+ catch (error) {
58
+ errors.push(error);
59
+ }
84
60
  }
85
61
  try {
86
- await host.application.close(signal);
62
+ await closeApplication(signal);
87
63
  }
88
64
  catch (error) {
89
65
  errors.push(error);
@@ -91,14 +67,60 @@ async function serve(options) {
91
67
  if (errors.length > 0)
92
68
  throw new AggregateError(errors, 'Bun runtime shutdown failed');
93
69
  };
94
- const removeShutdownHooks = options.shutdownHooks === false
95
- ? undefined
96
- : registerBunShutdownHooks(closeRuntime);
97
- const close = async (signal) => {
98
- removeShutdownHooks?.();
99
- await closeRuntime(signal);
70
+ const serve = async (serveOptions = {}) => {
71
+ if (closed) {
72
+ throw new Error('LUTRE_APP_STOPPED: Application is stopped.');
73
+ }
74
+ if (serving) {
75
+ throw new Error('LUTRE_RUNTIME_ALREADY_SERVING: Bun runtime Application is already serving.');
76
+ }
77
+ if (!bun?.serve) {
78
+ await close().catch(() => undefined);
79
+ throw new Error('LUTRE_BUN_UNAVAILABLE: Bun.serve() is not available.');
80
+ }
81
+ serving = true;
82
+ try {
83
+ if ('triggers' in application)
84
+ await application.triggers.start();
85
+ const requestedPort = serveOptions.port;
86
+ let port = requestedPort ?? 3000;
87
+ while (true) {
88
+ try {
89
+ server = bun.serve({
90
+ port,
91
+ ...(serveOptions.hostname === undefined
92
+ ? {}
93
+ : { hostname: serveOptions.hostname }),
94
+ fetch: createBunFetchDriver(http),
95
+ });
96
+ break;
97
+ }
98
+ catch (error) {
99
+ if (requestedPort !== undefined || !canRetryOnNextPort(error, port)) {
100
+ throw error;
101
+ }
102
+ port += 1;
103
+ }
104
+ }
105
+ presentation.ready({
106
+ server: serverUrl(serveOptions.hostname, port),
107
+ runtime: `Bun ${bun.version ?? 'unknown'}`,
108
+ environment: environment.NODE_ENV ?? 'development',
109
+ startupDurationMs: performance.now() - startedAt,
110
+ });
111
+ removeShutdownHooks =
112
+ serveOptions.shutdownHooks === false
113
+ ? undefined
114
+ : registerBunShutdownHooks(close);
115
+ return { port };
116
+ }
117
+ catch (error) {
118
+ await close().catch(() => undefined);
119
+ throw error;
120
+ }
100
121
  };
101
- return { application: host.application, port, close };
122
+ Object.assign(application, { serve, close });
123
+ return application;
102
124
  }
103
125
  function registerBunShutdownHooks(close) {
104
126
  const handlers = new Map();
@@ -5,26 +5,28 @@ export type DenoRuntimeOptions<TDefinition extends ApplicationDefinition> = {
5
5
  readonly application: HttpApplication<TDefinition>;
6
6
  readonly environment?: unknown;
7
7
  } & BootstrapArguments<TDefinition>;
8
- export type DenoServeOptions<TDefinition extends ApplicationDefinition> = DenoRuntimeOptions<TDefinition> & {
8
+ export type DenoCreateOptions<TDefinition extends ApplicationDefinition> = DenoRuntimeOptions<TDefinition>;
9
+ export interface DenoServeOptions {
9
10
  readonly port?: number;
10
11
  readonly hostname?: string;
11
12
  readonly shutdownHooks?: boolean;
12
- };
13
+ }
13
14
  export interface DenoBinding {
14
15
  fetch(request: Request): Promise<Response>;
15
16
  close(signal?: string): Promise<void>;
16
17
  }
17
- export interface DenoServeHandle<TDefinition extends ApplicationDefinition = ApplicationDefinition> {
18
- readonly application: HostBindingApplication<TDefinition>;
18
+ export interface DenoListenerHandle {
19
19
  readonly port: number;
20
- close(signal?: string): Promise<void>;
21
20
  }
21
+ export type DenoRuntimeApplication<TDefinition extends ApplicationDefinition = ApplicationDefinition> = HostBindingApplication<TDefinition> & {
22
+ serve(options?: DenoServeOptions): Promise<DenoListenerHandle>;
23
+ };
22
24
  export declare const denoRuntime: {
23
25
  readonly runtime: 'deno';
24
26
  readonly capabilities: Set<string>;
25
27
  readonly bind: typeof bind;
26
- readonly serve: typeof serve;
28
+ readonly create: typeof create;
27
29
  };
28
30
  declare function bind<const TDefinition extends ApplicationDefinition>(options: DenoRuntimeOptions<TDefinition>): DenoBinding;
29
- declare function serve<const TDefinition extends ApplicationDefinition>(options: DenoServeOptions<TDefinition>): Promise<DenoServeHandle<TDefinition>>;
31
+ declare function create<const TDefinition extends ApplicationDefinition>(options: DenoCreateOptions<TDefinition>): Promise<DenoRuntimeApplication<TDefinition>>;
30
32
  export {};
@@ -16,7 +16,7 @@ export const denoRuntime = {
16
16
  'crypto.random',
17
17
  ]),
18
18
  bind,
19
- serve,
19
+ create,
20
20
  };
21
21
  function bind(options) {
22
22
  assertRuntimeEngine('deno');
@@ -34,18 +34,15 @@ function bind(options) {
34
34
  close: (signal) => invocation.application.close(signal),
35
35
  };
36
36
  }
37
- async function serve(options) {
37
+ async function create(options) {
38
38
  assertRuntimeEngine('deno');
39
39
  const deno = denoGlobal();
40
- if (!deno?.serve) {
41
- throw new Error('LUTRE_DENO_UNAVAILABLE: Deno.serve() is not available.');
42
- }
43
40
  const startedAt = performance.now();
44
- const isTTY = deno.stdout?.isTerminal?.() === true;
41
+ const isTTY = deno?.stdout?.isTerminal?.() === true;
45
42
  const presentation = startStartupPresentation({ version: LOUTRE_VERSION }, {
46
43
  terminal: {
47
44
  isTTY,
48
- color: isTTY && deno.env?.get?.('NO_COLOR') === undefined,
45
+ color: isTTY && deno?.env?.get?.('NO_COLOR') === undefined,
49
46
  },
50
47
  write: (value) => console.log(value),
51
48
  });
@@ -53,55 +50,32 @@ async function serve(options) {
53
50
  const http = 'http' in host ? host.http : undefined;
54
51
  if (!http) {
55
52
  await host.application.close();
56
- throw new Error('LUTRE_RUNTIME_HTTP_REQUIRED: denoRuntime.serve() requires an HTTP-capable Application.');
53
+ throw new Error('LUTRE_RUNTIME_HTTP_REQUIRED: denoRuntime.create() requires an HTTP-capable Application.');
57
54
  }
58
55
  await host.application.init();
59
- if ('triggers' in host.application)
60
- await host.application.triggers.start();
56
+ const application = host.application;
57
+ const closeApplication = host.application.close.bind(host.application);
61
58
  let server;
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
- }
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
- });
59
+ let removeShutdownHooks;
60
+ let serving = false;
91
61
  let closed = false;
92
- const closeRuntime = async (signal) => {
62
+ const close = async (signal) => {
93
63
  if (closed)
94
64
  return;
95
65
  closed = true;
66
+ removeShutdownHooks?.();
67
+ removeShutdownHooks = undefined;
96
68
  const errors = [];
97
- try {
98
- await server.shutdown();
99
- }
100
- catch (error) {
101
- errors.push(error);
69
+ if (server) {
70
+ try {
71
+ await server.shutdown();
72
+ }
73
+ catch (error) {
74
+ errors.push(error);
75
+ }
102
76
  }
103
77
  try {
104
- await host.application.close(signal);
78
+ await closeApplication(signal);
105
79
  }
106
80
  catch (error) {
107
81
  errors.push(error);
@@ -109,14 +83,62 @@ async function serve(options) {
109
83
  if (errors.length > 0)
110
84
  throw new AggregateError(errors, 'Deno runtime shutdown failed');
111
85
  };
112
- const removeShutdownHooks = options.shutdownHooks === false
113
- ? undefined
114
- : registerDenoShutdownHooks(deno, closeRuntime);
115
- const close = async (signal) => {
116
- removeShutdownHooks?.();
117
- await closeRuntime(signal);
86
+ const serve = async (serveOptions = {}) => {
87
+ if (closed) {
88
+ throw new Error('LUTRE_APP_STOPPED: Application is stopped.');
89
+ }
90
+ if (serving) {
91
+ throw new Error('LUTRE_RUNTIME_ALREADY_SERVING: Deno runtime Application is already serving.');
92
+ }
93
+ if (!deno?.serve) {
94
+ await close().catch(() => undefined);
95
+ throw new Error('LUTRE_DENO_UNAVAILABLE: Deno.serve() is not available.');
96
+ }
97
+ serving = true;
98
+ try {
99
+ if ('triggers' in application)
100
+ await application.triggers.start();
101
+ const requestedPort = serveOptions.port;
102
+ let port = requestedPort ?? 3000;
103
+ while (true) {
104
+ try {
105
+ server = deno.serve({
106
+ port,
107
+ ...(serveOptions.hostname === undefined
108
+ ? {}
109
+ : { hostname: serveOptions.hostname }),
110
+ onListen: () => undefined,
111
+ }, createDenoFetchDriver(http));
112
+ break;
113
+ }
114
+ catch (error) {
115
+ if (requestedPort !== undefined || !canRetryOnNextPort(error, port)) {
116
+ throw error;
117
+ }
118
+ port += 1;
119
+ }
120
+ }
121
+ presentation.ready({
122
+ server: serverUrl(serveOptions.hostname, port),
123
+ runtime: `Deno ${deno.version?.deno ?? 'unknown'}`,
124
+ environment: deno.env?.get?.('DENO_ENV') ??
125
+ deno.env?.get?.('NODE_ENV') ??
126
+ 'development',
127
+ startupDurationMs: performance.now() - startedAt,
128
+ });
129
+ removeShutdownHooks =
130
+ serveOptions.shutdownHooks === false
131
+ ? undefined
132
+ : registerDenoShutdownHooks(deno, close);
133
+ return { port };
134
+ }
135
+ catch (error) {
136
+ await close().catch(() => undefined);
137
+ throw error;
138
+ }
118
139
  };
119
- return { application: host.application, port, close };
140
+ Object.assign(application, { serve, close });
141
+ return application;
120
142
  }
121
143
  function registerDenoShutdownHooks(deno, close) {
122
144
  if (!deno.addSignalListener || !deno.removeSignalListener) {
@@ -109,6 +109,9 @@ function createRuntimeState(options) {
109
109
  function createInvocationApplication(state) {
110
110
  const application = {
111
111
  graph: state.graph,
112
+ get(token) {
113
+ return state.runtime.get(token);
114
+ },
112
115
  async init() {
113
116
  await state.runtime.initialize();
114
117
  return application;
@@ -1,4 +1,4 @@
1
- import type { ArgsClass, ModuleProtocols, ModuleInstance, SchemaInput, TaskArguments, TaskDescriptor, TaskOutput, TriggerDescriptor } from '../core/index.js';
1
+ import type { ArgsClass, ModuleProtocols, ModuleInstance, SchemaInput, TaskArguments, TaskDescriptor, TaskOutput, TokenLike, TokenValue, TriggerDescriptor } from '../core/index.js';
2
2
  import type { ApplicationGraphIR } from '../graph/index.js';
3
3
  import type { Logger } from '../runtime/index.js';
4
4
  export interface ApplicationDefinitionOptions<TModules extends readonly ModuleInstance[], TArguments extends ArgsClass | undefined, TTasks extends readonly TaskDescriptor<any, any>[], TTriggers extends readonly TriggerDescriptor[]> {
@@ -27,6 +27,7 @@ export type BootstrapArguments<TDefinition extends ApplicationDefinition> = TDef
27
27
  };
28
28
  export interface BaseApplication {
29
29
  readonly graph: ApplicationGraphIR;
30
+ get<TToken extends TokenLike>(token: TToken): TokenValue<TToken>;
30
31
  init(): Promise<this>;
31
32
  close(signal?: string): Promise<void>;
32
33
  }
@@ -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 {