@loutrejs/loutre 0.2.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.
- package/dist/application/openapi.d.ts +6 -0
- package/dist/application/openapi.js +14 -19
- package/dist/core/contract.d.ts +47 -7
- package/dist/core/contract.js +59 -10
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/graph/compiler.d.ts +4 -0
- package/dist/graph/compiler.js +228 -0
- package/dist/graph/graph.js +56 -31
- package/dist/graph/index.d.ts +2 -1
- package/dist/graph/index.js +2 -1
- package/dist/graph/ir.d.ts +24 -8
- package/dist/http/client.d.ts +72 -0
- package/dist/http/client.js +258 -0
- package/dist/http/definitions.d.ts +13 -5
- package/dist/http/definitions.js +6 -2
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +1 -0
- package/dist/message-port/index.d.ts +12 -4
- package/dist/message-port/index.js +9 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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 =
|
|
68
|
-
|
|
69
|
-
:
|
|
70
|
-
|
|
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.
|
|
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.
|
|
338
|
+
return `${target.definition.method.toUpperCase()} ${target.definition.path} (${target.procedure})`;
|
|
344
339
|
}
|
|
345
340
|
function dedupeJsonSchemas(schemas) {
|
|
346
341
|
const seen = new Set();
|
package/dist/core/contract.d.ts
CHANGED
|
@@ -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
|
|
16
|
-
readonly
|
|
17
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 {
|
package/dist/core/contract.js
CHANGED
|
@@ -1,15 +1,64 @@
|
|
|
1
|
-
export function
|
|
2
|
-
return {
|
|
1
|
+
export function protocolGroup(protocol, procedures) {
|
|
2
|
+
return Object.freeze({
|
|
3
|
+
kind: 'protocol-group',
|
|
4
|
+
protocol,
|
|
5
|
+
procedures: Object.freeze({ ...procedures }),
|
|
6
|
+
});
|
|
3
7
|
}
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
function defineContract(groups) {
|
|
9
|
+
if (groups.length === 0) {
|
|
10
|
+
throw new Error('contract([]) requires at least one protocol group');
|
|
11
|
+
}
|
|
12
|
+
const procedures = {};
|
|
13
|
+
for (const group of groups) {
|
|
14
|
+
for (const [procedureName, descriptor] of Object.entries(group.procedures)) {
|
|
15
|
+
const current = procedures[procedureName];
|
|
16
|
+
if (Object.hasOwn(current?.protocols ?? {}, group.protocol)) {
|
|
17
|
+
throw new Error(`Duplicate contract procedure protocol: ${procedureName}.${group.protocol}`);
|
|
18
|
+
}
|
|
19
|
+
procedures[procedureName] = {
|
|
20
|
+
kind: 'procedure',
|
|
21
|
+
protocols: {
|
|
22
|
+
...current?.protocols,
|
|
23
|
+
[group.protocol]: descriptor,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
assertUniqueDispatchKeys(procedures);
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
kind: 'contract',
|
|
31
|
+
procedures: Object.freeze(procedures),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function mergeContracts(contracts) {
|
|
35
|
+
if (contracts.length === 0) {
|
|
36
|
+
throw new Error('contract.merge([]) requires at least one Contract');
|
|
37
|
+
}
|
|
38
|
+
const procedures = {};
|
|
39
|
+
for (const source of contracts) {
|
|
40
|
+
for (const [procedureName, procedureDefinition] of Object.entries(source.procedures)) {
|
|
41
|
+
const current = procedures[procedureName];
|
|
42
|
+
const protocols = { ...current?.protocols };
|
|
43
|
+
for (const [protocolName, descriptor] of Object.entries(procedureDefinition.protocols)) {
|
|
44
|
+
if (Object.hasOwn(protocols, protocolName)) {
|
|
45
|
+
throw new Error(`Duplicate contract procedure protocol: ${procedureName}.${protocolName}`);
|
|
46
|
+
}
|
|
47
|
+
protocols[protocolName] = descriptor;
|
|
48
|
+
}
|
|
49
|
+
procedures[procedureName] = { kind: 'procedure', protocols };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
assertUniqueDispatchKeys(procedures);
|
|
53
|
+
return Object.freeze({
|
|
7
54
|
kind: 'contract',
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
};
|
|
55
|
+
procedures: Object.freeze(procedures),
|
|
56
|
+
});
|
|
11
57
|
}
|
|
12
|
-
|
|
58
|
+
export const contract = Object.assign(defineContract, {
|
|
59
|
+
merge: mergeContracts,
|
|
60
|
+
});
|
|
61
|
+
function assertUniqueDispatchKeys(procedures) {
|
|
13
62
|
const paths = new Map();
|
|
14
63
|
for (const [procedureName, procedureDefinition] of Object.entries(procedures)) {
|
|
15
64
|
for (const [protocolName, protocol] of Object.entries(procedureDefinition.protocols)) {
|
|
@@ -19,7 +68,7 @@ function assertUniqueDispatchKeys(procedures, contractName) {
|
|
|
19
68
|
if (typeof dispatchKey !== 'string') {
|
|
20
69
|
throw new Error(`Protocol dispatchKey must be a string or null: ${procedureName}.${protocolName}`);
|
|
21
70
|
}
|
|
22
|
-
const path = `${
|
|
71
|
+
const path = `${procedureName}.${protocolName}`;
|
|
23
72
|
const existing = paths.get(dispatchKey);
|
|
24
73
|
if (existing) {
|
|
25
74
|
throw new Error(`Duplicate protocol dispatch key "${dispatchKey}" between ${existing} and ${path}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const LOUTRE_VERSION = "0.
|
|
1
|
+
export declare const LOUTRE_VERSION = "0.3.0";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// package manifestとのずれを避けるため、scripts/generate-version.mjsがbuild前に更新する。
|
|
2
|
-
export const LOUTRE_VERSION = '0.
|
|
2
|
+
export const LOUTRE_VERSION = '0.3.0';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ApplicationCompilationInput } from './graph.js';
|
|
2
|
+
import type { CompilationResult } from './ir.js';
|
|
3
|
+
export declare function compileApplication(input: ApplicationCompilationInput): CompilationResult;
|
|
4
|
+
export declare const buildApplicationGraph: typeof compileApplication;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { asModuleInstance, normalizeProvider, tokenName, } from '../core/index.js';
|
|
2
|
+
import { compileApplication as compileBaseApplication, } from './graph.js';
|
|
3
|
+
export function compileApplication(input) {
|
|
4
|
+
const base = compileBaseApplication(input);
|
|
5
|
+
const modules = collectModules(input.modules);
|
|
6
|
+
const moduleIds = new Map(modules.map((module, index) => [module, `module:${index + 1}`]));
|
|
7
|
+
const implementationModules = collectImplementationModules(modules, moduleIds);
|
|
8
|
+
const implementationNames = new Map(base.graph.implementations.map((implementation) => [
|
|
9
|
+
implementation.id,
|
|
10
|
+
implementation.name,
|
|
11
|
+
]));
|
|
12
|
+
const graphModules = new Map(base.graph.modules.map((module) => [module.id, module]));
|
|
13
|
+
const executionModules = new Map();
|
|
14
|
+
for (const execution of base.graph.executions) {
|
|
15
|
+
if (execution.kind !== 'protocol')
|
|
16
|
+
continue;
|
|
17
|
+
const implementationName = implementationNames.get(execution.implementation);
|
|
18
|
+
const module = implementationName
|
|
19
|
+
? implementationModules.get(implementationName)
|
|
20
|
+
: undefined;
|
|
21
|
+
if (!module)
|
|
22
|
+
continue;
|
|
23
|
+
executionModules.set(`${execution.contract}:${execution.procedure}:${execution.protocol}`, module);
|
|
24
|
+
}
|
|
25
|
+
const providerModules = collectProviderModules(modules, moduleIds);
|
|
26
|
+
const nodes = base.graph.nodes.map((node) => {
|
|
27
|
+
const module = node.module ??
|
|
28
|
+
providerModules.get(node.label) ??
|
|
29
|
+
moduleForImplementation(node, implementationModules) ??
|
|
30
|
+
moduleForLifecycle(node) ??
|
|
31
|
+
moduleForLayer(node, executionModules);
|
|
32
|
+
if (!module)
|
|
33
|
+
return node;
|
|
34
|
+
const owner = graphModules.get(module);
|
|
35
|
+
if (!owner)
|
|
36
|
+
return node;
|
|
37
|
+
return {
|
|
38
|
+
...node,
|
|
39
|
+
module,
|
|
40
|
+
visibility: owner.exports.includes(node.label) ? 'exported' : 'private',
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
const diagnostics = [
|
|
44
|
+
...base.diagnostics,
|
|
45
|
+
...validateModuleExports(base.graph.modules),
|
|
46
|
+
...validateModuleVisibility(base.graph, nodes),
|
|
47
|
+
];
|
|
48
|
+
const graph = {
|
|
49
|
+
...base.graph,
|
|
50
|
+
nodes,
|
|
51
|
+
diagnostics,
|
|
52
|
+
};
|
|
53
|
+
return { graph, diagnostics };
|
|
54
|
+
}
|
|
55
|
+
export const buildApplicationGraph = compileApplication;
|
|
56
|
+
function collectModules(roots) {
|
|
57
|
+
const modules = [];
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
const visit = (candidate) => {
|
|
60
|
+
const module = asModuleInstance(candidate);
|
|
61
|
+
if (seen.has(module))
|
|
62
|
+
return;
|
|
63
|
+
seen.add(module);
|
|
64
|
+
for (const imported of module.definition.imports ?? [])
|
|
65
|
+
visit(imported);
|
|
66
|
+
modules.push(module);
|
|
67
|
+
};
|
|
68
|
+
for (const root of roots)
|
|
69
|
+
visit(root);
|
|
70
|
+
return modules;
|
|
71
|
+
}
|
|
72
|
+
function collectProviderModules(modules, moduleIds) {
|
|
73
|
+
const result = new Map();
|
|
74
|
+
const ambiguous = new Set();
|
|
75
|
+
for (const module of modules) {
|
|
76
|
+
const moduleId = moduleIds.get(module);
|
|
77
|
+
if (!moduleId)
|
|
78
|
+
continue;
|
|
79
|
+
for (const declaration of module.definition.providers ?? []) {
|
|
80
|
+
const provider = normalizeProvider(declaration);
|
|
81
|
+
const labels = [
|
|
82
|
+
tokenName(provider.provide),
|
|
83
|
+
...(provider.kind === 'class' ? [provider.useClass.name] : []),
|
|
84
|
+
...(provider.kind === 'conditional'
|
|
85
|
+
? Reflect.ownKeys(provider.mapping).flatMap((key) => {
|
|
86
|
+
const candidate = provider.mapping[key];
|
|
87
|
+
return candidate ? [candidate.name] : [];
|
|
88
|
+
})
|
|
89
|
+
: []),
|
|
90
|
+
];
|
|
91
|
+
for (const label of labels) {
|
|
92
|
+
const current = result.get(label);
|
|
93
|
+
if (current && current !== moduleId)
|
|
94
|
+
ambiguous.add(label);
|
|
95
|
+
else
|
|
96
|
+
result.set(label, moduleId);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const label of ambiguous)
|
|
101
|
+
result.delete(label);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
function collectImplementationModules(modules, moduleIds) {
|
|
105
|
+
const result = new Map();
|
|
106
|
+
const ambiguous = new Set();
|
|
107
|
+
for (const module of modules) {
|
|
108
|
+
const moduleId = moduleIds.get(module);
|
|
109
|
+
if (!moduleId)
|
|
110
|
+
continue;
|
|
111
|
+
for (const implementation of module.definition.implementations ?? []) {
|
|
112
|
+
const current = result.get(implementation.name);
|
|
113
|
+
if (current && current !== moduleId)
|
|
114
|
+
ambiguous.add(implementation.name);
|
|
115
|
+
else
|
|
116
|
+
result.set(implementation.name, moduleId);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const name of ambiguous)
|
|
120
|
+
result.delete(name);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function moduleForImplementation(node, modules) {
|
|
124
|
+
return node.kind === 'implementation' ? modules.get(node.label) : undefined;
|
|
125
|
+
}
|
|
126
|
+
function moduleForLifecycle(node) {
|
|
127
|
+
if (node.kind !== 'framework' || !node.id.startsWith('lifecycle:module:')) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const match = /^lifecycle:(module:\d+):/.exec(node.id);
|
|
131
|
+
return match?.[1];
|
|
132
|
+
}
|
|
133
|
+
function moduleForLayer(node, executionModules) {
|
|
134
|
+
if (node.kind !== 'layer')
|
|
135
|
+
return undefined;
|
|
136
|
+
const match = /^layer:(contract:\d+)\/([^/]+)\/([^/]+)\//.exec(node.id);
|
|
137
|
+
if (!match)
|
|
138
|
+
return undefined;
|
|
139
|
+
const [, contract, procedure, protocol] = match;
|
|
140
|
+
return executionModules.get(`${contract}:${procedure}:${protocol}`);
|
|
141
|
+
}
|
|
142
|
+
function validateModuleExports(modules) {
|
|
143
|
+
const byId = new Map(modules.map((module) => [module.id, module]));
|
|
144
|
+
return modules.flatMap((module) => module.exports.flatMap((token) => declaresToken(module, token) ||
|
|
145
|
+
canResolveExport(module, token, byId, new Set())
|
|
146
|
+
? []
|
|
147
|
+
: [
|
|
148
|
+
{
|
|
149
|
+
code: 'LUTRE_MODULE_EXPORT_UNRESOLVED',
|
|
150
|
+
message: `${describeModule(module)} exports ${token}, but the token is neither declared nor re-exported from an imported Module.`,
|
|
151
|
+
path: `${module.id}.exports.${token}`,
|
|
152
|
+
},
|
|
153
|
+
]));
|
|
154
|
+
}
|
|
155
|
+
function declaresToken(module, token) {
|
|
156
|
+
return module.providers.includes(token) || module.environment.includes(token);
|
|
157
|
+
}
|
|
158
|
+
function canResolveExport(source, token, modules, visited) {
|
|
159
|
+
if (visited.has(source.id))
|
|
160
|
+
return false;
|
|
161
|
+
visited.add(source.id);
|
|
162
|
+
for (const importedId of source.imports) {
|
|
163
|
+
const imported = modules.get(importedId);
|
|
164
|
+
if (!imported || !imported.exports.includes(token))
|
|
165
|
+
continue;
|
|
166
|
+
if (declaresToken(imported, token))
|
|
167
|
+
return true;
|
|
168
|
+
if (canResolveExport(imported, token, modules, new Set(visited)))
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
function validateModuleVisibility(graph, nodes) {
|
|
174
|
+
const modules = new Map(graph.modules.map((module) => [module.id, module]));
|
|
175
|
+
const nodesById = new Map(nodes.map((node) => [node.id, node]));
|
|
176
|
+
const seen = new Set();
|
|
177
|
+
const diagnostics = [];
|
|
178
|
+
for (const edge of graph.edges) {
|
|
179
|
+
const consumer = nodesById.get(edge.from);
|
|
180
|
+
const dependency = nodesById.get(edge.to);
|
|
181
|
+
if (!consumer?.module ||
|
|
182
|
+
!dependency?.module ||
|
|
183
|
+
consumer.module === dependency.module ||
|
|
184
|
+
!modules.has(consumer.module) ||
|
|
185
|
+
!modules.has(dependency.module)) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const key = `${consumer.id}->${dependency.id}`;
|
|
189
|
+
if (seen.has(key))
|
|
190
|
+
continue;
|
|
191
|
+
seen.add(key);
|
|
192
|
+
if (isTokenVisibleFrom(consumer.module, dependency.module, dependency.label, modules)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
diagnostics.push({
|
|
196
|
+
code: 'LUTRE_MODULE_VISIBILITY',
|
|
197
|
+
message: `${consumer.label} in ${describeModuleId(consumer.module, modules)} depends on private ${dependency.label} from ${describeModuleId(dependency.module, modules)}. Export the dependency and import its Module explicitly.`,
|
|
198
|
+
path: consumer.id,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return diagnostics;
|
|
202
|
+
}
|
|
203
|
+
function isTokenVisibleFrom(sourceId, targetId, token, modules, visited = new Set()) {
|
|
204
|
+
if (visited.has(sourceId))
|
|
205
|
+
return false;
|
|
206
|
+
visited.add(sourceId);
|
|
207
|
+
const source = modules.get(sourceId);
|
|
208
|
+
if (!source)
|
|
209
|
+
return false;
|
|
210
|
+
for (const importedId of source.imports) {
|
|
211
|
+
const imported = modules.get(importedId);
|
|
212
|
+
if (!imported || !imported.exports.includes(token))
|
|
213
|
+
continue;
|
|
214
|
+
if (importedId === targetId)
|
|
215
|
+
return true;
|
|
216
|
+
if (isTokenVisibleFrom(importedId, targetId, token, modules, new Set(visited))) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
function describeModule(module) {
|
|
223
|
+
return module.name ?? module.description ?? module.id;
|
|
224
|
+
}
|
|
225
|
+
function describeModuleId(id, modules) {
|
|
226
|
+
const module = modules.get(id);
|
|
227
|
+
return module ? describeModule(module) : id;
|
|
228
|
+
}
|
package/dist/graph/graph.js
CHANGED
|
@@ -38,17 +38,29 @@ export function compileApplication(input) {
|
|
|
38
38
|
validateNamedDescriptors(queues, 'LUTRE_QUEUE_DUPLICATE', diagnostics);
|
|
39
39
|
validateCronTriggers(triggers, diagnostics);
|
|
40
40
|
diagnostics.push(...validateQueueDrivers(input.modules, queueTriggers));
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const current = contractNames.get(contract);
|
|
41
|
+
const contractIds = new Map();
|
|
42
|
+
const idContract = (contract) => {
|
|
43
|
+
const current = contractIds.get(contract);
|
|
45
44
|
if (current)
|
|
46
45
|
return current;
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
return
|
|
46
|
+
const id = `contract:${contractIds.size + 1}`;
|
|
47
|
+
contractIds.set(contract, id);
|
|
48
|
+
return id;
|
|
49
|
+
};
|
|
50
|
+
const implementationIds = new Map();
|
|
51
|
+
const idImplementation = (implementation) => {
|
|
52
|
+
const current = implementationIds.get(implementation);
|
|
53
|
+
if (current)
|
|
54
|
+
return current;
|
|
55
|
+
const id = `implementation:${implementationIds.size + 1}`;
|
|
56
|
+
implementationIds.set(implementation, id);
|
|
57
|
+
return id;
|
|
50
58
|
};
|
|
51
59
|
const descriptors = modules.flatMap((module) => module.definition.implementations ?? []);
|
|
60
|
+
for (const implementation of new Set(descriptors)) {
|
|
61
|
+
idContract(implementation.contract);
|
|
62
|
+
idImplementation(implementation);
|
|
63
|
+
}
|
|
52
64
|
const targets = [];
|
|
53
65
|
for (const implementation of descriptors) {
|
|
54
66
|
for (const procedureName of implementation.procedures) {
|
|
@@ -58,13 +70,14 @@ export function compileApplication(input) {
|
|
|
58
70
|
diagnostics.push({
|
|
59
71
|
code: 'LUTRE_IMPL_003',
|
|
60
72
|
message: `${procedureName} is not declared for protocol ${implementation.protocol}`,
|
|
61
|
-
path: `${
|
|
73
|
+
path: `${idContract(implementation.contract)}.${procedureName}.${implementation.protocol}`,
|
|
62
74
|
});
|
|
63
75
|
continue;
|
|
64
76
|
}
|
|
65
77
|
targets.push({
|
|
66
78
|
implementation,
|
|
67
|
-
|
|
79
|
+
contractId: idContract(implementation.contract),
|
|
80
|
+
implementationId: idImplementation(implementation),
|
|
68
81
|
procedure: procedureName,
|
|
69
82
|
protocol: implementation.protocol,
|
|
70
83
|
dispatchKey: protocol.dispatchKey,
|
|
@@ -77,28 +90,27 @@ export function compileApplication(input) {
|
|
|
77
90
|
}
|
|
78
91
|
}
|
|
79
92
|
validateDispatchKeys(targets, diagnostics);
|
|
80
|
-
validateCoverage(descriptors,
|
|
93
|
+
validateCoverage(descriptors, contractIds, diagnostics);
|
|
81
94
|
validateDuplicateProviders(modules, diagnostics);
|
|
82
95
|
const tokensById = collectCustomTokens(providers, targets, diagnostics);
|
|
83
96
|
const contextKeysByName = collectContextKeys(targets, diagnostics);
|
|
84
97
|
const pipelines = [];
|
|
85
|
-
const implementations = [];
|
|
86
98
|
for (const target of targets) {
|
|
87
99
|
validatePipeline(target, diagnostics);
|
|
88
100
|
pipelines.push({
|
|
89
|
-
contract: target.
|
|
101
|
+
contract: target.contractId,
|
|
90
102
|
procedure: target.procedure,
|
|
91
103
|
protocol: target.protocol,
|
|
92
104
|
layers: target.pipeline.map(toLayerIR),
|
|
93
105
|
});
|
|
94
|
-
implementations.push({
|
|
95
|
-
contract: target.contractName,
|
|
96
|
-
procedure: target.procedure,
|
|
97
|
-
protocol: target.protocol,
|
|
98
|
-
implementation: target.implementation.name,
|
|
99
|
-
method: target.procedure,
|
|
100
|
-
});
|
|
101
106
|
}
|
|
107
|
+
const implementations = [...new Set(descriptors)].map((implementation) => ({
|
|
108
|
+
id: idImplementation(implementation),
|
|
109
|
+
name: implementation.name,
|
|
110
|
+
contract: idContract(implementation.contract),
|
|
111
|
+
protocol: implementation.protocol,
|
|
112
|
+
procedures: implementation.procedures,
|
|
113
|
+
}));
|
|
102
114
|
const dependencyGraph = buildDependencyGraph(modules, descriptors, targets, tasks, diagnostics);
|
|
103
115
|
const probedTokenIds = dependencyGraph.nodes
|
|
104
116
|
.filter((node) => node.kind === 'token')
|
|
@@ -150,7 +162,7 @@ export function compileApplication(input) {
|
|
|
150
162
|
})),
|
|
151
163
|
tokens: [...new Set([...tokensById.keys(), ...probedTokenIds])].map((id) => ({ id })),
|
|
152
164
|
contextKeys: [...contextKeysByName.keys()].map((name) => ({ name })),
|
|
153
|
-
contracts: [...
|
|
165
|
+
contracts: [...contractIds.entries()].map(([contract, id]) => toContractIR(contract, id)),
|
|
154
166
|
pipelines,
|
|
155
167
|
implementations,
|
|
156
168
|
tasks: tasks.map((task) => ({
|
|
@@ -161,12 +173,12 @@ export function compileApplication(input) {
|
|
|
161
173
|
queues: queues.map(toQueueIR),
|
|
162
174
|
executions: [
|
|
163
175
|
...targets.map((target) => ({
|
|
164
|
-
id: `protocol:${target.
|
|
176
|
+
id: `protocol:${target.contractId}:${target.procedure}:${target.protocol}`,
|
|
165
177
|
kind: 'protocol',
|
|
166
178
|
protocol: target.protocol,
|
|
167
|
-
contract: target.
|
|
179
|
+
contract: target.contractId,
|
|
168
180
|
procedure: target.procedure,
|
|
169
|
-
implementation: target.
|
|
181
|
+
implementation: target.implementationId,
|
|
170
182
|
})),
|
|
171
183
|
...publicTasks.map((task) => ({
|
|
172
184
|
id: `task:${task.name}`,
|
|
@@ -177,7 +189,7 @@ export function compileApplication(input) {
|
|
|
177
189
|
],
|
|
178
190
|
capabilities: [
|
|
179
191
|
...targets.flatMap((target) => {
|
|
180
|
-
const requiredBy = `${target.
|
|
192
|
+
const requiredBy = `${target.contractId}.${target.procedure}`;
|
|
181
193
|
return [
|
|
182
194
|
{ name: 'crypto.random', scope: 'execution', requiredBy },
|
|
183
195
|
...(target.protocol === 'http'
|
|
@@ -257,7 +269,7 @@ function validateDispatchKeys(targets, diagnostics) {
|
|
|
257
269
|
for (const target of targets) {
|
|
258
270
|
if (target.dispatchKey === null)
|
|
259
271
|
continue;
|
|
260
|
-
const path = `${target.
|
|
272
|
+
const path = `${target.contractId}.${target.procedure}.${target.protocol}`;
|
|
261
273
|
const existing = targetsByKey.get(target.dispatchKey);
|
|
262
274
|
if (existing) {
|
|
263
275
|
if (existing.implementation.contract === target.implementation.contract &&
|
|
@@ -265,7 +277,7 @@ function validateDispatchKeys(targets, diagnostics) {
|
|
|
265
277
|
existing.protocol === target.protocol) {
|
|
266
278
|
continue;
|
|
267
279
|
}
|
|
268
|
-
const existingPath = `${existing.
|
|
280
|
+
const existingPath = `${existing.contractId}.${existing.procedure}.${existing.protocol}`;
|
|
269
281
|
diagnostics.push({
|
|
270
282
|
code: 'LUTRE_PROTOCOL_001',
|
|
271
283
|
message: `Duplicate protocol dispatch key "${target.dispatchKey}": ${existingPath}, ${path}`,
|
|
@@ -736,7 +748,7 @@ function buildDependencyGraph(modules, descriptors, targets, tasks, diagnostics)
|
|
|
736
748
|
const definition = layerDefinitionOf(item);
|
|
737
749
|
const consumer = {
|
|
738
750
|
kind: 'layer-consumer',
|
|
739
|
-
id: `layer:${target.
|
|
751
|
+
id: `layer:${target.contractId}/${target.procedure}/${target.protocol}/${indexPath.join('.')}`,
|
|
740
752
|
name: definition.name,
|
|
741
753
|
};
|
|
742
754
|
if (!nodes.some(({ id }) => id === consumer.id)) {
|
|
@@ -847,7 +859,7 @@ function collectCustomTokens(providers, targets, diagnostics) {
|
|
|
847
859
|
function collectContextKeys(targets, diagnostics) {
|
|
848
860
|
const keys = new Map();
|
|
849
861
|
for (const target of targets) {
|
|
850
|
-
const path = `${target.
|
|
862
|
+
const path = `${target.contractId}.${target.procedure}.${target.protocol}`;
|
|
851
863
|
visitPipelineItems(target.pipeline, (item) => {
|
|
852
864
|
if (item.kind !== 'layer')
|
|
853
865
|
return;
|
|
@@ -867,6 +879,19 @@ function collectContextKeys(targets, diagnostics) {
|
|
|
867
879
|
}
|
|
868
880
|
return keys;
|
|
869
881
|
}
|
|
882
|
+
function toContractIR(contract, id) {
|
|
883
|
+
return {
|
|
884
|
+
id,
|
|
885
|
+
procedures: Object.entries(contract.procedures).map(([procedureName, procedure]) => ({
|
|
886
|
+
name: procedureName,
|
|
887
|
+
protocols: Object.entries(procedure.protocols).map(([protocolName, protocol]) => ({
|
|
888
|
+
name: protocolName,
|
|
889
|
+
dispatchKey: protocol.dispatchKey,
|
|
890
|
+
interaction: protocol.interaction ?? 'unary',
|
|
891
|
+
})),
|
|
892
|
+
})),
|
|
893
|
+
};
|
|
894
|
+
}
|
|
870
895
|
export function assertValidCompilation(result) {
|
|
871
896
|
if (result.diagnostics.length > 0) {
|
|
872
897
|
throw new StaticValidationError(result.diagnostics, result.graph);
|
|
@@ -889,7 +914,7 @@ function collectModules(roots) {
|
|
|
889
914
|
visit(root);
|
|
890
915
|
return modules;
|
|
891
916
|
}
|
|
892
|
-
function validateCoverage(implementations,
|
|
917
|
+
function validateCoverage(implementations, contractIds, diagnostics) {
|
|
893
918
|
const contracts = new Set(implementations.map((implementation) => implementation.contract));
|
|
894
919
|
for (const contract of contracts) {
|
|
895
920
|
const protocols = new Set(Object.values(contract.procedures).flatMap((procedure) => Object.keys(procedure.protocols)));
|
|
@@ -900,7 +925,7 @@ function validateCoverage(implementations, contractNames, diagnostics) {
|
|
|
900
925
|
const covering = implementations.filter((implementation) => implementation.contract === contract &&
|
|
901
926
|
implementation.protocol === protocol &&
|
|
902
927
|
implementation.procedures.includes(procedureName));
|
|
903
|
-
const path = `${
|
|
928
|
+
const path = `${contractIds.get(contract) ?? 'contract:unknown'}.${procedureName}.${protocol}`;
|
|
904
929
|
if (covering.length === 0) {
|
|
905
930
|
diagnostics.push({
|
|
906
931
|
code: 'LUTRE_IMPL_001',
|
|
@@ -920,7 +945,7 @@ function validateCoverage(implementations, contractNames, diagnostics) {
|
|
|
920
945
|
}
|
|
921
946
|
}
|
|
922
947
|
function validatePipeline(target, diagnostics) {
|
|
923
|
-
const path = `${target.
|
|
948
|
+
const path = `${target.contractId}.${target.procedure}.${target.protocol}`;
|
|
924
949
|
const flattened = [];
|
|
925
950
|
visitPipelineItems(target.pipeline, (item, indexPath) => {
|
|
926
951
|
flattened.push({ item, indexPath });
|
package/dist/graph/index.d.ts
CHANGED
package/dist/graph/index.js
CHANGED
package/dist/graph/ir.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface DependencyNodeIR {
|
|
|
21
21
|
readonly kind: 'class' | 'token' | 'factory' | 'conditional' | 'environment' | 'arguments' | 'implementation' | 'layer' | 'task' | 'framework';
|
|
22
22
|
readonly scope?: 'application' | 'transient';
|
|
23
23
|
readonly module?: string;
|
|
24
|
+
readonly visibility?: 'private' | 'exported';
|
|
24
25
|
}
|
|
25
26
|
export interface DependencyEdgeIR {
|
|
26
27
|
readonly from: string;
|
|
@@ -57,18 +58,33 @@ export interface LayerIR {
|
|
|
57
58
|
readonly response?: Readonly<Record<string, unknown>>;
|
|
58
59
|
}[];
|
|
59
60
|
}
|
|
61
|
+
export type ContractId = `contract:${number}`;
|
|
62
|
+
export type ImplementationId = `implementation:${number}`;
|
|
63
|
+
export interface ContractProtocolIR {
|
|
64
|
+
readonly name: string;
|
|
65
|
+
readonly dispatchKey: string | null;
|
|
66
|
+
readonly interaction: string;
|
|
67
|
+
}
|
|
68
|
+
export interface ContractProcedureIR {
|
|
69
|
+
readonly name: string;
|
|
70
|
+
readonly protocols: readonly ContractProtocolIR[];
|
|
71
|
+
}
|
|
72
|
+
export interface ContractIR {
|
|
73
|
+
readonly id: ContractId;
|
|
74
|
+
readonly procedures: readonly ContractProcedureIR[];
|
|
75
|
+
}
|
|
60
76
|
export interface PipelineIR {
|
|
61
|
-
readonly contract:
|
|
77
|
+
readonly contract: ContractId;
|
|
62
78
|
readonly procedure: string;
|
|
63
79
|
readonly protocol: string;
|
|
64
80
|
readonly layers: readonly LayerIR[];
|
|
65
81
|
}
|
|
66
82
|
export interface ImplementationIR {
|
|
67
|
-
readonly
|
|
68
|
-
readonly
|
|
83
|
+
readonly id: ImplementationId;
|
|
84
|
+
readonly name: string;
|
|
85
|
+
readonly contract: ContractId;
|
|
69
86
|
readonly protocol: string;
|
|
70
|
-
readonly
|
|
71
|
-
readonly method: string;
|
|
87
|
+
readonly procedures: readonly string[];
|
|
72
88
|
}
|
|
73
89
|
export interface CapabilityIR {
|
|
74
90
|
readonly name: string;
|
|
@@ -79,9 +95,9 @@ export interface ProtocolExecutionRootIR {
|
|
|
79
95
|
readonly id: `protocol:${string}`;
|
|
80
96
|
readonly kind: 'protocol';
|
|
81
97
|
readonly protocol: string;
|
|
82
|
-
readonly contract:
|
|
98
|
+
readonly contract: ContractId;
|
|
83
99
|
readonly procedure: string;
|
|
84
|
-
readonly implementation:
|
|
100
|
+
readonly implementation: ImplementationId;
|
|
85
101
|
}
|
|
86
102
|
export interface ApplicationArgumentsIR {
|
|
87
103
|
readonly name: string;
|
|
@@ -136,7 +152,7 @@ export interface ApplicationGraphIR {
|
|
|
136
152
|
readonly providers: readonly ProviderIR[];
|
|
137
153
|
readonly tokens: readonly TokenIR[];
|
|
138
154
|
readonly contextKeys: readonly ContextKeyIR[];
|
|
139
|
-
readonly contracts: readonly
|
|
155
|
+
readonly contracts: readonly ContractIR[];
|
|
140
156
|
readonly pipelines: readonly PipelineIR[];
|
|
141
157
|
readonly implementations: readonly ImplementationIR[];
|
|
142
158
|
readonly tasks: readonly TaskIR[];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type ContractDefinition, type ProcedureDefinition, type SchemaInput, type SchemaOutput, type StandardSchemaV1 } from '../core/index.js';
|
|
2
|
+
import type { HttpHeaders, HttpParamsSchemas, HttpProtocol, HttpProtocolDefinition, HttpRequestBodyDefinition, HttpResponseDefinition } from './definitions.js';
|
|
3
|
+
import { type PathParamNames, type RawPathParams } from './path.js';
|
|
4
|
+
export interface HttpClientTransportRequest {
|
|
5
|
+
readonly method: string;
|
|
6
|
+
readonly path: string;
|
|
7
|
+
readonly query?: unknown;
|
|
8
|
+
readonly headers?: unknown;
|
|
9
|
+
readonly body?: unknown;
|
|
10
|
+
readonly contentType?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface HttpClientTransportResponse {
|
|
13
|
+
readonly status: number;
|
|
14
|
+
readonly body: unknown;
|
|
15
|
+
readonly headers?: HttpHeaders;
|
|
16
|
+
}
|
|
17
|
+
export type HttpClientTransport = (request: HttpClientTransportRequest) => Promise<HttpClientTransportResponse>;
|
|
18
|
+
type HttpProtocolOf<TProcedure extends ProcedureDefinition> = TProcedure['protocols'] extends {
|
|
19
|
+
readonly http: infer TProtocol;
|
|
20
|
+
} ? TProtocol extends HttpProtocol<infer TDefinition> ? HttpProtocol<TDefinition> : never : never;
|
|
21
|
+
type HttpProcedureNames<TContract extends ContractDefinition> = {
|
|
22
|
+
[TProcedure in keyof TContract['procedures'] & string]: HttpProtocolOf<TContract['procedures'][TProcedure]> extends never ? never : TProcedure;
|
|
23
|
+
}[keyof TContract['procedures'] & string];
|
|
24
|
+
type HttpDefinitionOf<TContract extends ContractDefinition, TProcedure extends HttpProcedureNames<TContract>> = HttpProtocolOf<TContract['procedures'][TProcedure]>['definition'];
|
|
25
|
+
type RequestBodySchema<TBody> = TBody extends HttpRequestBodyDefinition<infer TSchema> ? TSchema : never;
|
|
26
|
+
type PathParamsInput<TDefinition extends HttpProtocolDefinition> = PathParamNames<TDefinition['path']> extends never ? never : TDefinition['request'] extends {
|
|
27
|
+
readonly params: infer TSchemas extends HttpParamsSchemas;
|
|
28
|
+
} ? Readonly<{
|
|
29
|
+
[TName in keyof TSchemas]: SchemaInput<TSchemas[TName]>;
|
|
30
|
+
}> : RawPathParams<TDefinition['path']>;
|
|
31
|
+
type RequestPartInput<TDefinition extends HttpProtocolDefinition, TPart extends 'query' | 'headers'> = TDefinition['request'] extends infer TRequest ? TRequest extends Record<TPart, infer TSchema extends StandardSchemaV1> ? SchemaInput<TSchema> : never : never;
|
|
32
|
+
type RequestBodyInput<TDefinition extends HttpProtocolDefinition> = TDefinition['request'] extends {
|
|
33
|
+
readonly body: infer TBody extends HttpRequestBodyDefinition;
|
|
34
|
+
} ? SchemaInput<RequestBodySchema<TBody>> : never;
|
|
35
|
+
type RequestField<TName extends string, TValue> = [TValue] extends [never] ? object : undefined extends TValue ? {
|
|
36
|
+
readonly [TKey in TName]?: Exclude<TValue, undefined>;
|
|
37
|
+
} : {
|
|
38
|
+
readonly [TKey in TName]: TValue;
|
|
39
|
+
};
|
|
40
|
+
export type HttpClientRequest<TDefinition extends HttpProtocolDefinition> = RequestField<'params', PathParamsInput<TDefinition>> & RequestField<'query', RequestPartInput<TDefinition, 'query'>> & RequestField<'headers', RequestPartInput<TDefinition, 'headers'>> & RequestField<'body', RequestBodyInput<TDefinition>>;
|
|
41
|
+
type ResponseBodyOutput<TResponse extends HttpResponseDefinition> = TResponse extends {
|
|
42
|
+
readonly stream: 'server';
|
|
43
|
+
} ? AsyncIterable<SchemaOutput<TResponse['body']>> : SchemaOutput<TResponse['body']>;
|
|
44
|
+
type ResponseHeadersOutput<TResponse extends HttpResponseDefinition> = TResponse extends {
|
|
45
|
+
readonly headers: infer THeaders extends StandardSchemaV1;
|
|
46
|
+
} ? SchemaOutput<THeaders> : HttpHeaders;
|
|
47
|
+
export type HttpClientResponse<TDefinition extends HttpProtocolDefinition> = {
|
|
48
|
+
[TVariant in keyof TDefinition['responses'] & string]: NonNullable<TDefinition['responses'][TVariant]> extends infer TResponse extends HttpResponseDefinition ? {
|
|
49
|
+
readonly status: TResponse['status'];
|
|
50
|
+
readonly body: ResponseBodyOutput<TResponse>;
|
|
51
|
+
readonly headers: ResponseHeadersOutput<TResponse>;
|
|
52
|
+
} : never;
|
|
53
|
+
}[keyof TDefinition['responses'] & string];
|
|
54
|
+
type HttpClientMethod<TDefinition extends HttpProtocolDefinition> = keyof HttpClientRequest<TDefinition> extends never ? () => Promise<HttpClientResponse<TDefinition>> : (request: HttpClientRequest<TDefinition>) => Promise<HttpClientResponse<TDefinition>>;
|
|
55
|
+
export type HttpClient<TContract extends ContractDefinition> = {
|
|
56
|
+
readonly [TProcedure in HttpProcedureNames<TContract>]: HttpClientMethod<HttpDefinitionOf<TContract, TProcedure>>;
|
|
57
|
+
};
|
|
58
|
+
export declare class HttpClientResponseError extends Error {
|
|
59
|
+
readonly status: number;
|
|
60
|
+
readonly method: string;
|
|
61
|
+
readonly path: string;
|
|
62
|
+
readonly procedure: string;
|
|
63
|
+
constructor(status: number, method: string, path: string, procedure: string, message: string, options?: ErrorOptions);
|
|
64
|
+
}
|
|
65
|
+
export declare function createHttpClient<const TContract extends ContractDefinition>(contract: TContract, transport: HttpClientTransport): HttpClient<TContract>;
|
|
66
|
+
export interface FetchHttpTransportOptions {
|
|
67
|
+
readonly baseUrl: string | URL;
|
|
68
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
69
|
+
readonly headers?: HeadersInit;
|
|
70
|
+
}
|
|
71
|
+
export declare function fetchHttpTransport(options: FetchHttpTransportOptions): HttpClientTransport;
|
|
72
|
+
export {};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { SchemaValidationError, validateSchema, } from '../core/index.js';
|
|
2
|
+
import { parseHttpPath, } from './path.js';
|
|
3
|
+
export class HttpClientResponseError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
method;
|
|
6
|
+
path;
|
|
7
|
+
procedure;
|
|
8
|
+
constructor(status, method, path, procedure, message, options) {
|
|
9
|
+
super(message, options);
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.method = method;
|
|
12
|
+
this.path = path;
|
|
13
|
+
this.procedure = procedure;
|
|
14
|
+
this.name = 'HttpClientResponseError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function createHttpClient(contract, transport) {
|
|
18
|
+
const client = {};
|
|
19
|
+
for (const [procedureName, procedure] of Object.entries(contract.procedures)) {
|
|
20
|
+
const definition = httpDefinitionOf(procedure);
|
|
21
|
+
if (!definition)
|
|
22
|
+
continue;
|
|
23
|
+
client[procedureName] = async (request = {}) => {
|
|
24
|
+
const response = await transport({
|
|
25
|
+
method: definition.method,
|
|
26
|
+
path: interpolatePath(definition.path, request.params),
|
|
27
|
+
...(request.query === undefined ? {} : { query: request.query }),
|
|
28
|
+
...(request.headers === undefined ? {} : { headers: request.headers }),
|
|
29
|
+
...(request.body === undefined ? {} : { body: request.body }),
|
|
30
|
+
...(definition.request?.body === undefined
|
|
31
|
+
? {}
|
|
32
|
+
: { contentType: definition.request.body.contentType }),
|
|
33
|
+
});
|
|
34
|
+
return decodeResponse(procedureName, definition, response);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return Object.freeze(client);
|
|
38
|
+
}
|
|
39
|
+
function httpDefinitionOf(procedure) {
|
|
40
|
+
const protocol = procedure.protocols.http;
|
|
41
|
+
if (!protocol ||
|
|
42
|
+
protocol.protocol !== 'http' ||
|
|
43
|
+
!('definition' in protocol)) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return protocol.definition;
|
|
47
|
+
}
|
|
48
|
+
function interpolatePath(path, params) {
|
|
49
|
+
const segments = parseHttpPath(path);
|
|
50
|
+
if (segments.length === 0)
|
|
51
|
+
return '/';
|
|
52
|
+
return `/${segments
|
|
53
|
+
.map((segment) => {
|
|
54
|
+
if (segment.kind === 'static')
|
|
55
|
+
return segment.value;
|
|
56
|
+
const value = params?.[segment.name];
|
|
57
|
+
if (value === undefined) {
|
|
58
|
+
throw new Error(`Missing HTTP path parameter: ${segment.name}`);
|
|
59
|
+
}
|
|
60
|
+
return encodeURIComponent(String(value));
|
|
61
|
+
})
|
|
62
|
+
.join('/')}`;
|
|
63
|
+
}
|
|
64
|
+
async function decodeResponse(procedure, definition, response) {
|
|
65
|
+
const target = describeHttpTarget(definition, procedure);
|
|
66
|
+
const candidates = Object.values(definition.responses).filter((candidate) => candidate.status === response.status);
|
|
67
|
+
if (candidates.length === 0) {
|
|
68
|
+
throw new HttpClientResponseError(response.status, definition.method, definition.path, procedure, `${target} returned undeclared HTTP status ${response.status}`);
|
|
69
|
+
}
|
|
70
|
+
let lastValidationError;
|
|
71
|
+
for (const candidate of candidates) {
|
|
72
|
+
try {
|
|
73
|
+
const body = candidate.stream
|
|
74
|
+
? validatedResponseStream(candidate.body, response.body, procedure, definition, response.status)
|
|
75
|
+
: await validateSchema(candidate.body, response.body);
|
|
76
|
+
const headers = candidate.headers
|
|
77
|
+
? await validateSchema(candidate.headers, response.headers ?? {})
|
|
78
|
+
: (response.headers ?? {});
|
|
79
|
+
return Object.freeze({ status: response.status, body, headers });
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (!(error instanceof SchemaValidationError))
|
|
83
|
+
throw error;
|
|
84
|
+
lastValidationError = error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
throw new HttpClientResponseError(response.status, definition.method, definition.path, procedure, `${target} returned a response that does not match its Contract`, lastValidationError ? { cause: lastValidationError } : undefined);
|
|
88
|
+
}
|
|
89
|
+
function validatedResponseStream(schema, value, procedure, definition, status) {
|
|
90
|
+
const target = describeHttpTarget(definition, procedure);
|
|
91
|
+
if (!isAsyncIterable(value)) {
|
|
92
|
+
throw new HttpClientResponseError(status, definition.method, definition.path, procedure, `${target} returned a non-stream body for a server-stream response`);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
async *[Symbol.asyncIterator]() {
|
|
96
|
+
try {
|
|
97
|
+
for await (const item of value)
|
|
98
|
+
yield await validateSchema(schema, item);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
throw new HttpClientResponseError(status, definition.method, definition.path, procedure, `${target} returned an invalid server-stream item`, { cause: error });
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function describeHttpTarget(definition, procedure) {
|
|
107
|
+
return `${definition.method.toUpperCase()} ${definition.path} (${procedure})`;
|
|
108
|
+
}
|
|
109
|
+
function isAsyncIterable(value) {
|
|
110
|
+
return (typeof value === 'object' &&
|
|
111
|
+
value !== null &&
|
|
112
|
+
Symbol.asyncIterator in value &&
|
|
113
|
+
typeof value[Symbol.asyncIterator] === 'function');
|
|
114
|
+
}
|
|
115
|
+
export function fetchHttpTransport(options) {
|
|
116
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
117
|
+
return async (request) => {
|
|
118
|
+
const url = new URL(request.path, options.baseUrl);
|
|
119
|
+
appendQuery(url.searchParams, request.query);
|
|
120
|
+
const headers = new Headers(options.headers);
|
|
121
|
+
appendHeaders(headers, request.headers);
|
|
122
|
+
if (request.contentType && !headers.has('content-type')) {
|
|
123
|
+
headers.set('content-type', request.contentType);
|
|
124
|
+
}
|
|
125
|
+
const response = await fetchImplementation(url, {
|
|
126
|
+
method: request.method,
|
|
127
|
+
headers,
|
|
128
|
+
...(request.body === undefined
|
|
129
|
+
? {}
|
|
130
|
+
: { body: encodeBody(request.body, request.contentType) }),
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
status: response.status,
|
|
134
|
+
body: await decodeFetchBody(response),
|
|
135
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function appendQuery(searchParams, query) {
|
|
140
|
+
if (query === undefined || query === null)
|
|
141
|
+
return;
|
|
142
|
+
if (query instanceof URLSearchParams) {
|
|
143
|
+
for (const [key, value] of query)
|
|
144
|
+
searchParams.append(key, value);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (typeof query !== 'object') {
|
|
148
|
+
throw new TypeError('HTTP client query must be an object or URLSearchParams');
|
|
149
|
+
}
|
|
150
|
+
for (const [key, value] of Object.entries(query)) {
|
|
151
|
+
if (value === undefined)
|
|
152
|
+
continue;
|
|
153
|
+
if (Array.isArray(value)) {
|
|
154
|
+
for (const item of value)
|
|
155
|
+
searchParams.append(key, String(item));
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
searchParams.append(key, String(value));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function appendHeaders(headers, value) {
|
|
163
|
+
if (value === undefined || value === null)
|
|
164
|
+
return;
|
|
165
|
+
if (value instanceof Headers) {
|
|
166
|
+
for (const [name, header] of value)
|
|
167
|
+
headers.set(name, header);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (typeof value !== 'object') {
|
|
171
|
+
throw new TypeError('HTTP client headers must be an object or Headers');
|
|
172
|
+
}
|
|
173
|
+
for (const [name, header] of Object.entries(value)) {
|
|
174
|
+
if (header === undefined)
|
|
175
|
+
continue;
|
|
176
|
+
if (Array.isArray(header)) {
|
|
177
|
+
headers.set(name, header.map(String).join(', '));
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
headers.set(name, String(header));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function encodeBody(body, contentType) {
|
|
185
|
+
const mediaType = normalizeMediaType(contentType);
|
|
186
|
+
if (mediaType === 'application/json' || mediaType?.endsWith('+json')) {
|
|
187
|
+
return JSON.stringify(body);
|
|
188
|
+
}
|
|
189
|
+
return body;
|
|
190
|
+
}
|
|
191
|
+
async function decodeFetchBody(response) {
|
|
192
|
+
if (response.status === 204 || response.status === 205)
|
|
193
|
+
return undefined;
|
|
194
|
+
const mediaType = normalizeMediaType(response.headers.get('content-type'));
|
|
195
|
+
if (mediaType === 'text/event-stream') {
|
|
196
|
+
if (!response.body) {
|
|
197
|
+
throw new Error('Server-stream HTTP response has no body');
|
|
198
|
+
}
|
|
199
|
+
return decodeServerSentEvents(response.body);
|
|
200
|
+
}
|
|
201
|
+
if (mediaType === 'application/json' || mediaType?.endsWith('+json')) {
|
|
202
|
+
return response.json();
|
|
203
|
+
}
|
|
204
|
+
return response.text();
|
|
205
|
+
}
|
|
206
|
+
function normalizeMediaType(value) {
|
|
207
|
+
const mediaType = value?.split(';', 1)[0]?.trim().toLowerCase();
|
|
208
|
+
return mediaType || undefined;
|
|
209
|
+
}
|
|
210
|
+
async function* decodeServerSentEvents(stream) {
|
|
211
|
+
const reader = stream.getReader();
|
|
212
|
+
const decoder = new TextDecoder();
|
|
213
|
+
let buffer = '';
|
|
214
|
+
let completed = false;
|
|
215
|
+
try {
|
|
216
|
+
while (true) {
|
|
217
|
+
const next = await reader.read();
|
|
218
|
+
if (next.done) {
|
|
219
|
+
completed = true;
|
|
220
|
+
buffer += decoder.decode();
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
buffer += decoder.decode(next.value, { stream: true });
|
|
224
|
+
buffer = normalizeEventStreamNewlines(buffer);
|
|
225
|
+
let boundary = buffer.indexOf('\n\n');
|
|
226
|
+
while (boundary >= 0) {
|
|
227
|
+
const event = buffer.slice(0, boundary);
|
|
228
|
+
buffer = buffer.slice(boundary + 2);
|
|
229
|
+
const data = event
|
|
230
|
+
.split('\n')
|
|
231
|
+
.filter((line) => line.startsWith('data:'))
|
|
232
|
+
.map((line) => line.slice('data:'.length).trimStart())
|
|
233
|
+
.join('\n');
|
|
234
|
+
if (data.length > 0)
|
|
235
|
+
yield JSON.parse(data);
|
|
236
|
+
boundary = buffer.indexOf('\n\n');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
buffer = normalizeEventStreamNewlines(buffer).trim();
|
|
240
|
+
if (buffer.length > 0) {
|
|
241
|
+
const data = buffer
|
|
242
|
+
.split('\n')
|
|
243
|
+
.filter((line) => line.startsWith('data:'))
|
|
244
|
+
.map((line) => line.slice('data:'.length).trimStart())
|
|
245
|
+
.join('\n');
|
|
246
|
+
if (data.length > 0)
|
|
247
|
+
yield JSON.parse(data);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
if (!completed)
|
|
252
|
+
await reader.cancel();
|
|
253
|
+
reader.releaseLock();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function normalizeEventStreamNewlines(value) {
|
|
257
|
+
return value.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
|
258
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ContractDefinition, ContextProvidedBeforeTerminal, HasValidationBeforeTerminal, IsValidProtocolPipeline, PipelineItem, ProtocolDescriptor, SchemaInput, SchemaOutput, ShortCircuitDeclarationsOf, ShortCircuitResultOf, StandardSchemaV1, TerminalLayerDescriptor, ValidationLayerDescriptor } from '../core/index.js';
|
|
1
|
+
import type { ContractDefinition, ContextProvidedBeforeTerminal, HasValidationBeforeTerminal, IsValidProtocolPipeline, PipelineItem, ProtocolDescriptor, ProtocolFactory, ProtocolGroup, SchemaInput, SchemaOutput, ShortCircuitDeclarationsOf, ShortCircuitResultOf, StandardSchemaV1, TerminalLayerDescriptor, ValidationLayerDescriptor } from '../core/index.js';
|
|
2
2
|
import type { Logger } from '../runtime/index.js';
|
|
3
3
|
import { type HttpDispatchKey, type IsValidHttpPath, type PathParamNames, type RawPathParams } from './path.js';
|
|
4
4
|
export type HttpParamsSchemas = Readonly<Record<string, StandardSchemaV1>>;
|
|
@@ -133,11 +133,19 @@ type AreErrorMappingsCompatible<TResponses extends HttpProtocolDefinition['respo
|
|
|
133
133
|
[TVariant in keyof TResponses]: IsErrorMappingCompatible<TResponses[TVariant]>;
|
|
134
134
|
}[keyof TResponses] ? false : true;
|
|
135
135
|
declare function defineHttp<const TDefinition extends HttpProtocolDefinition>(definition: TDefinition & HttpPipelineConstraint<TDefinition> & HttpResponseConstraint<TDefinition> & HttpPathConstraint<TDefinition>): HttpProtocol<TDefinition>;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
type HttpProtocolGroup<TDefinitions extends Record<string, HttpProtocolDefinition>> = ProtocolGroup<'http', {
|
|
137
|
+
[K in keyof TDefinitions]: HttpProtocol<TDefinitions[K]>;
|
|
138
|
+
}>;
|
|
139
|
+
type HttpDefinitionsConstraint<TDefinitions extends Record<string, HttpProtocolDefinition>> = {
|
|
140
|
+
[K in keyof TDefinitions]: TDefinitions[K] & HttpPipelineConstraint<TDefinitions[K]> & HttpResponseConstraint<TDefinitions[K]> & HttpPathConstraint<TDefinitions[K]>;
|
|
140
141
|
};
|
|
142
|
+
export interface HttpProtocolFactory extends ProtocolFactory<'http'> {
|
|
143
|
+
<const TDefinitions extends Record<string, HttpProtocolDefinition>>(definitions: TDefinitions & HttpDefinitionsConstraint<TDefinitions>): HttpProtocolGroup<TDefinitions>;
|
|
144
|
+
readonly route: typeof defineHttp;
|
|
145
|
+
readonly controller: TerminalLayerDescriptor<'http'>;
|
|
146
|
+
readonly error: typeof httpError;
|
|
147
|
+
}
|
|
148
|
+
export declare const http: HttpProtocolFactory;
|
|
141
149
|
export declare const validate: Readonly<{
|
|
142
150
|
params: ValidationLayerDescriptor & {
|
|
143
151
|
readonly part: "params";
|
package/dist/http/definitions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { childPipelineOf } from '../core/index.js';
|
|
1
|
+
import { childPipelineOf, protocolGroup } from '../core/index.js';
|
|
2
2
|
import { createHttpDispatchKey, parseHttpPath, } from './path.js';
|
|
3
3
|
const controller = Object.freeze({
|
|
4
4
|
kind: 'terminal',
|
|
@@ -37,8 +37,12 @@ function hasParamsValidation(pipeline) {
|
|
|
37
37
|
(item.kind === 'layer' &&
|
|
38
38
|
hasParamsValidation(childPipelineOf(item) ?? [])));
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
function defineHttpGroup(definitions) {
|
|
41
|
+
return protocolGroup('http', Object.fromEntries(Object.entries(definitions).map(([name, definition]) => [name, defineHttp(definition)])));
|
|
42
|
+
}
|
|
43
|
+
export const http = Object.assign(defineHttpGroup, {
|
|
41
44
|
protocol: 'http',
|
|
45
|
+
route: defineHttp,
|
|
42
46
|
controller,
|
|
43
47
|
error: httpError,
|
|
44
48
|
});
|
package/dist/http/index.d.ts
CHANGED
package/dist/http/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ContextProvidedBeforeTerminal, type IsValidProtocolPipeline, type ContractDefinition, type PipelineItem, type ProtocolDescriptor, type SchemaOutput, type StandardSchemaV1, type TerminalLayerDescriptor } from '../core/index.js';
|
|
1
|
+
import { type ContextProvidedBeforeTerminal, type IsValidProtocolPipeline, type ContractDefinition, type PipelineItem, type ProtocolDescriptor, type ProtocolFactory, type ProtocolGroup, type SchemaOutput, type StandardSchemaV1, type TerminalLayerDescriptor } from '../core/index.js';
|
|
2
2
|
import type { ApplicationGraphIR } from '../graph/index.js';
|
|
3
3
|
import { ApplicationRuntime, Logger } from '../runtime/index.js';
|
|
4
4
|
export interface MessagePortResponseDefinition {
|
|
@@ -17,10 +17,18 @@ declare function defineMessagePort<const TDefinition extends MessagePortProtocol
|
|
|
17
17
|
type MessagePortPipelineConstraint<TDefinition extends MessagePortProtocolDefinition> = IsValidProtocolPipeline<TDefinition['pipeline'], 'messagePort'> extends true ? unknown : {
|
|
18
18
|
readonly pipeline: never;
|
|
19
19
|
};
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
type MessagePortProtocolGroup<TDefinitions extends Record<string, MessagePortProtocolDefinition>> = ProtocolGroup<'messagePort', {
|
|
21
|
+
[K in keyof TDefinitions]: MessagePortProtocol<TDefinitions[K]>;
|
|
22
|
+
}>;
|
|
23
|
+
type MessagePortDefinitionsConstraint<TDefinitions extends Record<string, MessagePortProtocolDefinition>> = {
|
|
24
|
+
[K in keyof TDefinitions]: TDefinitions[K] & MessagePortPipelineConstraint<TDefinitions[K]>;
|
|
23
25
|
};
|
|
26
|
+
export interface MessagePortProtocolFactory extends ProtocolFactory<'messagePort'> {
|
|
27
|
+
<const TDefinitions extends Record<string, MessagePortProtocolDefinition>>(definitions: TDefinitions & MessagePortDefinitionsConstraint<TDefinitions>): MessagePortProtocolGroup<TDefinitions>;
|
|
28
|
+
readonly route: typeof defineMessagePort;
|
|
29
|
+
readonly handler: TerminalLayerDescriptor<'messagePort'>;
|
|
30
|
+
}
|
|
31
|
+
export declare const messagePort: MessagePortProtocolFactory;
|
|
24
32
|
export interface LogicalMessagePortResult<TVariant extends string = string, TValue = unknown> {
|
|
25
33
|
readonly kind: 'message-port-result';
|
|
26
34
|
readonly variant: TVariant;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { validateSchema, } from '../core/index.js';
|
|
1
|
+
import { validateSchema, protocolGroup, } from '../core/index.js';
|
|
2
2
|
import { executePipeline, Logger, normalizeUnknownError, } from '../runtime/index.js';
|
|
3
3
|
const handler = Object.freeze({
|
|
4
4
|
kind: 'terminal',
|
|
@@ -15,8 +15,15 @@ function defineMessagePort(definition) {
|
|
|
15
15
|
definition,
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
function defineMessagePortGroup(definitions) {
|
|
19
|
+
return protocolGroup('messagePort', Object.fromEntries(Object.entries(definitions).map(([name, definition]) => [
|
|
20
|
+
name,
|
|
21
|
+
defineMessagePort(definition),
|
|
22
|
+
])));
|
|
23
|
+
}
|
|
24
|
+
export const messagePort = Object.assign(defineMessagePortGroup, {
|
|
19
25
|
protocol: 'messagePort',
|
|
26
|
+
route: defineMessagePort,
|
|
20
27
|
handler,
|
|
21
28
|
});
|
|
22
29
|
export function createMessagePortExecution(options) {
|