@loutrejs/loutre 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/dist/adapters/aws-lambda.js +2 -0
- package/dist/adapters/bun.d.ts +3 -1
- package/dist/adapters/bun.js +90 -33
- package/dist/adapters/cloudflare-workers.js +2 -0
- package/dist/adapters/deno.d.ts +3 -1
- package/dist/adapters/deno.js +99 -33
- package/dist/adapters/electron.js +2 -0
- 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 -0
- package/dist/generated/version.js +2 -0
- package/dist/graph/compiler.d.ts +4 -0
- package/dist/graph/compiler.js +228 -0
- package/dist/graph/graph.js +65 -40
- 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/application.js +5 -5
- package/dist/http/basic-auth.js +1 -1
- package/dist/http/client.d.ts +72 -0
- package/dist/http/client.js +258 -0
- package/dist/http/cors.js +4 -4
- 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 +13 -6
- package/dist/presentation.d.ts +33 -0
- package/dist/presentation.js +233 -0
- package/dist/runtime/capability.js +1 -1
- package/dist/runtime/di.js +2 -2
- package/dist/runtime/engine.d.ts +3 -0
- package/dist/runtime/engine.js +38 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/pipeline.js +13 -13
- package/dist/runtime/server-url.d.ts +1 -0
- package/dist/runtime/server-url.js +5 -0
- package/package.json +5 -1
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[];
|
package/dist/http/application.js
CHANGED
|
@@ -301,7 +301,7 @@ async function finalizeResponse(definition, result, signal) {
|
|
|
301
301
|
const responseHeaders = await validateResponseHeaders(response.headers, result.headers);
|
|
302
302
|
if (response.stream === 'server') {
|
|
303
303
|
if (!isAsyncIterable(result.body)) {
|
|
304
|
-
throw new Error('
|
|
304
|
+
throw new Error('Server-stream response requires an AsyncIterable');
|
|
305
305
|
}
|
|
306
306
|
const encoder = new TextEncoder();
|
|
307
307
|
const iterator = result.body[Symbol.asyncIterator]();
|
|
@@ -315,7 +315,7 @@ async function finalizeResponse(definition, result, signal) {
|
|
|
315
315
|
finished = true;
|
|
316
316
|
signal.removeEventListener('abort', abort);
|
|
317
317
|
void iterator.return?.(signal.reason).finally(() => {
|
|
318
|
-
controller.error(signal.reason ?? new Error('HTTP request
|
|
318
|
+
controller.error(signal.reason ?? new Error('HTTP request was aborted'));
|
|
319
319
|
});
|
|
320
320
|
};
|
|
321
321
|
if (signal.aborted)
|
|
@@ -368,7 +368,7 @@ async function finalizeResponse(definition, result, signal) {
|
|
|
368
368
|
async function validateResponseHeaders(schema, headers) {
|
|
369
369
|
if (!schema) {
|
|
370
370
|
if (headers !== undefined) {
|
|
371
|
-
throw new Error('
|
|
371
|
+
throw new Error('Undeclared HTTP response header was returned');
|
|
372
372
|
}
|
|
373
373
|
return undefined;
|
|
374
374
|
}
|
|
@@ -376,7 +376,7 @@ async function validateResponseHeaders(schema, headers) {
|
|
|
376
376
|
if (validated === undefined)
|
|
377
377
|
return undefined;
|
|
378
378
|
if (!isHttpHeaders(validated)) {
|
|
379
|
-
throw new Error('HTTP response header schema
|
|
379
|
+
throw new Error('HTTP response header schema produced an invalid value');
|
|
380
380
|
}
|
|
381
381
|
return validated;
|
|
382
382
|
}
|
|
@@ -458,7 +458,7 @@ async function mapDeclaredError(definition, error) {
|
|
|
458
458
|
if (typeof result !== 'object' ||
|
|
459
459
|
result === null ||
|
|
460
460
|
!('body' in result)) {
|
|
461
|
-
throw new Error('HTTP error mapping
|
|
461
|
+
throw new Error('HTTP error mapping must return a body');
|
|
462
462
|
}
|
|
463
463
|
return {
|
|
464
464
|
kind: 'http-result',
|
package/dist/http/basic-auth.js
CHANGED
|
@@ -40,7 +40,7 @@ function unauthorizedResult(unauthorized, challenge) {
|
|
|
40
40
|
}
|
|
41
41
|
function formatBasicChallenge(realm) {
|
|
42
42
|
if (realm.length === 0 || /[\u0000-\u001f\u007f]/.test(realm)) {
|
|
43
|
-
throw new TypeError('Basic
|
|
43
|
+
throw new TypeError('Basic authentication realm cannot be empty or contain control characters');
|
|
44
44
|
}
|
|
45
45
|
const escaped = realm.replace(/[\\"]/g, '\\$&');
|
|
46
46
|
return `Basic realm="${escaped}", charset="UTF-8"`;
|
|
@@ -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
|
+
}
|
package/dist/http/cors.js
CHANGED
|
@@ -123,12 +123,12 @@ function normalizeOrigin(origin, credentials) {
|
|
|
123
123
|
}
|
|
124
124
|
for (const value of origins) {
|
|
125
125
|
if (value.length === 0 || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
126
|
-
throw new TypeError('CORS origin
|
|
126
|
+
throw new TypeError('CORS origin cannot be empty or contain control characters');
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
if (origins.includes('*')) {
|
|
130
130
|
if (credentials) {
|
|
131
|
-
throw new TypeError('
|
|
131
|
+
throw new TypeError('CORS origin cannot be a wildcard when credentials are enabled');
|
|
132
132
|
}
|
|
133
133
|
return Object.freeze({ kind: 'wildcard' });
|
|
134
134
|
}
|
|
@@ -141,7 +141,7 @@ function normalizeTokens(values, option, uppercase) {
|
|
|
141
141
|
return Object.freeze(values.map((value) => {
|
|
142
142
|
const token = value.trim();
|
|
143
143
|
if (!httpToken.test(token)) {
|
|
144
|
-
throw new TypeError(`CORS ${option}
|
|
144
|
+
throw new TypeError(`CORS ${option} contains an invalid HTTP token`);
|
|
145
145
|
}
|
|
146
146
|
return uppercase ? token.toUpperCase() : token;
|
|
147
147
|
}));
|
|
@@ -150,7 +150,7 @@ function normalizeMaxAge(maxAge) {
|
|
|
150
150
|
if (maxAge === undefined)
|
|
151
151
|
return undefined;
|
|
152
152
|
if (!Number.isSafeInteger(maxAge) || maxAge < 0) {
|
|
153
|
-
throw new TypeError('CORS maxAge
|
|
153
|
+
throw new TypeError('CORS maxAge must be a non-negative safe integer');
|
|
154
154
|
}
|
|
155
155
|
return maxAge;
|
|
156
156
|
}
|
|
@@ -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) {
|
|
@@ -41,7 +48,7 @@ export function createMessagePortExecution(options) {
|
|
|
41
48
|
return await runtime.execute(async () => {
|
|
42
49
|
const route = routes.find((candidate) => candidate.procedure === procedure);
|
|
43
50
|
if (!route)
|
|
44
|
-
throw new Error(`MessagePort procedure
|
|
51
|
+
throw new Error(`MessagePort procedure not found: ${procedure}`);
|
|
45
52
|
invocationLogger = invocationLogger.child({
|
|
46
53
|
source: `${route.implementation.name}.${route.procedure}`,
|
|
47
54
|
});
|
|
@@ -66,7 +73,7 @@ export function createMessagePortExecution(options) {
|
|
|
66
73
|
const target = runtime.container.implementationRuntime(route.implementation);
|
|
67
74
|
const method = target[route.procedure];
|
|
68
75
|
if (typeof method !== 'function')
|
|
69
|
-
throw new Error('Handler method
|
|
76
|
+
throw new Error('Handler method is missing');
|
|
70
77
|
return Reflect.apply(method, target, [
|
|
71
78
|
terminalContext,
|
|
72
79
|
]);
|
|
@@ -106,10 +113,10 @@ export function createMessagePortExecution(options) {
|
|
|
106
113
|
async function finalize(definition, result) {
|
|
107
114
|
const response = definition.responses[result.variant];
|
|
108
115
|
if (!response)
|
|
109
|
-
throw new Error(
|
|
116
|
+
throw new Error(`Undeclared variant: ${result.variant}`);
|
|
110
117
|
if (response.stream === 'server') {
|
|
111
118
|
if (!isAsyncIterable(result.value)) {
|
|
112
|
-
throw new Error('
|
|
119
|
+
throw new Error('Server-stream result requires an AsyncIterable');
|
|
113
120
|
}
|
|
114
121
|
const source = result.value;
|
|
115
122
|
return {
|