@loutrejs/loutre 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/adapters/aws-lambda.d.ts +52 -0
  2. package/dist/adapters/aws-lambda.js +114 -0
  3. package/dist/adapters/bun.d.ts +20 -0
  4. package/dist/adapters/bun.js +82 -0
  5. package/dist/adapters/cloudflare-workers.d.ts +18 -0
  6. package/dist/adapters/cloudflare-workers.js +55 -0
  7. package/dist/adapters/deno.d.ts +28 -0
  8. package/dist/adapters/deno.js +106 -0
  9. package/dist/adapters/electron.d.ts +27 -0
  10. package/dist/adapters/electron.js +52 -0
  11. package/dist/application/binding.d.ts +33 -0
  12. package/dist/application/binding.js +292 -0
  13. package/dist/application/host.d.ts +8 -0
  14. package/dist/application/host.js +14 -0
  15. package/dist/application/index.d.ts +62 -0
  16. package/dist/application/index.js +11 -0
  17. package/dist/application/openapi.d.ts +28 -0
  18. package/dist/application/openapi.js +362 -0
  19. package/dist/application/queue.d.ts +3 -0
  20. package/dist/application/queue.js +4 -0
  21. package/dist/application/scheduler.d.ts +2 -0
  22. package/dist/application/scheduler.js +45 -0
  23. package/dist/core/args.d.ts +16 -0
  24. package/dist/core/args.js +29 -0
  25. package/dist/core/context-key.d.ts +14 -0
  26. package/dist/core/context-key.js +14 -0
  27. package/dist/core/contract.d.ts +54 -0
  28. package/dist/core/contract.js +30 -0
  29. package/dist/core/env.d.ts +16 -0
  30. package/dist/core/env.js +29 -0
  31. package/dist/core/error.d.ts +24 -0
  32. package/dist/core/error.js +25 -0
  33. package/dist/core/execution.d.ts +78 -0
  34. package/dist/core/execution.js +61 -0
  35. package/dist/core/implementation.d.ts +34 -0
  36. package/dist/core/implementation.js +30 -0
  37. package/dist/core/index.d.ts +15 -0
  38. package/dist/core/index.js +15 -0
  39. package/dist/core/injection.d.ts +28 -0
  40. package/dist/core/injection.js +35 -0
  41. package/dist/core/layer.d.ts +109 -0
  42. package/dist/core/layer.js +52 -0
  43. package/dist/core/lifecycle.d.ts +34 -0
  44. package/dist/core/lifecycle.js +3 -0
  45. package/dist/core/module.d.ts +53 -0
  46. package/dist/core/module.js +31 -0
  47. package/dist/core/provider.d.ts +57 -0
  48. package/dist/core/provider.js +63 -0
  49. package/dist/core/runtime-input.d.ts +11 -0
  50. package/dist/core/runtime-input.js +1 -0
  51. package/dist/core/schema.d.ts +13 -0
  52. package/dist/core/schema.js +20 -0
  53. package/dist/core/token.d.ts +16 -0
  54. package/dist/core/token.js +13 -0
  55. package/dist/graph/graph.d.ts +17 -0
  56. package/dist/graph/graph.js +1078 -0
  57. package/dist/graph/index.d.ts +2 -0
  58. package/dist/graph/index.js +2 -0
  59. package/dist/graph/ir.d.ts +159 -0
  60. package/dist/graph/ir.js +1 -0
  61. package/dist/http/application.d.ts +18 -0
  62. package/dist/http/application.js +506 -0
  63. package/dist/http/basic-auth.d.ts +37 -0
  64. package/dist/http/basic-auth.js +67 -0
  65. package/dist/http/cors.d.ts +17 -0
  66. package/dist/http/cors.js +171 -0
  67. package/dist/http/definitions.d.ts +217 -0
  68. package/dist/http/definitions.js +65 -0
  69. package/dist/http/hosted-protocol.d.ts +4 -0
  70. package/dist/http/hosted-protocol.js +4 -0
  71. package/dist/http/index.d.ts +7 -0
  72. package/dist/http/index.js +6 -0
  73. package/dist/http/params.d.ts +2 -0
  74. package/dist/http/params.js +19 -0
  75. package/dist/http/path.d.ts +55 -0
  76. package/dist/http/path.js +88 -0
  77. package/dist/http/request-body.d.ts +4 -0
  78. package/dist/http/request-body.js +6 -0
  79. package/dist/http/validate.d.ts +20 -0
  80. package/dist/http/validate.js +6 -0
  81. package/dist/index.d.ts +2 -0
  82. package/dist/index.js +2 -0
  83. package/dist/message-port/environment.d.ts +2 -0
  84. package/dist/message-port/environment.js +4 -0
  85. package/dist/message-port/index.d.ts +65 -0
  86. package/dist/message-port/index.js +182 -0
  87. package/dist/runtime/application.d.ts +37 -0
  88. package/dist/runtime/application.js +324 -0
  89. package/dist/runtime/capability.d.ts +16 -0
  90. package/dist/runtime/capability.js +18 -0
  91. package/dist/runtime/di.d.ts +37 -0
  92. package/dist/runtime/di.js +441 -0
  93. package/dist/runtime/error.d.ts +10 -0
  94. package/dist/runtime/error.js +15 -0
  95. package/dist/runtime/index.d.ts +7 -0
  96. package/dist/runtime/index.js +7 -0
  97. package/dist/runtime/logger.d.ts +36 -0
  98. package/dist/runtime/logger.js +207 -0
  99. package/dist/runtime/node-capabilities.d.ts +5 -0
  100. package/dist/runtime/node-capabilities.js +18 -0
  101. package/dist/runtime/pipeline.d.ts +11 -0
  102. package/dist/runtime/pipeline.js +161 -0
  103. package/package.json +81 -0
@@ -0,0 +1,362 @@
1
+ import { supportsJsonSchema, } from '../core/index.js';
2
+ import { assertValidCompilation, compileApplication } from '../graph/index.js';
3
+ import { httpRequestBodyContentType, httpRequestBodySchema, } from '../http/index.js';
4
+ const FIXED_METHODS = new Set([
5
+ 'get',
6
+ 'put',
7
+ 'post',
8
+ 'delete',
9
+ 'options',
10
+ 'head',
11
+ 'patch',
12
+ 'trace',
13
+ 'query',
14
+ ]);
15
+ export function generateOpenApi(application, options) {
16
+ const graph = assertValidCompilation(compileApplication({
17
+ modules: application.modules,
18
+ ...(application.arguments === undefined
19
+ ? {}
20
+ : { arguments: application.arguments }),
21
+ tasks: application.tasks,
22
+ triggers: application.triggers,
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
+ const registry = new SchemaRegistry();
30
+ const paths = {};
31
+ const operationIds = new Set();
32
+ for (const module of collectModules(application.modules)) {
33
+ for (const implementation of module.definition.implementations ?? []) {
34
+ if (implementation.protocol !== 'http')
35
+ continue;
36
+ for (const procedure of implementation.procedures) {
37
+ if (!executable.has(`${implementation.name}\u0000${procedure}`))
38
+ continue;
39
+ const protocol = implementation.contract.procedures[procedure]?.protocols.http;
40
+ if (!protocol || protocol.protocol !== 'http')
41
+ continue;
42
+ const typed = protocol;
43
+ const target = {
44
+ definition: typed.definition,
45
+ ...(implementation.contract.name === undefined
46
+ ? {}
47
+ : { contractName: implementation.contract.name }),
48
+ procedure,
49
+ };
50
+ const operation = createOperation(target, registry, operationIds);
51
+ attachOperation(paths, typed.definition, operation);
52
+ }
53
+ }
54
+ }
55
+ const schemas = registry.components();
56
+ return {
57
+ openapi: '3.2.0',
58
+ jsonSchemaDialect: 'https://json-schema.org/draft/2020-12/schema',
59
+ info: options.info,
60
+ ...(options.servers === undefined ? {} : { servers: options.servers }),
61
+ paths,
62
+ ...(Object.keys(schemas).length === 0 ? {} : { components: { schemas } }),
63
+ };
64
+ }
65
+ function createOperation(target, registry, operationIds) {
66
+ const definition = target.definition;
67
+ const operationId = target.contractName
68
+ ? `${target.contractName}.${target.procedure}`
69
+ : undefined;
70
+ if (operationId) {
71
+ if (operationIds.has(operationId)) {
72
+ throw openApiError('LUTRE_OPENAPI_OPERATION_ID_001', `Duplicate operationId: ${operationId}`);
73
+ }
74
+ operationIds.add(operationId);
75
+ }
76
+ const parameters = [];
77
+ const request = definition.request;
78
+ if (request?.params) {
79
+ for (const [name, schema] of Object.entries(request.params)) {
80
+ parameters.push({
81
+ name,
82
+ in: 'path',
83
+ required: true,
84
+ schema: registry.reference(schema, 'input', componentName(target, `RequestParam_${name}_Input`)),
85
+ });
86
+ }
87
+ }
88
+ if (request?.query) {
89
+ parameters.push({
90
+ in: 'querystring',
91
+ content: {
92
+ 'application/x-www-form-urlencoded': {
93
+ schema: registry.reference(request.query, 'input', componentName(target, 'RequestQuery_Input')),
94
+ },
95
+ },
96
+ });
97
+ }
98
+ if (request?.headers) {
99
+ parameters.push(...headerParameters(request.headers, registry, target, 'RequestHeaders_Input'));
100
+ }
101
+ const requestBody = request?.body
102
+ ? createRequestBody(request.body, registry, target)
103
+ : undefined;
104
+ return {
105
+ ...(definition.tags === undefined ? {} : { tags: definition.tags }),
106
+ ...(definition.summary === undefined
107
+ ? {}
108
+ : { summary: definition.summary }),
109
+ ...(definition.description === undefined
110
+ ? {}
111
+ : { description: definition.description }),
112
+ ...(operationId === undefined ? {} : { operationId }),
113
+ ...(parameters.length === 0 ? {} : { parameters }),
114
+ ...(requestBody === undefined ? {} : { requestBody }),
115
+ responses: createResponses(target, registry),
116
+ ...(definition.deprecated === undefined
117
+ ? {}
118
+ : { deprecated: definition.deprecated }),
119
+ };
120
+ }
121
+ function createRequestBody(body, registry, target) {
122
+ const contentType = httpRequestBodyContentType(body);
123
+ return {
124
+ content: {
125
+ [contentType]: {
126
+ schema: registry.reference(httpRequestBodySchema(body), 'input', componentName(target, 'RequestBody_Input')),
127
+ },
128
+ },
129
+ };
130
+ }
131
+ function createResponses(target, registry) {
132
+ const grouped = new Map();
133
+ for (const [variant, response] of Object.entries(target.definition.responses)) {
134
+ const current = grouped.get(response.status) ?? [];
135
+ current.push({ variant, response });
136
+ grouped.set(response.status, current);
137
+ }
138
+ return Object.fromEntries([...grouped.entries()]
139
+ .toSorted(([left], [right]) => left - right)
140
+ .map(([status, entries]) => [
141
+ String(status),
142
+ createResponse(target, entries, registry),
143
+ ]));
144
+ }
145
+ function createResponse(target, entries, registry) {
146
+ const streamKinds = new Set(entries.map(({ response }) => response.stream === 'server'));
147
+ if (streamKinds.size > 1) {
148
+ throw openApiError('LUTRE_OPENAPI_RESPONSE_STREAM_001', `${describeTarget(target)} declares streaming and non-streaming responses with the same status.`);
149
+ }
150
+ const schemas = entries.map(({ variant, response }) => registry.reference(response.body, 'output', componentName(target, `Response_${variant}_Output`)));
151
+ const payloadSchema = schemas.length === 1 ? schemas[0] : { oneOf: schemas };
152
+ const streaming = entries[0]?.response.stream === 'server';
153
+ const headers = mergeResponseHeaders(entries, registry, target);
154
+ const descriptions = [
155
+ ...new Set(entries.map(({ variant, response }) => response.description ?? variant)),
156
+ ];
157
+ return {
158
+ description: descriptions.join(' / '),
159
+ ...(headers === undefined ? {} : { headers }),
160
+ content: streaming
161
+ ? {
162
+ 'text/event-stream': {
163
+ itemSchema: {
164
+ type: 'object',
165
+ required: ['data'],
166
+ properties: {
167
+ data: {
168
+ type: 'string',
169
+ contentMediaType: 'application/json',
170
+ contentSchema: payloadSchema,
171
+ },
172
+ },
173
+ },
174
+ },
175
+ }
176
+ : {
177
+ 'application/json': {
178
+ schema: payloadSchema,
179
+ },
180
+ },
181
+ };
182
+ }
183
+ function mergeResponseHeaders(entries, registry, target) {
184
+ const result = new Map();
185
+ for (const { variant, response } of entries) {
186
+ if (!response.headers)
187
+ continue;
188
+ const materialized = registry.materialize(response.headers, 'output', componentName(target, `Response_${variant}_Headers_Output`));
189
+ const properties = objectProperties(materialized.schema, 'LUTRE_OPENAPI_HEADER_SCHEMA_001', `${describeTarget(target)} response headers`);
190
+ for (const [name, schema] of Object.entries(properties)) {
191
+ const current = result.get(name) ?? [];
192
+ current.push(schema);
193
+ result.set(name, current);
194
+ }
195
+ }
196
+ if (result.size === 0)
197
+ return undefined;
198
+ return Object.fromEntries([...result.entries()].map(([name, schemas]) => [
199
+ name,
200
+ {
201
+ schema: schemas.length === 1
202
+ ? schemas[0]
203
+ : { oneOf: dedupeJsonSchemas(schemas) },
204
+ },
205
+ ]));
206
+ }
207
+ function headerParameters(schema, registry, target, suffix) {
208
+ const materialized = registry.materialize(schema, 'input', componentName(target, suffix));
209
+ const properties = objectProperties(materialized.schema, 'LUTRE_OPENAPI_HEADER_SCHEMA_001', `${describeTarget(target)} request headers`);
210
+ const required = new Set(Array.isArray(materialized.schema.required)
211
+ ? materialized.schema.required.filter((value) => typeof value === 'string')
212
+ : []);
213
+ return Object.entries(properties).map(([name, propertySchema]) => ({
214
+ name,
215
+ in: 'header',
216
+ required: required.has(name),
217
+ schema: propertySchema,
218
+ }));
219
+ }
220
+ function objectProperties(schema, code, description) {
221
+ const properties = schema.properties;
222
+ if (schema.type !== 'object' ||
223
+ typeof properties !== 'object' ||
224
+ properties === null ||
225
+ Array.isArray(properties)) {
226
+ throw openApiError(code, `${description} must convert to an object JSON Schema with properties.`);
227
+ }
228
+ return properties;
229
+ }
230
+ function attachOperation(paths, definition, operation) {
231
+ const path = definition.path;
232
+ const pathItem = (paths[path] ??= {});
233
+ const method = definition.method.toLowerCase();
234
+ if (FIXED_METHODS.has(method)) {
235
+ if (pathItem[method] !== undefined) {
236
+ throw openApiError('LUTRE_OPENAPI_OPERATION_001', `Duplicate OpenAPI operation: ${definition.method.toUpperCase()} ${path}`);
237
+ }
238
+ pathItem[method] = operation;
239
+ return;
240
+ }
241
+ const additional = (pathItem.additionalOperations ??= {});
242
+ const methodName = definition.method.toUpperCase();
243
+ if (additional[methodName] !== undefined) {
244
+ throw openApiError('LUTRE_OPENAPI_OPERATION_001', `Duplicate OpenAPI operation: ${methodName} ${path}`);
245
+ }
246
+ additional[methodName] = operation;
247
+ }
248
+ function collectModules(roots) {
249
+ const result = [];
250
+ const seen = new Set();
251
+ const visit = (module) => {
252
+ if (seen.has(module))
253
+ return;
254
+ seen.add(module);
255
+ for (const imported of module.definition.imports ?? [])
256
+ visit(imported);
257
+ result.push(module);
258
+ };
259
+ for (const root of roots)
260
+ visit(root);
261
+ return result;
262
+ }
263
+ class SchemaRegistry {
264
+ bySchema = new Map();
265
+ schemas = {};
266
+ reference(schema, direction, preferredName) {
267
+ return this.materialize(schema, direction, preferredName).reference;
268
+ }
269
+ materialize(schema, direction, preferredName) {
270
+ const existing = this.bySchema.get(schema)?.[direction];
271
+ if (existing)
272
+ return existing;
273
+ if (!supportsJsonSchema(schema)) {
274
+ throw openApiError('LUTRE_OPENAPI_SCHEMA_001', `${preferredName} does not implement StandardJSONSchemaV1.`);
275
+ }
276
+ let converted;
277
+ try {
278
+ converted = schema['~standard'].jsonSchema[direction]({
279
+ target: 'draft-2020-12',
280
+ });
281
+ }
282
+ catch (error) {
283
+ throw openApiError('LUTRE_OPENAPI_SCHEMA_002', `${preferredName} could not be converted to draft-2020-12 JSON Schema: ${errorMessage(error)}`);
284
+ }
285
+ const name = uniqueComponentName(preferredName, this.schemas);
286
+ const rebased = rebaseLocalDefinitions(converted, name);
287
+ this.schemas[name] = rebased;
288
+ const materialized = {
289
+ name,
290
+ schema: rebased,
291
+ reference: { $ref: `#/components/schemas/${escapeJsonPointer(name)}` },
292
+ };
293
+ const directions = this.bySchema.get(schema) ?? {};
294
+ directions[direction] = materialized;
295
+ this.bySchema.set(schema, directions);
296
+ return materialized;
297
+ }
298
+ components() {
299
+ return this.schemas;
300
+ }
301
+ }
302
+ function rebaseLocalDefinitions(schema, schemaComponentName) {
303
+ const visit = (value) => {
304
+ if (Array.isArray(value))
305
+ return value.map(visit);
306
+ if (typeof value !== 'object' || value === null)
307
+ return value;
308
+ const result = {};
309
+ for (const [key, child] of Object.entries(value)) {
310
+ if (key === '$ref' &&
311
+ typeof child === 'string' &&
312
+ child.startsWith('#/$defs/')) {
313
+ result[key] =
314
+ `#/components/schemas/${escapeJsonPointer(schemaComponentName)}/$defs/${child.slice('#/$defs/'.length)}`;
315
+ }
316
+ else {
317
+ result[key] = visit(child);
318
+ }
319
+ }
320
+ return result;
321
+ };
322
+ return visit(schema);
323
+ }
324
+ function componentName(target, suffix) {
325
+ return sanitizeComponentName(`${target.contractName ?? 'AnonymousContract'}_${target.procedure}_${suffix}`);
326
+ }
327
+ function sanitizeComponentName(value) {
328
+ const sanitized = value.replace(/[^A-Za-z0-9_.-]+/g, '_');
329
+ return sanitized.length === 0 ? 'Schema' : sanitized;
330
+ }
331
+ function uniqueComponentName(preferred, schemas) {
332
+ if (!(preferred in schemas))
333
+ return preferred;
334
+ let index = 2;
335
+ while (`${preferred}_${index}` in schemas)
336
+ index += 1;
337
+ return `${preferred}_${index}`;
338
+ }
339
+ function escapeJsonPointer(value) {
340
+ return value.replace(/~/g, '~0').replace(/\//g, '~1');
341
+ }
342
+ function describeTarget(target) {
343
+ return `${target.contractName ?? 'AnonymousContract'}.${target.procedure}`;
344
+ }
345
+ function dedupeJsonSchemas(schemas) {
346
+ const seen = new Set();
347
+ const result = [];
348
+ for (const schema of schemas) {
349
+ const serialized = JSON.stringify(schema);
350
+ if (seen.has(serialized))
351
+ continue;
352
+ seen.add(serialized);
353
+ result.push(schema);
354
+ }
355
+ return result;
356
+ }
357
+ function errorMessage(error) {
358
+ return error instanceof Error ? error.message : String(error);
359
+ }
360
+ function openApiError(code, message) {
361
+ return new Error(`${code}: ${message}`);
362
+ }
@@ -0,0 +1,3 @@
1
+ import { type ProviderDescriptor, type QueueDescriptor } from '../core/index.js';
2
+ import type { QueueConsumerDriver } from './index.js';
3
+ export declare function bindQueueDriver(queue: QueueDescriptor, driver: QueueConsumerDriver): ProviderDescriptor;
@@ -0,0 +1,4 @@
1
+ import { provide, queueRuntimeToken, } from '../core/index.js';
2
+ export function bindQueueDriver(queue, driver) {
3
+ return provide(queueRuntimeToken(queue)).useValue(driver);
4
+ }
@@ -0,0 +1,2 @@
1
+ import type { CronTriggerDescriptor } from '../core/index.js';
2
+ export declare function matchesCronTrigger(trigger: CronTriggerDescriptor<any>, instant: Date): boolean;
@@ -0,0 +1,45 @@
1
+ export function matchesCronTrigger(trigger, instant) {
2
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = trigger.expression
3
+ .trim()
4
+ .split(/\s+/);
5
+ const fields = { minute, hour, dayOfMonth, month, dayOfWeek };
6
+ const parts = new Intl.DateTimeFormat('en-US', {
7
+ timeZone: trigger.timezone,
8
+ minute: 'numeric',
9
+ hour: 'numeric',
10
+ day: 'numeric',
11
+ month: 'numeric',
12
+ weekday: 'short',
13
+ hourCycle: 'h23',
14
+ }).formatToParts(instant);
15
+ const value = (type) => parts.find((part) => part.type === type)?.value ?? '';
16
+ const weekday = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].indexOf(value('weekday'));
17
+ return (matchesCronField(fields.minute, Number(value('minute')), 0, 59) &&
18
+ matchesCronField(fields.hour, Number(value('hour')), 0, 23) &&
19
+ matchesCronField(fields.dayOfMonth, Number(value('day')), 1, 31) &&
20
+ matchesCronField(fields.month, Number(value('month')), 1, 12) &&
21
+ matchesCronField(fields.dayOfWeek, weekday, 0, 7, true));
22
+ }
23
+ function matchesCronField(expression, value, minimum, maximum, sundayAlias = false) {
24
+ return expression.split(',').some((segment) => {
25
+ const [rangeExpression, stepExpression] = segment.split('/');
26
+ const step = stepExpression === undefined ? 1 : Number(stepExpression);
27
+ if (!Number.isInteger(step) || step <= 0 || !rangeExpression)
28
+ return false;
29
+ let start = minimum;
30
+ let end = maximum;
31
+ if (rangeExpression !== '*') {
32
+ const [startExpression, endExpression] = rangeExpression.split('-');
33
+ start = Number(startExpression);
34
+ end = endExpression === undefined ? start : Number(endExpression);
35
+ }
36
+ const normalized = sundayAlias && value === 0 && start === 7 ? 7 : value;
37
+ return (Number.isInteger(start) &&
38
+ Number.isInteger(end) &&
39
+ start >= minimum &&
40
+ end <= maximum &&
41
+ normalized >= start &&
42
+ normalized <= end &&
43
+ (normalized - start) % step === 0);
44
+ });
45
+ }
@@ -0,0 +1,16 @@
1
+ import type { Class } from './token.js';
2
+ import { type SchemaOutput, type StandardSchemaV1 } from './schema.js';
3
+ import type { RuntimeInputKey } from './runtime-input.js';
4
+ export type ArgsSchema = StandardSchemaV1<unknown, object>;
5
+ export type ArgsKey<TValue = unknown> = RuntimeInputKey<TValue> & {
6
+ readonly source: 'arguments';
7
+ readonly contract: ArgsClass;
8
+ };
9
+ export interface ArgsClass<TSchema extends ArgsSchema = ArgsSchema> extends Class<SchemaOutput<TSchema>> {
10
+ readonly kind: 'arguments';
11
+ readonly schema: TSchema;
12
+ key<TKey extends keyof SchemaOutput<TSchema> & string>(key: TKey): ArgsKey<SchemaOutput<TSchema>[TKey]>;
13
+ }
14
+ export declare function defineArgs<TSchema extends ArgsSchema>(schema: TSchema): ArgsClass<TSchema>;
15
+ export declare function isArgsClass(value: unknown): value is ArgsClass;
16
+ export declare function loadArgs<TSchema extends ArgsSchema>(Args: ArgsClass<TSchema>, source: unknown): Promise<InstanceType<ArgsClass<TSchema>>>;
@@ -0,0 +1,29 @@
1
+ import { validateSchema, } from './schema.js';
2
+ export function defineArgs(schema) {
3
+ class DefinedArgs {
4
+ static kind = 'arguments';
5
+ static schema = schema;
6
+ static key(key) {
7
+ return {
8
+ kind: 'runtime-input-key',
9
+ source: 'arguments',
10
+ contract: this,
11
+ key,
12
+ };
13
+ }
14
+ constructor(values) {
15
+ Object.assign(this, values);
16
+ }
17
+ }
18
+ return DefinedArgs;
19
+ }
20
+ export function isArgsClass(value) {
21
+ return (typeof value === 'function' &&
22
+ value.kind === 'arguments' &&
23
+ 'schema' in value &&
24
+ typeof value.key === 'function');
25
+ }
26
+ export async function loadArgs(Args, source) {
27
+ const values = await validateSchema(Args.schema, source);
28
+ return new Args(values);
29
+ }
@@ -0,0 +1,14 @@
1
+ export interface ContextKey<TName extends string = string, TValue = unknown> {
2
+ readonly kind: 'context-key';
3
+ readonly name: TName;
4
+ readonly key: symbol;
5
+ readonly '~value'?: TValue;
6
+ }
7
+ export type ContextKeyValue<TKey> = TKey extends ContextKey<string, infer TValue> ? TValue : never;
8
+ export type ContextProperties<TKeys extends readonly ContextKey[]> = {
9
+ readonly [TKey in TKeys[number] as TKey['name']]: ContextKeyValue<TKey>;
10
+ };
11
+ export declare function contextKey<const TName extends string>(name: TName): {
12
+ of<TValue>(): ContextKey<TName, TValue>;
13
+ };
14
+ export declare function contextKeyName(key: ContextKey): string;
@@ -0,0 +1,14 @@
1
+ export function contextKey(name) {
2
+ return {
3
+ of() {
4
+ return Object.freeze({
5
+ kind: 'context-key',
6
+ name,
7
+ key: Symbol(name),
8
+ });
9
+ },
10
+ };
11
+ }
12
+ export function contextKeyName(key) {
13
+ return key.name;
14
+ }
@@ -0,0 +1,54 @@
1
+ export interface ProtocolDescriptor<TName extends string = string, TContext = unknown, TResult = unknown, TDispatchKey extends string | null = string | null, TCapabilities extends readonly string[] = readonly string[]> {
2
+ readonly kind: 'protocol';
3
+ readonly protocol: TName;
4
+ readonly interaction?: InteractionMode;
5
+ readonly dispatchKey: TDispatchKey;
6
+ readonly capabilities?: TCapabilities;
7
+ readonly '~context'?: TContext;
8
+ readonly '~result'?: TResult;
9
+ }
10
+ export type InteractionMode = 'unary' | 'server-stream' | 'client-stream' | 'duplex';
11
+ export interface ProcedureDefinition<TProtocols extends Record<string, ProtocolDescriptor> = Record<string, ProtocolDescriptor>> {
12
+ readonly kind: 'procedure';
13
+ readonly protocols: TProtocols;
14
+ }
15
+ export declare function procedure<const TProtocols extends Record<string, ProtocolDescriptor>>(definition: {
16
+ readonly protocols: TProtocols;
17
+ }): ProcedureDefinition<TProtocols>;
18
+ export interface ContractDefinition<TProcedures extends Record<string, ProcedureDefinition> = Record<string, ProcedureDefinition>> {
19
+ readonly kind: 'contract';
20
+ readonly name?: string;
21
+ readonly procedures: TProcedures;
22
+ }
23
+ export declare function contract<const TProcedures extends Record<string, ProcedureDefinition>>(procedures: TProcedures & DispatchKeyUniquenessConstraint<TProcedures>, options?: {
24
+ readonly name?: string;
25
+ }): ContractDefinition<TProcedures>;
26
+ type ProtocolDispatchEntries<TProcedures extends Record<string, ProcedureDefinition>> = {
27
+ [TProcedure in keyof TProcedures & string]: {
28
+ [TProtocol in keyof TProcedures[TProcedure]['protocols'] & string]: TProcedures[TProcedure]['protocols'][TProtocol] extends {
29
+ readonly dispatchKey: infer TDispatchKey extends string | null;
30
+ } ? TDispatchKey extends string ? {
31
+ readonly key: TDispatchKey;
32
+ readonly path: `${TProcedure}.${TProtocol}`;
33
+ } : never : never;
34
+ }[keyof TProcedures[TProcedure]['protocols'] & string];
35
+ }[keyof TProcedures & string];
36
+ type IsUnion<TValue, TCandidate = TValue> = TValue extends unknown ? [TCandidate] extends [TValue] ? false : true : never;
37
+ type DuplicateDispatchKeys<TEntries, TAllEntries = TEntries> = TEntries extends {
38
+ readonly key: infer TKey extends string;
39
+ } ? string extends TKey ? never : IsUnion<Extract<TAllEntries, {
40
+ readonly key: TKey;
41
+ }> extends {
42
+ readonly path: infer TPath;
43
+ } ? TPath : never> extends true ? TKey : never : never;
44
+ type DispatchKeyUniquenessConstraint<TProcedures extends Record<string, ProcedureDefinition>> = [DuplicateDispatchKeys<ProtocolDispatchEntries<TProcedures>>] extends [
45
+ never
46
+ ] ? unknown : {
47
+ readonly __duplicateProtocolDispatchKey__: DuplicateDispatchKeys<ProtocolDispatchEntries<TProcedures>>;
48
+ };
49
+ export type ContractProcedures<TContract extends ContractDefinition> = TContract['procedures'];
50
+ export interface ProtocolFactory<TName extends string, TCapabilities extends readonly string[] = readonly string[]> {
51
+ readonly protocol: TName;
52
+ readonly capabilities?: TCapabilities;
53
+ }
54
+ export {};
@@ -0,0 +1,30 @@
1
+ export function procedure(definition) {
2
+ return { kind: 'procedure', protocols: definition.protocols };
3
+ }
4
+ export function contract(procedures, options = {}) {
5
+ assertUniqueDispatchKeys(procedures, options.name);
6
+ return {
7
+ kind: 'contract',
8
+ ...(options.name === undefined ? {} : { name: options.name }),
9
+ procedures,
10
+ };
11
+ }
12
+ function assertUniqueDispatchKeys(procedures, contractName) {
13
+ const paths = new Map();
14
+ for (const [procedureName, procedureDefinition] of Object.entries(procedures)) {
15
+ for (const [protocolName, protocol] of Object.entries(procedureDefinition.protocols)) {
16
+ const dispatchKey = protocol.dispatchKey;
17
+ if (dispatchKey === null)
18
+ continue;
19
+ if (typeof dispatchKey !== 'string') {
20
+ throw new Error(`Protocol dispatchKey must be a string or null: ${procedureName}.${protocolName}`);
21
+ }
22
+ const path = `${contractName ?? 'Contract'}.${procedureName}.${protocolName}`;
23
+ const existing = paths.get(dispatchKey);
24
+ if (existing) {
25
+ throw new Error(`Duplicate protocol dispatch key "${dispatchKey}" between ${existing} and ${path}`);
26
+ }
27
+ paths.set(dispatchKey, path);
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,16 @@
1
+ import type { Class } from './token.js';
2
+ import { type SchemaOutput, type StandardSchemaV1 } from './schema.js';
3
+ import type { RuntimeInputKey } from './runtime-input.js';
4
+ export type EnvSchema = StandardSchemaV1<unknown, object>;
5
+ export type EnvKey<TValue = unknown> = RuntimeInputKey<TValue> & {
6
+ readonly source: 'environment';
7
+ readonly contract: EnvClass;
8
+ };
9
+ export interface EnvClass<TSchema extends EnvSchema = EnvSchema> extends Class<SchemaOutput<TSchema>> {
10
+ readonly kind: 'environment';
11
+ readonly schema: TSchema;
12
+ key<TKey extends keyof SchemaOutput<TSchema> & string>(key: TKey): EnvKey<SchemaOutput<TSchema>[TKey]>;
13
+ }
14
+ export declare function defineEnv<TSchema extends EnvSchema>(schema: TSchema): EnvClass<TSchema>;
15
+ export declare function isEnvClass(value: unknown): value is EnvClass;
16
+ export declare function loadEnv<TSchema extends EnvSchema>(Env: EnvClass<TSchema>, source: unknown): Promise<InstanceType<EnvClass<TSchema>>>;
@@ -0,0 +1,29 @@
1
+ import { validateSchema, } from './schema.js';
2
+ export function defineEnv(schema) {
3
+ class DefinedEnv {
4
+ static kind = 'environment';
5
+ static schema = schema;
6
+ static key(key) {
7
+ return {
8
+ kind: 'runtime-input-key',
9
+ source: 'environment',
10
+ contract: this,
11
+ key,
12
+ };
13
+ }
14
+ constructor(values) {
15
+ Object.assign(this, values);
16
+ }
17
+ }
18
+ return DefinedEnv;
19
+ }
20
+ export function isEnvClass(value) {
21
+ return (typeof value === 'function' &&
22
+ value.kind === 'environment' &&
23
+ 'schema' in value &&
24
+ typeof value.key === 'function');
25
+ }
26
+ export async function loadEnv(Env, source) {
27
+ const values = await validateSchema(Env.schema, source);
28
+ return new Env(values);
29
+ }
@@ -0,0 +1,24 @@
1
+ import type { SchemaOutput, StandardSchemaV1 } from './schema.js';
2
+ declare const domainErrorMarker: unique symbol;
3
+ export declare class DomainError<TData = unknown> extends Error {
4
+ readonly code: string;
5
+ readonly data: TData;
6
+ readonly [domainErrorMarker] = true;
7
+ constructor(code: string, data: TData, options?: {
8
+ readonly cause?: unknown;
9
+ });
10
+ }
11
+ export interface ErrorDefinition<TCode extends string = string, TSchema extends StandardSchemaV1 = StandardSchemaV1> {
12
+ (data: SchemaOutput<TSchema>, options?: {
13
+ readonly cause?: unknown;
14
+ }): DomainError<SchemaOutput<TSchema>>;
15
+ readonly kind: 'error-definition';
16
+ readonly code: TCode;
17
+ readonly data: TSchema;
18
+ is(error: unknown): error is DomainError<SchemaOutput<TSchema>>;
19
+ }
20
+ export declare function defineError<const TCode extends string, TSchema extends StandardSchemaV1>(definition: {
21
+ readonly code: TCode;
22
+ readonly data: TSchema;
23
+ }): ErrorDefinition<TCode, TSchema>;
24
+ export {};