@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.
- package/dist/adapters/bun.d.ts +11 -8
- package/dist/adapters/bun.js +72 -50
- package/dist/adapters/deno.d.ts +9 -7
- package/dist/adapters/deno.js +75 -53
- package/dist/application/binding.js +3 -0
- package/dist/application/index.d.ts +2 -1
- 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/dist/runtime/application.d.ts +2 -1
- package/dist/runtime/application.js +18 -3
- package/dist/runtime/di.d.ts +1 -0
- package/dist/runtime/di.js +31 -0
- package/package.json +1 -1
|
@@ -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) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ArgsClass, type EnvClass, type ModuleInstance, type ModuleTemplate, type TaskArguments, type TaskDescriptor, type TaskOutput } from '../core/index.js';
|
|
1
|
+
import { type ArgsClass, type EnvClass, type ModuleInstance, type ModuleTemplate, type TaskArguments, type TaskDescriptor, type TaskOutput, type TokenLike } from '../core/index.js';
|
|
2
2
|
import { Container, type RuntimeModuleGraph } from './di.js';
|
|
3
3
|
import { Logger } from './logger.js';
|
|
4
4
|
export interface ApplicationRuntimeOptions {
|
|
@@ -27,6 +27,7 @@ export declare class ApplicationRuntime {
|
|
|
27
27
|
readonly container: Container;
|
|
28
28
|
constructor(roots: readonly (ModuleInstance | ModuleTemplate<void>)[], options?: ApplicationRuntimeOptions);
|
|
29
29
|
initialize(): Promise<void>;
|
|
30
|
+
get<T>(token: TokenLike<T>): T;
|
|
30
31
|
run<TTask extends TaskDescriptor<any, any>>(task: TTask, ...args: TaskArguments<TTask>): Promise<TaskOutput<TTask>>;
|
|
31
32
|
/** public run()ではTrigger専用Taskを拒否するため、Trigger Host専用経路を分ける。 */
|
|
32
33
|
runTask<TTask extends TaskDescriptor<any, any>>(task: TTask, ...args: TaskArguments<TTask>): Promise<TaskOutput<TTask>>;
|
|
@@ -164,6 +164,18 @@ export class ApplicationRuntime {
|
|
|
164
164
|
}
|
|
165
165
|
this.#prepared = true;
|
|
166
166
|
}
|
|
167
|
+
get(token) {
|
|
168
|
+
if (this.#state === 'created' || this.#state === 'initializing') {
|
|
169
|
+
throw applicationStateError('LUTRE_APP_NOT_INITIALIZED');
|
|
170
|
+
}
|
|
171
|
+
if (this.#state === 'stopping') {
|
|
172
|
+
throw applicationStateError('LUTRE_APP_STOPPING');
|
|
173
|
+
}
|
|
174
|
+
if (this.#state === 'stopped') {
|
|
175
|
+
throw applicationStateError('LUTRE_APP_STOPPED');
|
|
176
|
+
}
|
|
177
|
+
return this.container.get(token);
|
|
178
|
+
}
|
|
167
179
|
run(task, ...args) {
|
|
168
180
|
if (!this.#publicTasks.has(task)) {
|
|
169
181
|
return Promise.reject(new Error(`LUTRE_TASK_002: Task ${task.name} is not registered in Application.tasks.`));
|
|
@@ -283,9 +295,12 @@ export class ApplicationRuntime {
|
|
|
283
295
|
}
|
|
284
296
|
}
|
|
285
297
|
function applicationStateError(code) {
|
|
286
|
-
|
|
287
|
-
?
|
|
288
|
-
:
|
|
298
|
+
const message = code === 'LUTRE_APP_NOT_INITIALIZED'
|
|
299
|
+
? 'Application is not initialized.'
|
|
300
|
+
: code === 'LUTRE_APP_STOPPING'
|
|
301
|
+
? 'Application is stopping and cannot accept new executions.'
|
|
302
|
+
: 'Application is stopped and cannot accept new executions.';
|
|
303
|
+
return new Error(`${code}: ${message}`);
|
|
289
304
|
}
|
|
290
305
|
function collectApplicationPipelines(modules) {
|
|
291
306
|
const pipelines = [];
|
package/dist/runtime/di.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export declare class Container {
|
|
|
24
24
|
bindEnvironment(environment: EnvClass, value: object): void;
|
|
25
25
|
bindArguments(argumentsContract: ArgsClass, value: object): void;
|
|
26
26
|
resolve<T>(token: TokenLike<T>): T;
|
|
27
|
+
get<T>(token: TokenLike<T>): T;
|
|
27
28
|
probeClass<T>(target: Class<T>): T;
|
|
28
29
|
prepareImplementation(implementation: ImplementationDescriptor): void;
|
|
29
30
|
implementationRuntime(implementation: ImplementationDescriptor): object;
|
package/dist/runtime/di.js
CHANGED
|
@@ -110,6 +110,37 @@ export class Container {
|
|
|
110
110
|
resolve(token) {
|
|
111
111
|
return this.#resolve(token);
|
|
112
112
|
}
|
|
113
|
+
get(token) {
|
|
114
|
+
const provider = this.#providers.get(token);
|
|
115
|
+
if (!provider) {
|
|
116
|
+
if (isEnvClass(token)) {
|
|
117
|
+
throw new DependencyResolutionError(`LUTRE_ENV_002: ${token.name} is not declared by any Module.environment.`);
|
|
118
|
+
}
|
|
119
|
+
if (isArgsClass(token)) {
|
|
120
|
+
throw new DependencyResolutionError(`LUTRE_ARGS_002: ${token.name} is not declared by Application.arguments.`);
|
|
121
|
+
}
|
|
122
|
+
throw new DependencyResolutionError(`LUTRE_DI_UNRESOLVED: Application requires ${tokenName(token)}, but no provider is declared for ${tokenName(token)}.`);
|
|
123
|
+
}
|
|
124
|
+
if (provider.scope !== 'application') {
|
|
125
|
+
throw new DependencyResolutionError(`LUTRE_DI_SCOPED_GET: ${tokenName(token)} is ${provider.scope}-scoped and cannot be retrieved with Application.get().`);
|
|
126
|
+
}
|
|
127
|
+
if (provider.kind === 'environment') {
|
|
128
|
+
if (this.#environment.has(provider.provide)) {
|
|
129
|
+
return this.#environment.get(provider.provide);
|
|
130
|
+
}
|
|
131
|
+
throw new DependencyResolutionError(`LUTRE_ENV_005: Environment ${provider.provide.name} requires a runtime Environment source before Application initialization.`);
|
|
132
|
+
}
|
|
133
|
+
if (provider.kind === 'arguments') {
|
|
134
|
+
if (this.#arguments.has(provider.provide)) {
|
|
135
|
+
return this.#arguments.get(provider.provide);
|
|
136
|
+
}
|
|
137
|
+
throw new DependencyResolutionError(`LUTRE_ARGS_005: Arguments ${provider.provide.name} requires runtime Arguments before Application initialization.`);
|
|
138
|
+
}
|
|
139
|
+
if (this.#applicationCache.has(token)) {
|
|
140
|
+
return this.#applicationCache.get(token);
|
|
141
|
+
}
|
|
142
|
+
throw new DependencyResolutionError(`LUTRE_DI_NOT_INITIALIZED: ${tokenName(token)} has not been constructed by Application initialization.`);
|
|
143
|
+
}
|
|
113
144
|
probeClass(target) {
|
|
114
145
|
try {
|
|
115
146
|
return this.#instantiate(target, [target]);
|