@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,292 @@
1
+ import { queueRuntimeToken, validateSchema, } from '../core/index.js';
2
+ import { assertValidCompilation, compileApplication } from '../graph/index.js';
3
+ import { createHttpExecution, } from '../http/index.js';
4
+ import { createMessagePortExecution, } from '../message-port/index.js';
5
+ import { ApplicationRuntime, Logger } from '../runtime/index.js';
6
+ import { bindQueueDriver } from './queue.js';
7
+ import { matchesCronTrigger } from './scheduler.js';
8
+ /** flatなcreateXXX/bindXXX APIを増殖させないため、外部bindingをnamespaceへ集約する。 */
9
+ export const binding = Object.freeze({
10
+ invocation: createInvocationBinding,
11
+ host: createHostBinding,
12
+ queue: bindQueueDriver,
13
+ });
14
+ function createInvocationBinding(options) {
15
+ const state = createRuntimeState(options);
16
+ const application = createInvocationApplication(state);
17
+ return createProtocolBinding(state, application);
18
+ }
19
+ function createHostBinding(options) {
20
+ const state = createRuntimeState(options);
21
+ const application = createInvocationApplication(state);
22
+ let triggersStarted = false;
23
+ let triggerHandles = [];
24
+ const stopTriggers = async () => {
25
+ const handles = triggerHandles;
26
+ triggerHandles = [];
27
+ triggersStarted = false;
28
+ const results = await Promise.allSettled(handles.toReversed().map((handle) => handle.stop()));
29
+ const errors = results.flatMap((result) => result.status === 'rejected' ? [result.reason] : []);
30
+ if (errors.length > 0)
31
+ throw new AggregateError(errors, 'Trigger stop failed');
32
+ };
33
+ Object.assign(application, {
34
+ async close(signal) {
35
+ state.runtime.stopAcceptingExecutions();
36
+ const errors = [];
37
+ try {
38
+ await stopTriggers();
39
+ }
40
+ catch (error) {
41
+ errors.push(error);
42
+ }
43
+ try {
44
+ await state.runtime.shutdown(signal);
45
+ }
46
+ catch (error) {
47
+ errors.push(error);
48
+ }
49
+ if (errors.length > 0)
50
+ throw new AggregateError(errors, 'Application shutdown failed');
51
+ },
52
+ ...(state.definition.triggers.length > 0
53
+ ? {
54
+ triggers: {
55
+ async start() {
56
+ if (triggersStarted) {
57
+ throw new Error('LUTRE_TRIGGERS_ALREADY_STARTED: Trigger Engine is already started.');
58
+ }
59
+ await state.runtime.initialize();
60
+ triggersStarted = true;
61
+ const started = [];
62
+ try {
63
+ for (const trigger of state.definition.triggers) {
64
+ started.push(await startTrigger(trigger, state.runtime, state.logger));
65
+ }
66
+ triggerHandles = started;
67
+ }
68
+ catch (error) {
69
+ await Promise.allSettled(started.toReversed().map((handle) => handle.stop()));
70
+ triggersStarted = false;
71
+ throw error;
72
+ }
73
+ },
74
+ stop: stopTriggers,
75
+ },
76
+ }
77
+ : {}),
78
+ });
79
+ return createProtocolBinding(state, application);
80
+ }
81
+ function createRuntimeState(options) {
82
+ const definition = options.application;
83
+ const logger = definition.logger ?? new Logger();
84
+ const tasks = registeredTasks(definition);
85
+ const graph = assertValidCompilation(compileApplication({
86
+ modules: definition.modules,
87
+ ...(definition.arguments === undefined
88
+ ? {}
89
+ : { arguments: definition.arguments }),
90
+ tasks: definition.tasks,
91
+ triggers: definition.triggers,
92
+ }));
93
+ const runtime = new ApplicationRuntime(definition.modules, {
94
+ logger,
95
+ tasks,
96
+ publicTasks: definition.tasks,
97
+ ...(definition.arguments === undefined
98
+ ? {}
99
+ : {
100
+ arguments: definition.arguments,
101
+ argumentsSource: 'arguments' in options ? options.arguments : Object.freeze({}),
102
+ }),
103
+ ...('environment' in options
104
+ ? { environmentSource: options.environment }
105
+ : {}),
106
+ });
107
+ return { definition, logger, runtime, graph };
108
+ }
109
+ function createInvocationApplication(state) {
110
+ const application = {
111
+ graph: state.graph,
112
+ async init() {
113
+ await state.runtime.initialize();
114
+ return application;
115
+ },
116
+ ...(state.definition.tasks.length > 0
117
+ ? {
118
+ run(task, ...args) {
119
+ return Reflect.apply(state.runtime.run, state.runtime, [
120
+ task,
121
+ ...args,
122
+ ]);
123
+ },
124
+ }
125
+ : {}),
126
+ close: (signal) => state.runtime.shutdown(signal),
127
+ };
128
+ return application;
129
+ }
130
+ function createProtocolBinding(state, application) {
131
+ const protocols = new Set(state.graph.pipelines.map((pipeline) => pipeline.protocol));
132
+ return {
133
+ application,
134
+ ...(protocols.has('http')
135
+ ? {
136
+ http: createHttpExecution({
137
+ runtime: state.runtime,
138
+ graph: state.graph,
139
+ logger: state.logger,
140
+ }),
141
+ }
142
+ : {}),
143
+ ...(protocols.has('messagePort')
144
+ ? {
145
+ messagePort: createMessagePortExecution({
146
+ runtime: state.runtime,
147
+ graph: state.graph,
148
+ logger: state.logger,
149
+ }),
150
+ }
151
+ : {}),
152
+ };
153
+ }
154
+ async function startTrigger(trigger, runtime, logger) {
155
+ switch (trigger.trigger) {
156
+ case 'cron':
157
+ return startCronTrigger(trigger, runtime, logger);
158
+ case 'fixed-delay':
159
+ return startFixedDelayTrigger(trigger, runtime, logger);
160
+ case 'queue-consumer':
161
+ return startQueueTrigger(trigger, runtime);
162
+ }
163
+ throw new Error('LUTRE_TRIGGER_UNSUPPORTED: Unsupported trigger type.');
164
+ }
165
+ function startCronTrigger(trigger, runtime, logger) {
166
+ let stopped = false;
167
+ let lastMinute;
168
+ const active = new Set();
169
+ const tick = () => {
170
+ if (stopped)
171
+ return;
172
+ const now = new Date();
173
+ if (!matchesCronTrigger(trigger, now))
174
+ return;
175
+ const minute = cronMinuteIdentity(trigger.timezone, now);
176
+ if (lastMinute === minute)
177
+ return;
178
+ lastMinute = minute;
179
+ if (trigger.overlap === 'skip' && active.size > 0) {
180
+ logger.info('Cron trigger skipped overlapping execution', {
181
+ event: 'trigger.cron.skipped',
182
+ trigger: trigger.name,
183
+ });
184
+ return;
185
+ }
186
+ const execution = runtime.runTask(trigger.task).catch((error) => {
187
+ logger.error('Cron trigger execution failed', {
188
+ event: 'trigger.cron.execution.failed',
189
+ trigger: trigger.name,
190
+ error: error instanceof Error ? error.message : String(error),
191
+ });
192
+ });
193
+ active.add(execution);
194
+ void execution.finally(() => active.delete(execution));
195
+ };
196
+ tick();
197
+ const timer = setInterval(tick, 1_000);
198
+ return {
199
+ async stop() {
200
+ if (stopped)
201
+ return;
202
+ stopped = true;
203
+ clearInterval(timer);
204
+ await Promise.allSettled(active);
205
+ },
206
+ };
207
+ }
208
+ function startFixedDelayTrigger(trigger, runtime, logger) {
209
+ let stopped = false;
210
+ let timer;
211
+ let releaseSleep;
212
+ const sleep = () => new Promise((resolve) => {
213
+ releaseSleep = resolve;
214
+ timer = setTimeout(() => {
215
+ timer = undefined;
216
+ releaseSleep = undefined;
217
+ resolve();
218
+ }, trigger.delay);
219
+ });
220
+ const execute = async () => {
221
+ try {
222
+ await runtime.runTask(trigger.task);
223
+ }
224
+ catch (error) {
225
+ logger.error('Fixed-delay trigger execution failed', {
226
+ event: 'trigger.fixed_delay.execution.failed',
227
+ trigger: trigger.name,
228
+ error: error instanceof Error ? error.message : String(error),
229
+ });
230
+ }
231
+ };
232
+ const loop = (async () => {
233
+ if (!trigger.immediate)
234
+ await sleep();
235
+ while (true) {
236
+ if (stopped)
237
+ break;
238
+ await execute();
239
+ if (stopped)
240
+ break;
241
+ await sleep();
242
+ }
243
+ })();
244
+ return {
245
+ async stop() {
246
+ if (stopped)
247
+ return;
248
+ stopped = true;
249
+ if (timer)
250
+ clearTimeout(timer);
251
+ timer = undefined;
252
+ releaseSleep?.();
253
+ releaseSleep = undefined;
254
+ await loop;
255
+ },
256
+ };
257
+ }
258
+ async function startQueueTrigger(trigger, runtime) {
259
+ const driver = runtime.container.resolve(queueRuntimeToken(trigger.queue));
260
+ if (!driver || typeof driver.start !== 'function') {
261
+ throw new Error(`LUTRE_QUEUE_DRIVER_INVALID: Queue ${trigger.queue.name} driver is invalid.`);
262
+ }
263
+ const handle = await driver.start({
264
+ consume: async (payload) => {
265
+ const validated = await validateSchema(trigger.queue.payload, payload);
266
+ await runtime.runTask(trigger.task, validated);
267
+ },
268
+ });
269
+ if (!handle || typeof handle.stop !== 'function') {
270
+ throw new Error(`LUTRE_QUEUE_DRIVER_INVALID: Queue ${trigger.queue.name} driver returned an invalid handle.`);
271
+ }
272
+ return handle;
273
+ }
274
+ function registeredTasks(definition) {
275
+ return [
276
+ ...new Set([
277
+ ...definition.tasks,
278
+ ...definition.triggers.map((trigger) => trigger.task),
279
+ ]),
280
+ ];
281
+ }
282
+ function cronMinuteIdentity(timezone, instant) {
283
+ return new Intl.DateTimeFormat('en-CA', {
284
+ timeZone: timezone,
285
+ year: 'numeric',
286
+ month: '2-digit',
287
+ day: '2-digit',
288
+ hour: '2-digit',
289
+ minute: '2-digit',
290
+ hourCycle: 'h23',
291
+ }).format(instant);
292
+ }
@@ -0,0 +1,8 @@
1
+ import type { ApplicationDefinition, BootstrapArguments, HostedApplication } from './index.js';
2
+ export interface BootstrapBaseOptions<TDefinition extends ApplicationDefinition> {
3
+ readonly application: TDefinition;
4
+ readonly environment?: unknown;
5
+ }
6
+ export type BootstrapOptions<TDefinition extends ApplicationDefinition> = BootstrapBaseOptions<TDefinition> & BootstrapArguments<TDefinition>;
7
+ /** generic hostへruntime固有listenerを持ち込まないため、listener ownershipはruntime adapterへ委ねる。 */
8
+ export declare function bootstrap<const TDefinition extends ApplicationDefinition>(options: BootstrapOptions<TDefinition>): HostedApplication<TDefinition>;
@@ -0,0 +1,14 @@
1
+ import { binding } from './binding.js';
2
+ /** generic hostへruntime固有listenerを持ち込まないため、listener ownershipはruntime adapterへ委ねる。 */
3
+ export function bootstrap(options) {
4
+ const host = binding.host(options);
5
+ const http = 'http' in host ? host.http : undefined;
6
+ if (http) {
7
+ Object.assign(host.application, {
8
+ fetch(request) {
9
+ return http.handle(request);
10
+ },
11
+ });
12
+ }
13
+ return host.application;
14
+ }
@@ -0,0 +1,62 @@
1
+ import type { ArgsClass, ModuleProtocols, ModuleInstance, SchemaInput, TaskArguments, TaskDescriptor, TaskOutput, TriggerDescriptor } from '../core/index.js';
2
+ import type { ApplicationGraphIR } from '../graph/index.js';
3
+ import type { Logger } from '../runtime/index.js';
4
+ export interface ApplicationDefinitionOptions<TModules extends readonly ModuleInstance[], TArguments extends ArgsClass | undefined, TTasks extends readonly TaskDescriptor<any, any>[], TTriggers extends readonly TriggerDescriptor[]> {
5
+ readonly modules: TModules;
6
+ readonly arguments?: TArguments;
7
+ readonly tasks?: TTasks;
8
+ readonly triggers?: TTriggers;
9
+ readonly logger?: Logger;
10
+ }
11
+ export interface ApplicationDefinition<TModules extends readonly ModuleInstance[] = readonly ModuleInstance[], TArguments extends ArgsClass | undefined = ArgsClass | undefined, TTasks extends readonly TaskDescriptor<any, any>[] = readonly TaskDescriptor<any, any>[], TTriggers extends readonly TriggerDescriptor[] = readonly TriggerDescriptor[]> {
12
+ readonly kind: 'application-definition';
13
+ readonly modules: TModules;
14
+ readonly arguments: TArguments;
15
+ readonly tasks: TTasks;
16
+ readonly triggers: TTriggers;
17
+ readonly logger?: Logger;
18
+ }
19
+ export declare function defineApplication<const TModules extends readonly ModuleInstance[], const TArguments extends ArgsClass | undefined = undefined, const TTasks extends readonly TaskDescriptor<any, any>[] = readonly [], const TTriggers extends readonly TriggerDescriptor[] = readonly []>(options: ApplicationDefinitionOptions<TModules, TArguments, TTasks, TTriggers>): ApplicationDefinition<TModules, TArguments, TTasks, TTriggers>;
20
+ export type ApplicationArgumentsInput<TDefinition extends ApplicationDefinition> = TDefinition['arguments'] extends ArgsClass<infer TSchema> ? SchemaInput<TSchema> : never;
21
+ export type BootstrapArguments<TDefinition extends ApplicationDefinition> = TDefinition['arguments'] extends ArgsClass<infer TSchema> ? {} extends SchemaInput<TSchema> ? {
22
+ readonly arguments?: SchemaInput<TSchema>;
23
+ } : {
24
+ readonly arguments: SchemaInput<TSchema>;
25
+ } : {
26
+ readonly arguments?: never;
27
+ };
28
+ export interface BaseApplication {
29
+ readonly graph: ApplicationGraphIR;
30
+ init(): Promise<this>;
31
+ close(signal?: string): Promise<void>;
32
+ }
33
+ export interface TaskApplicationCapability<TTasks extends readonly TaskDescriptor<any, any>[]> {
34
+ run<TTask extends TTasks[number]>(task: TTask, ...args: TaskArguments<TTask>): Promise<TaskOutput<TTask>>;
35
+ }
36
+ export interface HttpApplicationCapability {
37
+ fetch(request: Request): Promise<Response>;
38
+ }
39
+ export interface TriggerApplicationCapability {
40
+ readonly triggers: {
41
+ start(): Promise<void>;
42
+ stop(): Promise<void>;
43
+ };
44
+ }
45
+ export interface QueueConsumerHandle {
46
+ stop(): Promise<void>;
47
+ }
48
+ export interface QueueConsumerDriver {
49
+ start(options: {
50
+ consume(payload: unknown): Promise<void>;
51
+ }): Promise<QueueConsumerHandle>;
52
+ }
53
+ export type HasProtocol<TDefinition extends ApplicationDefinition, TProtocol extends string> = Extract<ModuleProtocols<TDefinition['modules'][number]>, TProtocol> extends never ? false : true;
54
+ export type HasHttp<TDefinition extends ApplicationDefinition> = HasProtocol<TDefinition, 'http'>;
55
+ export type HasMessagePort<TDefinition extends ApplicationDefinition> = HasProtocol<TDefinition, 'messagePort'>;
56
+ export type HasTasks<TDefinition extends ApplicationDefinition> = TDefinition['tasks'] extends readonly [] ? false : true;
57
+ export type HasTriggers<TDefinition extends ApplicationDefinition> = TDefinition['triggers'] extends readonly [] ? false : true;
58
+ type TaskCapability<TDefinition extends ApplicationDefinition> = TDefinition['tasks'] extends readonly [] ? {} : TaskApplicationCapability<TDefinition['tasks']>;
59
+ export type HostedApplication<TDefinition extends ApplicationDefinition> = BaseApplication & TaskCapability<TDefinition> & (HasHttp<TDefinition> extends true ? HttpApplicationCapability : {}) & (HasTriggers<TDefinition> extends true ? TriggerApplicationCapability : {});
60
+ export type InvocationApplication<TDefinition extends ApplicationDefinition> = BaseApplication & TaskCapability<TDefinition>;
61
+ export { binding } from './binding.js';
62
+ export type { HostBinding, HostBindingApplication, InvocationBinding, InvocationBindingBaseOptions, InvocationBindingOptions, } from './binding.js';
@@ -0,0 +1,11 @@
1
+ export function defineApplication(options) {
2
+ return Object.freeze({
3
+ kind: 'application-definition',
4
+ modules: options.modules,
5
+ arguments: options.arguments,
6
+ tasks: options.tasks ?? [],
7
+ triggers: options.triggers ?? [],
8
+ ...(options.logger === undefined ? {} : { logger: options.logger }),
9
+ });
10
+ }
11
+ export { binding } from './binding.js';
@@ -0,0 +1,28 @@
1
+ import type { ApplicationDefinition } from './index.js';
2
+ export interface OpenApiInfo {
3
+ readonly title: string;
4
+ readonly version: string;
5
+ readonly description?: string;
6
+ }
7
+ export interface OpenApiServer {
8
+ readonly url: string;
9
+ readonly description?: string;
10
+ }
11
+ export interface GenerateOpenApiOptions {
12
+ readonly info: OpenApiInfo;
13
+ readonly servers?: readonly OpenApiServer[];
14
+ }
15
+ export interface OpenApiDocument {
16
+ readonly openapi: '3.2.0';
17
+ readonly jsonSchemaDialect: 'https://json-schema.org/draft/2020-12/schema';
18
+ readonly info: OpenApiInfo;
19
+ readonly servers?: readonly OpenApiServer[];
20
+ readonly paths: Readonly<Record<string, OpenApiPathItem>>;
21
+ readonly components?: {
22
+ readonly schemas?: Readonly<Record<string, JsonSchema>>;
23
+ };
24
+ }
25
+ type JsonSchema = Record<string, unknown>;
26
+ type OpenApiPathItem = Record<string, unknown>;
27
+ export declare function generateOpenApi(application: ApplicationDefinition, options: GenerateOpenApiOptions): OpenApiDocument;
28
+ export {};