@loutrejs/loutre 0.5.0 → 0.7.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 +63 -32
- package/dist/adapters/aws-lambda.d.ts +5 -3
- package/dist/adapters/aws-lambda.js +53 -23
- package/dist/adapters/bun.d.ts +7 -4
- package/dist/adapters/bun.js +52 -42
- package/dist/adapters/cloudflare-workers.d.ts +5 -3
- package/dist/adapters/cloudflare-workers.js +23 -28
- package/dist/adapters/deno.d.ts +7 -4
- package/dist/adapters/deno.js +94 -57
- package/dist/adapters/electron.d.ts +13 -5
- package/dist/adapters/electron.js +76 -28
- package/dist/application/definition.d.ts +31 -0
- package/dist/application/definition.js +16 -0
- package/dist/application/index.d.ts +4 -63
- package/dist/application/index.js +2 -11
- package/dist/application/kernel-internal.d.ts +2 -0
- package/dist/application/kernel-internal.js +10 -0
- package/dist/application/kernel.d.ts +22 -0
- package/dist/application/kernel.js +65 -0
- package/dist/core/diagnostic.d.ts +9 -0
- package/dist/core/diagnostic.js +9 -0
- package/dist/core/extension.d.ts +118 -0
- package/dist/core/extension.js +73 -0
- package/dist/core/generic-layer.d.ts +26 -0
- package/dist/core/generic-layer.js +116 -0
- package/dist/core/index.d.ts +32 -15
- package/dist/core/index.js +15 -15
- package/dist/core/injection.d.ts +4 -13
- package/dist/core/injection.js +63 -0
- package/dist/core/instrumentation.d.ts +36 -0
- package/dist/core/model/context.d.ts +18 -0
- package/dist/core/model/context.js +34 -0
- package/dist/core/model/lifecycle.d.ts +5 -0
- package/dist/core/model/lifecycle.js +44 -0
- package/dist/core/model/modules.d.ts +2 -0
- package/dist/core/model/modules.js +101 -0
- package/dist/core/model/providers.d.ts +4 -0
- package/dist/core/model/providers.js +74 -0
- package/dist/core/model/snapshot.d.ts +6 -0
- package/dist/core/model/snapshot.js +31 -0
- package/dist/core/model/types.d.ts +78 -0
- package/dist/core/model/types.js +1 -0
- package/dist/core/model/validation.d.ts +4 -0
- package/dist/core/model/validation.js +40 -0
- package/dist/core/model/visibility.d.ts +4 -0
- package/dist/core/model/visibility.js +20 -0
- package/dist/core/model.d.ts +10 -0
- package/dist/core/model.js +346 -0
- package/dist/core/module.d.ts +8 -13
- package/dist/core/source-location.d.ts +8 -0
- package/dist/core/source-location.js +31 -0
- package/dist/core/token-internal.d.ts +3 -0
- package/dist/core/token-internal.js +4 -0
- package/dist/core/type.d.ts +7 -0
- package/dist/core/type.js +4 -0
- package/dist/devtools/capsule.d.ts +51 -0
- package/dist/devtools/capsule.js +245 -0
- package/dist/devtools/index.d.ts +11 -0
- package/dist/devtools/index.js +6 -0
- package/dist/devtools/inspector.d.ts +57 -0
- package/dist/devtools/inspector.js +1 -0
- package/dist/devtools/module.d.ts +18 -0
- package/dist/devtools/module.js +26 -0
- package/dist/devtools/observer.d.ts +77 -0
- package/dist/devtools/observer.js +10 -0
- package/dist/devtools/protocol.d.ts +1 -0
- package/dist/devtools/protocol.js +1 -0
- package/dist/devtools/provider-instrumentation.d.ts +14 -0
- package/dist/devtools/provider-instrumentation.js +117 -0
- package/dist/devtools/runtime.d.ts +20 -0
- package/dist/devtools/runtime.js +519 -0
- package/dist/devtools/values.d.ts +35 -0
- package/dist/devtools/values.js +175 -0
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/graph/index.d.ts +2 -3
- package/dist/graph/index.js +1 -3
- package/dist/graph/model.d.ts +34 -0
- package/dist/graph/model.js +127 -0
- package/dist/http/auth.d.ts +46 -0
- package/dist/http/auth.js +96 -0
- package/dist/http/client.d.ts +28 -31
- package/dist/http/client.js +58 -44
- package/dist/http/cors-internal.d.ts +29 -0
- package/dist/http/cors-internal.js +160 -0
- package/dist/http/cors.d.ts +5 -8
- package/dist/http/cors.js +7 -165
- package/dist/http/extension.d.ts +348 -0
- package/dist/http/extension.js +1061 -0
- package/dist/http/index.d.ts +9 -7
- package/dist/http/index.js +5 -7
- package/dist/{application → http}/openapi.d.ts +12 -3
- package/dist/{application → http}/openapi.js +164 -102
- package/dist/http/path.d.ts +1 -42
- package/dist/http/path.js +1 -2
- package/dist/http/runtime/request.d.ts +17 -0
- package/dist/http/runtime/request.js +109 -0
- package/dist/http/runtime/response.d.ts +18 -0
- package/dist/http/runtime/response.js +108 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/message-port/extension.d.ts +66 -0
- package/dist/message-port/extension.js +411 -0
- package/dist/message-port/index.d.ts +4 -73
- package/dist/message-port/index.js +2 -189
- package/dist/message-port/transport.d.ts +11 -0
- package/dist/message-port/transport.js +45 -0
- package/dist/runtime/async-iterator-lifecycle.d.ts +12 -0
- package/dist/runtime/async-iterator-lifecycle.js +95 -0
- package/dist/runtime/capability.d.ts +11 -11
- package/dist/runtime/capability.js +3 -3
- package/dist/runtime/di.d.ts +5 -21
- package/dist/runtime/di.js +22 -289
- package/dist/runtime/execution-tracker.d.ts +8 -0
- package/dist/runtime/execution-tracker.js +54 -0
- package/dist/runtime/index.d.ts +15 -9
- package/dist/runtime/index.js +9 -9
- package/dist/runtime/ingress-gate.d.ts +7 -0
- package/dist/runtime/ingress-gate.js +33 -0
- package/dist/runtime/kernel.d.ts +25 -0
- package/dist/runtime/kernel.js +447 -0
- package/dist/runtime/logger.d.ts +2 -0
- package/dist/runtime/logger.js +2 -0
- package/dist/runtime/node-capabilities.d.ts +1 -1
- package/dist/runtime/node-capabilities.js +1 -1
- package/dist/runtime/server-port.d.ts +2 -0
- package/dist/runtime/server-port.js +11 -0
- package/dist/tasks/extension.d.ts +144 -0
- package/dist/tasks/extension.js +525 -0
- package/dist/tasks/index.d.ts +2 -0
- package/dist/tasks/index.js +1 -0
- package/dist/tasks/runtime/cron.d.ts +6 -0
- package/dist/tasks/runtime/cron.js +89 -0
- package/dist/websocket/errors.d.ts +9 -0
- package/dist/websocket/errors.js +18 -0
- package/dist/websocket/extension.d.ts +154 -0
- package/dist/websocket/extension.js +533 -0
- package/dist/websocket/index.d.ts +3 -0
- package/dist/websocket/index.js +2 -0
- package/package.json +16 -16
- package/dist/application/binding.d.ts +0 -33
- package/dist/application/binding.js +0 -295
- package/dist/application/host.d.ts +0 -8
- package/dist/application/host.js +0 -14
- package/dist/application/queue.d.ts +0 -3
- package/dist/application/queue.js +0 -4
- package/dist/application/scheduler.d.ts +0 -2
- package/dist/application/scheduler.js +0 -45
- package/dist/core/context-key.d.ts +0 -16
- package/dist/core/context-key.js +0 -17
- package/dist/core/contract-internal.d.ts +0 -22
- package/dist/core/contract-internal.js +0 -61
- package/dist/core/contract.d.ts +0 -96
- package/dist/core/contract.js +0 -88
- package/dist/core/execution.d.ts +0 -78
- package/dist/core/execution.js +0 -61
- package/dist/core/implementation.d.ts +0 -44
- package/dist/core/implementation.js +0 -35
- package/dist/core/layer.d.ts +0 -137
- package/dist/core/layer.js +0 -81
- package/dist/graph/compiler.d.ts +0 -4
- package/dist/graph/compiler.js +0 -228
- package/dist/graph/graph.d.ts +0 -18
- package/dist/graph/graph.js +0 -1118
- package/dist/graph/ir.d.ts +0 -175
- package/dist/http/application.d.ts +0 -18
- package/dist/http/application.js +0 -515
- package/dist/http/basic-auth.d.ts +0 -37
- package/dist/http/basic-auth.js +0 -67
- package/dist/http/definitions.d.ts +0 -363
- package/dist/http/definitions.js +0 -310
- package/dist/http/hosted-protocol.d.ts +0 -4
- package/dist/http/hosted-protocol.js +0 -4
- package/dist/http/internal.d.ts +0 -8
- package/dist/http/internal.js +0 -1
- package/dist/http/params.d.ts +0 -2
- package/dist/http/params.js +0 -19
- package/dist/http/request-body.d.ts +0 -4
- package/dist/http/request-body.js +0 -6
- package/dist/http/validate.d.ts +0 -20
- package/dist/http/validate.js +0 -6
- package/dist/message-port/environment.d.ts +0 -2
- package/dist/message-port/environment.js +0 -4
- package/dist/runtime/application.d.ts +0 -38
- package/dist/runtime/application.js +0 -339
- package/dist/runtime/pipeline.d.ts +0 -11
- package/dist/runtime/pipeline.js +0 -161
- /package/dist/{graph/ir.js → core/instrumentation.js} +0 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { collectInjectedDependencies, defineExecution, defineExecutionExtension, provide, runInInjectionContext, token, validateSchema, } from '../core/index.js';
|
|
2
|
+
import { isValidCronExpression, matchesCronTrigger } from './runtime/cron.js';
|
|
3
|
+
const taskExecutionIdentityKey = Symbol.for('loutre.tasks.task-execution-identity');
|
|
4
|
+
const queueDriverToken = Symbol.for('loutre.tasks.queue-driver');
|
|
5
|
+
export const tasksExtension = defineExecutionExtension({
|
|
6
|
+
kind: 'execution-extension',
|
|
7
|
+
abiVersion: '1',
|
|
8
|
+
name: 'loutre:tasks',
|
|
9
|
+
compile(definition) {
|
|
10
|
+
switch (definition.type) {
|
|
11
|
+
case 'task':
|
|
12
|
+
return {
|
|
13
|
+
kind: 'execution',
|
|
14
|
+
id: `task.${definition.name}`,
|
|
15
|
+
executionKind: 'task.invocation',
|
|
16
|
+
dependencies: collectInjectedDependencies({
|
|
17
|
+
kind: 'execution',
|
|
18
|
+
id: `task:${definition.name}`,
|
|
19
|
+
name: definition.name,
|
|
20
|
+
}, () => definition.factory()),
|
|
21
|
+
capabilities: [],
|
|
22
|
+
compiled: Object.freeze({
|
|
23
|
+
type: 'task',
|
|
24
|
+
taskExecutionId: definition[taskExecutionIdentityKey],
|
|
25
|
+
name: definition.name,
|
|
26
|
+
factory: definition.factory,
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
case 'cron':
|
|
30
|
+
return {
|
|
31
|
+
kind: 'execution',
|
|
32
|
+
id: `trigger.${definition.name}`,
|
|
33
|
+
executionKind: 'trigger.cron',
|
|
34
|
+
dependencies: [],
|
|
35
|
+
capabilities: [],
|
|
36
|
+
compiled: Object.freeze({
|
|
37
|
+
type: 'cron',
|
|
38
|
+
name: definition.name,
|
|
39
|
+
expression: definition.expression,
|
|
40
|
+
timezone: definition.timezone,
|
|
41
|
+
overlap: definition.overlap,
|
|
42
|
+
taskExecutionId: definition.task[taskExecutionIdentityKey],
|
|
43
|
+
taskName: definition.task.name,
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
case 'fixed-delay':
|
|
47
|
+
return {
|
|
48
|
+
kind: 'execution',
|
|
49
|
+
id: `trigger.${definition.name}`,
|
|
50
|
+
executionKind: 'trigger.fixed-delay',
|
|
51
|
+
dependencies: [],
|
|
52
|
+
capabilities: [],
|
|
53
|
+
compiled: Object.freeze({
|
|
54
|
+
type: 'fixed-delay',
|
|
55
|
+
name: definition.name,
|
|
56
|
+
delay: definition.delay,
|
|
57
|
+
immediate: definition.immediate,
|
|
58
|
+
taskExecutionId: definition.task[taskExecutionIdentityKey],
|
|
59
|
+
taskName: definition.task.name,
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
case 'queue-consumer':
|
|
63
|
+
return {
|
|
64
|
+
kind: 'execution',
|
|
65
|
+
id: `trigger.${definition.name}`,
|
|
66
|
+
executionKind: 'trigger.queue-consumer',
|
|
67
|
+
dependencies: [queueRuntimeToken(definition.queue)],
|
|
68
|
+
capabilities: [],
|
|
69
|
+
compiled: Object.freeze({
|
|
70
|
+
type: 'queue-consumer',
|
|
71
|
+
name: definition.name,
|
|
72
|
+
queueName: definition.queue.name,
|
|
73
|
+
queuePayload: definition.queue.payload,
|
|
74
|
+
queueDriver: queueRuntimeToken(definition.queue),
|
|
75
|
+
taskExecutionId: definition.task[taskExecutionIdentityKey],
|
|
76
|
+
taskName: definition.task.name,
|
|
77
|
+
}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
references(definition) {
|
|
82
|
+
switch (definition.type) {
|
|
83
|
+
case 'task':
|
|
84
|
+
return [];
|
|
85
|
+
case 'cron':
|
|
86
|
+
case 'fixed-delay':
|
|
87
|
+
case 'queue-consumer':
|
|
88
|
+
return [definition.task];
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
validate({ executions }) {
|
|
92
|
+
const taskExecutionIds = new Set(executions.flatMap((execution) => execution.compiled.type === 'task'
|
|
93
|
+
? [execution.compiled.taskExecutionId]
|
|
94
|
+
: []));
|
|
95
|
+
const diagnostics = [];
|
|
96
|
+
for (const execution of executions) {
|
|
97
|
+
const compiled = execution.compiled;
|
|
98
|
+
if (compiled.type === 'task')
|
|
99
|
+
continue;
|
|
100
|
+
if (!taskExecutionIds.has(compiled.taskExecutionId)) {
|
|
101
|
+
diagnostics.push({
|
|
102
|
+
code: 'LUTRE_TRIGGER_TASK_MISSING',
|
|
103
|
+
message: `Trigger ${compiled.name} references a Task that is unavailable in the compiled Tasks execution set.`,
|
|
104
|
+
path: execution.id,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (compiled.type !== 'cron')
|
|
108
|
+
continue;
|
|
109
|
+
if (!isValidCronExpression(compiled.expression)) {
|
|
110
|
+
diagnostics.push({
|
|
111
|
+
code: 'LUTRE_TRIGGER_INVALID_CRON',
|
|
112
|
+
message: `Trigger ${compiled.name} must use a portable 5-field cron expression.`,
|
|
113
|
+
path: execution.id,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
new Intl.DateTimeFormat('en-US', {
|
|
118
|
+
timeZone: compiled.timezone,
|
|
119
|
+
}).format();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
diagnostics.push({
|
|
123
|
+
code: 'LUTRE_TRIGGER_INVALID_TIMEZONE',
|
|
124
|
+
message: `Trigger ${compiled.name} must use a valid IANA timezone.`,
|
|
125
|
+
path: execution.id,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return diagnostics;
|
|
130
|
+
},
|
|
131
|
+
createRuntime({ executions, applicationRuntime }) {
|
|
132
|
+
return createTasksRuntime(executions, applicationRuntime);
|
|
133
|
+
},
|
|
134
|
+
projectGraph: ({ execution }) => {
|
|
135
|
+
const compiled = execution.compiled;
|
|
136
|
+
switch (compiled.type) {
|
|
137
|
+
case 'task':
|
|
138
|
+
return { type: 'task', name: compiled.name };
|
|
139
|
+
case 'cron':
|
|
140
|
+
return {
|
|
141
|
+
type: 'cron',
|
|
142
|
+
name: compiled.name,
|
|
143
|
+
expression: compiled.expression,
|
|
144
|
+
timezone: compiled.timezone,
|
|
145
|
+
overlap: compiled.overlap,
|
|
146
|
+
task: compiled.taskName,
|
|
147
|
+
};
|
|
148
|
+
case 'fixed-delay':
|
|
149
|
+
return {
|
|
150
|
+
type: 'fixed-delay',
|
|
151
|
+
name: compiled.name,
|
|
152
|
+
delay: compiled.delay,
|
|
153
|
+
immediate: compiled.immediate,
|
|
154
|
+
task: compiled.taskName,
|
|
155
|
+
};
|
|
156
|
+
case 'queue-consumer':
|
|
157
|
+
return {
|
|
158
|
+
type: 'queue-consumer',
|
|
159
|
+
name: compiled.name,
|
|
160
|
+
queue: compiled.queueName,
|
|
161
|
+
task: compiled.taskName,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
host: {
|
|
166
|
+
namespace: 'tasks',
|
|
167
|
+
create: ({ runtime }) => ({
|
|
168
|
+
run: (definition, ...arguments_) => Reflect.apply(runtime.run, runtime, [
|
|
169
|
+
taskExecutionIdentity(definition),
|
|
170
|
+
definition.name,
|
|
171
|
+
...arguments_,
|
|
172
|
+
]),
|
|
173
|
+
start: () => runtime.startTriggers(),
|
|
174
|
+
stop: () => runtime.stopTriggers(),
|
|
175
|
+
}),
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
export function task(definition) {
|
|
179
|
+
const taskExecutionId = Symbol(`loutre.tasks.execution:${definition.name}`);
|
|
180
|
+
return defineExecution(tasksExtension, {
|
|
181
|
+
type: 'task',
|
|
182
|
+
name: definition.name,
|
|
183
|
+
factory: definition.factory,
|
|
184
|
+
[taskExecutionIdentityKey]: taskExecutionId,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
export function cron(declaration) {
|
|
188
|
+
return defineExecution(tasksExtension, {
|
|
189
|
+
type: 'cron',
|
|
190
|
+
name: declaration.name,
|
|
191
|
+
expression: declaration.expression,
|
|
192
|
+
timezone: declaration.timezone,
|
|
193
|
+
overlap: declaration.overlap ?? 'skip',
|
|
194
|
+
task: declaration.task,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
export function fixedDelay(declaration) {
|
|
198
|
+
if (!Number.isFinite(declaration.delay) ||
|
|
199
|
+
!Number.isInteger(declaration.delay) ||
|
|
200
|
+
declaration.delay < 0) {
|
|
201
|
+
throw new Error('LUTRE_TRIGGER_INVALID_FIXED_DELAY: fixedDelay.delay must be a non-negative finite integer.');
|
|
202
|
+
}
|
|
203
|
+
return defineExecution(tasksExtension, {
|
|
204
|
+
type: 'fixed-delay',
|
|
205
|
+
name: declaration.name,
|
|
206
|
+
delay: declaration.delay,
|
|
207
|
+
immediate: declaration.immediate ?? false,
|
|
208
|
+
task: declaration.task,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
export function queue(declaration) {
|
|
212
|
+
const payload = declaration.payload;
|
|
213
|
+
if (payload === null ||
|
|
214
|
+
(typeof payload !== 'object' && typeof payload !== 'function') ||
|
|
215
|
+
!('~standard' in payload)) {
|
|
216
|
+
throw new Error('LUTRE_QUEUE_PAYLOAD_SCHEMA: queue.payload must implement Standard Schema.');
|
|
217
|
+
}
|
|
218
|
+
return Object.freeze({
|
|
219
|
+
kind: 'queue',
|
|
220
|
+
name: declaration.name,
|
|
221
|
+
payload: declaration.payload,
|
|
222
|
+
[queueDriverToken]: token(`loutre.tasks.queue-driver.${declaration.name}`),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
function queueRuntimeToken(descriptor) {
|
|
226
|
+
return descriptor[queueDriverToken];
|
|
227
|
+
}
|
|
228
|
+
export function bindQueueDriver(descriptor, driver) {
|
|
229
|
+
return provide(queueRuntimeToken(descriptor)).useValue(driver);
|
|
230
|
+
}
|
|
231
|
+
export function consume(declaration) {
|
|
232
|
+
return defineExecution(tasksExtension, {
|
|
233
|
+
type: 'queue-consumer',
|
|
234
|
+
name: declaration.name,
|
|
235
|
+
queue: declaration.queue,
|
|
236
|
+
task: declaration.task,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function createTasksRuntime(executions, applicationRuntime) {
|
|
240
|
+
const runtimes = new Map();
|
|
241
|
+
const runtimeExecutionIds = new Map();
|
|
242
|
+
const triggers = [];
|
|
243
|
+
for (const execution of executions) {
|
|
244
|
+
const compiled = execution.compiled;
|
|
245
|
+
if (compiled.type !== 'task') {
|
|
246
|
+
triggers.push(compiled);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const runtime = runInInjectionContext({
|
|
250
|
+
consumer: {
|
|
251
|
+
kind: 'execution',
|
|
252
|
+
id: `task:${compiled.name}`,
|
|
253
|
+
name: compiled.name,
|
|
254
|
+
},
|
|
255
|
+
resolve: (dependency) => applicationRuntime.resolve(dependency, execution.id),
|
|
256
|
+
}, () => compiled.factory());
|
|
257
|
+
runtimes.set(compiled.taskExecutionId, runtime);
|
|
258
|
+
runtimeExecutionIds.set(compiled.taskExecutionId, execution.id);
|
|
259
|
+
}
|
|
260
|
+
let triggerHandles = [];
|
|
261
|
+
let triggerStartup;
|
|
262
|
+
let triggerStop;
|
|
263
|
+
let triggersStarted = false;
|
|
264
|
+
let state = 'running';
|
|
265
|
+
const activeInvocations = new Set();
|
|
266
|
+
const invokeTask = (taskExecutionId, taskName, arguments_) => {
|
|
267
|
+
if (state !== 'running') {
|
|
268
|
+
return Promise.reject(new Error(`LUTRE_TASKS_${state.toUpperCase()}`));
|
|
269
|
+
}
|
|
270
|
+
const runtime = runtimes.get(taskExecutionId);
|
|
271
|
+
if (!runtime) {
|
|
272
|
+
return Promise.reject(new Error(`LUTRE_TASK_NOT_REGISTERED: ${taskName}`));
|
|
273
|
+
}
|
|
274
|
+
const invocation = (async () => {
|
|
275
|
+
const executionId = runtimeExecutionIds.get(taskExecutionId) ?? taskName;
|
|
276
|
+
const hasInput = arguments_.length > 0;
|
|
277
|
+
const input = hasInput ? arguments_[0] : undefined;
|
|
278
|
+
const lease = applicationRuntime.beginExecution({
|
|
279
|
+
executionId,
|
|
280
|
+
executionKind: 'task.invocation',
|
|
281
|
+
graphNodeId: executionId,
|
|
282
|
+
name: taskName,
|
|
283
|
+
}, {
|
|
284
|
+
input,
|
|
285
|
+
invoke: (nextInput) => invokeTask(taskExecutionId, taskName, hasInput ? [nextInput] : []),
|
|
286
|
+
});
|
|
287
|
+
let result;
|
|
288
|
+
let hasResult = false;
|
|
289
|
+
try {
|
|
290
|
+
const invoke = () => Reflect.apply(runtime, undefined, arguments_);
|
|
291
|
+
result = await (lease.run ? lease.run(invoke) : invoke());
|
|
292
|
+
hasResult = true;
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
lease.fail?.(error);
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
if (hasResult)
|
|
301
|
+
lease.complete(result);
|
|
302
|
+
else
|
|
303
|
+
lease.complete();
|
|
304
|
+
}
|
|
305
|
+
})();
|
|
306
|
+
activeInvocations.add(invocation);
|
|
307
|
+
void invocation.then(() => activeInvocations.delete(invocation), () => activeInvocations.delete(invocation));
|
|
308
|
+
return invocation;
|
|
309
|
+
};
|
|
310
|
+
const run = (taskExecutionId, taskName, ...arguments_) => invokeTask(taskExecutionId, taskName, arguments_);
|
|
311
|
+
const stopTriggers = () => {
|
|
312
|
+
if (triggerStop)
|
|
313
|
+
return triggerStop;
|
|
314
|
+
const stopping = (async () => {
|
|
315
|
+
const startup = triggerStartup;
|
|
316
|
+
if (startup)
|
|
317
|
+
await startup.catch(() => undefined);
|
|
318
|
+
const handles = triggerHandles;
|
|
319
|
+
const stopOrder = handles.toReversed();
|
|
320
|
+
const results = await Promise.allSettled(stopOrder.map((handle) => handle.stop()));
|
|
321
|
+
const failedHandles = [];
|
|
322
|
+
const errors = [];
|
|
323
|
+
for (const [index, result] of results.entries()) {
|
|
324
|
+
if (result.status !== 'rejected')
|
|
325
|
+
continue;
|
|
326
|
+
failedHandles.push(stopOrder[index]);
|
|
327
|
+
errors.push(result.reason);
|
|
328
|
+
}
|
|
329
|
+
triggerHandles = failedHandles.toReversed();
|
|
330
|
+
triggersStarted = failedHandles.length > 0;
|
|
331
|
+
if (errors.length > 0) {
|
|
332
|
+
throw new AggregateError(errors, 'Trigger stop failed');
|
|
333
|
+
}
|
|
334
|
+
})();
|
|
335
|
+
let trackedStop;
|
|
336
|
+
trackedStop = stopping.finally(() => {
|
|
337
|
+
if (triggerStop === trackedStop)
|
|
338
|
+
triggerStop = undefined;
|
|
339
|
+
});
|
|
340
|
+
triggerStop = trackedStop;
|
|
341
|
+
return trackedStop;
|
|
342
|
+
};
|
|
343
|
+
const startTriggers = async () => {
|
|
344
|
+
if (state !== 'running') {
|
|
345
|
+
throw new Error(`LUTRE_TASKS_${state.toUpperCase()}`);
|
|
346
|
+
}
|
|
347
|
+
if (triggersStarted) {
|
|
348
|
+
throw new Error('LUTRE_TRIGGERS_ALREADY_STARTED: Trigger Engine is already started.');
|
|
349
|
+
}
|
|
350
|
+
const lease = applicationRuntime.beginExecution();
|
|
351
|
+
triggersStarted = true;
|
|
352
|
+
const started = [];
|
|
353
|
+
const startup = (async () => {
|
|
354
|
+
try {
|
|
355
|
+
for (const trigger of triggers) {
|
|
356
|
+
if (trigger.type === 'task')
|
|
357
|
+
continue;
|
|
358
|
+
const handle = await startTrigger(trigger, run, applicationRuntime);
|
|
359
|
+
started.push(handle);
|
|
360
|
+
if (state !== 'running') {
|
|
361
|
+
throw tasksStateError(state);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
triggerHandles = started;
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
const stopOrder = started.toReversed();
|
|
368
|
+
const cleanupResults = await Promise.allSettled(stopOrder.map((handle) => handle.stop()));
|
|
369
|
+
const failedHandles = [];
|
|
370
|
+
const cleanupErrors = [];
|
|
371
|
+
for (const [index, result] of cleanupResults.entries()) {
|
|
372
|
+
if (result.status !== 'rejected')
|
|
373
|
+
continue;
|
|
374
|
+
failedHandles.push(stopOrder[index]);
|
|
375
|
+
cleanupErrors.push(result.reason);
|
|
376
|
+
}
|
|
377
|
+
triggerHandles = failedHandles.toReversed();
|
|
378
|
+
triggersStarted = failedHandles.length > 0;
|
|
379
|
+
if (cleanupErrors.length > 0) {
|
|
380
|
+
throw new AggregateError([error, ...cleanupErrors], 'Trigger startup failed and cleanup also failed.', { cause: error });
|
|
381
|
+
}
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
})();
|
|
385
|
+
let trackedStartup;
|
|
386
|
+
trackedStartup = startup.finally(() => {
|
|
387
|
+
lease.complete();
|
|
388
|
+
if (triggerStartup === trackedStartup)
|
|
389
|
+
triggerStartup = undefined;
|
|
390
|
+
});
|
|
391
|
+
triggerStartup = trackedStartup;
|
|
392
|
+
await trackedStartup;
|
|
393
|
+
};
|
|
394
|
+
return {
|
|
395
|
+
run: run,
|
|
396
|
+
startTriggers,
|
|
397
|
+
stopTriggers,
|
|
398
|
+
async drain() {
|
|
399
|
+
if (state === 'stopped')
|
|
400
|
+
return;
|
|
401
|
+
state = 'draining';
|
|
402
|
+
await stopTriggers();
|
|
403
|
+
await Promise.allSettled(activeInvocations);
|
|
404
|
+
},
|
|
405
|
+
async close() {
|
|
406
|
+
if (state === 'stopped')
|
|
407
|
+
return;
|
|
408
|
+
try {
|
|
409
|
+
await stopTriggers();
|
|
410
|
+
}
|
|
411
|
+
finally {
|
|
412
|
+
state = 'stopped';
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
async function startTrigger(trigger, run, applicationRuntime) {
|
|
418
|
+
switch (trigger.type) {
|
|
419
|
+
case 'cron':
|
|
420
|
+
return startCronTrigger(trigger, run);
|
|
421
|
+
case 'fixed-delay':
|
|
422
|
+
return startFixedDelayTrigger(trigger, run);
|
|
423
|
+
case 'queue-consumer':
|
|
424
|
+
return startQueueTrigger(trigger, run, applicationRuntime);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function startCronTrigger(trigger, run) {
|
|
428
|
+
let stopped = false;
|
|
429
|
+
let lastMinute;
|
|
430
|
+
const active = new Set();
|
|
431
|
+
const tick = () => {
|
|
432
|
+
if (stopped)
|
|
433
|
+
return;
|
|
434
|
+
const now = new Date();
|
|
435
|
+
if (!matchesCronTrigger(trigger, now))
|
|
436
|
+
return;
|
|
437
|
+
const minute = Math.floor(now.getTime() / 60_000);
|
|
438
|
+
if (lastMinute === minute)
|
|
439
|
+
return;
|
|
440
|
+
lastMinute = minute;
|
|
441
|
+
if (trigger.overlap === 'skip' && active.size > 0)
|
|
442
|
+
return;
|
|
443
|
+
const execution = run(trigger.taskExecutionId, trigger.taskName).catch(() => undefined);
|
|
444
|
+
active.add(execution);
|
|
445
|
+
void execution.finally(() => active.delete(execution));
|
|
446
|
+
};
|
|
447
|
+
tick();
|
|
448
|
+
const timer = setInterval(tick, 1_000);
|
|
449
|
+
return {
|
|
450
|
+
async stop() {
|
|
451
|
+
if (stopped)
|
|
452
|
+
return;
|
|
453
|
+
stopped = true;
|
|
454
|
+
clearInterval(timer);
|
|
455
|
+
await Promise.allSettled(active);
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function startFixedDelayTrigger(trigger, run) {
|
|
460
|
+
let stopped = false;
|
|
461
|
+
let timer;
|
|
462
|
+
let releaseSleep;
|
|
463
|
+
const sleep = () => new Promise((resolve) => {
|
|
464
|
+
releaseSleep = resolve;
|
|
465
|
+
timer = setTimeout(() => {
|
|
466
|
+
timer = undefined;
|
|
467
|
+
releaseSleep = undefined;
|
|
468
|
+
resolve();
|
|
469
|
+
}, trigger.delay);
|
|
470
|
+
});
|
|
471
|
+
const execute = async () => {
|
|
472
|
+
await run(trigger.taskExecutionId, trigger.taskName).catch(() => undefined);
|
|
473
|
+
};
|
|
474
|
+
const loop = (async () => {
|
|
475
|
+
if (!trigger.immediate)
|
|
476
|
+
await sleep();
|
|
477
|
+
while (true) {
|
|
478
|
+
if (stopped)
|
|
479
|
+
break;
|
|
480
|
+
await execute();
|
|
481
|
+
if (stopped)
|
|
482
|
+
break;
|
|
483
|
+
await sleep();
|
|
484
|
+
}
|
|
485
|
+
})();
|
|
486
|
+
return {
|
|
487
|
+
async stop() {
|
|
488
|
+
if (stopped)
|
|
489
|
+
return;
|
|
490
|
+
stopped = true;
|
|
491
|
+
if (timer)
|
|
492
|
+
clearTimeout(timer);
|
|
493
|
+
timer = undefined;
|
|
494
|
+
releaseSleep?.();
|
|
495
|
+
releaseSleep = undefined;
|
|
496
|
+
await loop;
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
async function startQueueTrigger(trigger, run, applicationRuntime) {
|
|
501
|
+
const driver = applicationRuntime.resolve(trigger.queueDriver);
|
|
502
|
+
if (!driver || typeof driver.start !== 'function') {
|
|
503
|
+
throw new Error(`LUTRE_QUEUE_DRIVER_INVALID: Queue ${trigger.queueName} driver is invalid.`);
|
|
504
|
+
}
|
|
505
|
+
const handle = await driver.start({
|
|
506
|
+
consume: async (payload) => {
|
|
507
|
+
const validated = await validateSchema(trigger.queuePayload, payload);
|
|
508
|
+
await run(trigger.taskExecutionId, trigger.taskName, validated);
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
if (!handle || typeof handle.stop !== 'function') {
|
|
512
|
+
throw new Error(`LUTRE_QUEUE_DRIVER_INVALID: Queue ${trigger.queueName} driver returned an invalid handle.`);
|
|
513
|
+
}
|
|
514
|
+
return handle;
|
|
515
|
+
}
|
|
516
|
+
function taskExecutionIdentity(definition) {
|
|
517
|
+
if (taskExecutionIdentityKey in definition &&
|
|
518
|
+
typeof definition[taskExecutionIdentityKey] === 'symbol') {
|
|
519
|
+
return definition[taskExecutionIdentityKey];
|
|
520
|
+
}
|
|
521
|
+
return Symbol('loutre.tasks.unregistered');
|
|
522
|
+
}
|
|
523
|
+
function tasksStateError(state) {
|
|
524
|
+
return new Error(`LUTRE_TASKS_${state.toUpperCase()}`);
|
|
525
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { bindQueueDriver, consume, cron, fixedDelay, queue, task, tasksExtension, } from './extension.js';
|
|
2
|
+
export type { CronOverlap, CronTriggerDefinition, CronTriggerDefinitionData, FixedDelayTriggerDefinition, FixedDelayTriggerDefinitionData, QueueConsumerDriver, QueueConsumerHandle, QueueConsumerTriggerDefinition, QueueConsumerTriggerDefinitionData, QueueDescriptor, QueuePayload, TaskArguments, TaskDefinition, TaskDefinitionData, TaskExecutionIdentity, TaskInput, TaskOutput, TaskRuntime, TasksCompiledCronTrigger, TasksCompiledExecution, TasksCompiledFixedDelayTrigger, TasksCompiledQueueConsumerTrigger, TasksCompiledTask, TasksExtensionRuntime, TasksHostApi, TriggerDefinition, TriggerDefinitionData, } from './extension.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { bindQueueDriver, consume, cron, fixedDelay, queue, task, tasksExtension, } from './extension.js';
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export function matchesCronTrigger(trigger, instant) {
|
|
2
|
+
const [minute, hour, dayOfMonth, month, dayOfWeek] = trigger.expression
|
|
3
|
+
.trim()
|
|
4
|
+
.split(/\s+/);
|
|
5
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
6
|
+
timeZone: trigger.timezone,
|
|
7
|
+
minute: 'numeric',
|
|
8
|
+
hour: 'numeric',
|
|
9
|
+
day: 'numeric',
|
|
10
|
+
month: 'numeric',
|
|
11
|
+
weekday: 'short',
|
|
12
|
+
hourCycle: 'h23',
|
|
13
|
+
}).formatToParts(instant);
|
|
14
|
+
const value = (type) => parts.find((part) => part.type === type)?.value ?? '';
|
|
15
|
+
const weekday = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].indexOf(value('weekday'));
|
|
16
|
+
const dayOfMonthMatches = matchesCronField(dayOfMonth, Number(value('day')), 1, 31);
|
|
17
|
+
const dayOfWeekMatches = matchesCronField(dayOfWeek, weekday, 0, 7, true);
|
|
18
|
+
const bothDaysRestricted = !dayOfMonth.includes('*') && !dayOfWeek.includes('*');
|
|
19
|
+
const dayMatches = bothDaysRestricted
|
|
20
|
+
? dayOfMonthMatches || dayOfWeekMatches
|
|
21
|
+
: dayOfMonthMatches && dayOfWeekMatches;
|
|
22
|
+
return (matchesCronField(minute, Number(value('minute')), 0, 59) &&
|
|
23
|
+
matchesCronField(hour, Number(value('hour')), 0, 23) &&
|
|
24
|
+
matchesCronField(month, Number(value('month')), 1, 12) &&
|
|
25
|
+
dayMatches);
|
|
26
|
+
}
|
|
27
|
+
export function isValidCronExpression(expression) {
|
|
28
|
+
const fields = expression.trim().split(/\s+/);
|
|
29
|
+
if (fields.length !== 5)
|
|
30
|
+
return false;
|
|
31
|
+
const ranges = [
|
|
32
|
+
[0, 59],
|
|
33
|
+
[0, 23],
|
|
34
|
+
[1, 31],
|
|
35
|
+
[1, 12],
|
|
36
|
+
[0, 7],
|
|
37
|
+
];
|
|
38
|
+
return fields.every((field, index) => {
|
|
39
|
+
const range = ranges[index];
|
|
40
|
+
return range !== undefined && isValidCronField(field, range[0], range[1]);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function matchesCronField(expression, value, minimum, maximum, sundayAlias = false) {
|
|
44
|
+
return expression.split(',').some((segment) => {
|
|
45
|
+
const [rangeExpression, stepExpression] = segment.split('/');
|
|
46
|
+
const step = stepExpression === undefined ? 1 : Number(stepExpression);
|
|
47
|
+
if (!Number.isInteger(step) || step <= 0 || !rangeExpression)
|
|
48
|
+
return false;
|
|
49
|
+
let start = minimum;
|
|
50
|
+
let end = maximum;
|
|
51
|
+
if (rangeExpression !== '*') {
|
|
52
|
+
const [startExpression, endExpression] = rangeExpression.split('-');
|
|
53
|
+
start = Number(startExpression);
|
|
54
|
+
end = endExpression === undefined ? start : Number(endExpression);
|
|
55
|
+
}
|
|
56
|
+
const candidates = sundayAlias && value === 0 ? [0, 7] : [value];
|
|
57
|
+
return (Number.isInteger(start) &&
|
|
58
|
+
Number.isInteger(end) &&
|
|
59
|
+
start >= minimum &&
|
|
60
|
+
end <= maximum &&
|
|
61
|
+
candidates.some((candidate) => candidate >= start &&
|
|
62
|
+
candidate <= end &&
|
|
63
|
+
(candidate - start) % step === 0));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function isValidCronField(field, minimum, maximum) {
|
|
67
|
+
return field.split(',').every((segment) => {
|
|
68
|
+
const parts = segment.split('/');
|
|
69
|
+
if (parts.length > 2)
|
|
70
|
+
return false;
|
|
71
|
+
const rangeExpression = parts[0];
|
|
72
|
+
const stepExpression = parts[1];
|
|
73
|
+
if (!rangeExpression)
|
|
74
|
+
return false;
|
|
75
|
+
if (stepExpression !== undefined &&
|
|
76
|
+
(!/^\d+$/.test(stepExpression) || Number(stepExpression) <= 0)) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (rangeExpression === '*')
|
|
80
|
+
return true;
|
|
81
|
+
const bounds = rangeExpression.split('-');
|
|
82
|
+
if (bounds.length > 2 || bounds.some((bound) => !/^\d+$/.test(bound))) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const start = Number(bounds[0]);
|
|
86
|
+
const end = bounds[1] === undefined ? start : Number(bounds[1]);
|
|
87
|
+
return start >= minimum && end <= maximum && start <= end;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class WebSocketMessageDecodeError extends Error {
|
|
2
|
+
constructor(cause?: unknown);
|
|
3
|
+
}
|
|
4
|
+
export declare class WebSocketMessageEncodeError extends Error {
|
|
5
|
+
constructor(cause?: unknown);
|
|
6
|
+
}
|
|
7
|
+
export declare class WebSocketConnectionNotOpenError extends Error {
|
|
8
|
+
constructor(cause?: unknown);
|
|
9
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class WebSocketMessageDecodeError extends Error {
|
|
2
|
+
constructor(cause) {
|
|
3
|
+
super('WebSocket message could not be decoded.', { cause });
|
|
4
|
+
this.name = 'WebSocketMessageDecodeError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class WebSocketMessageEncodeError extends Error {
|
|
8
|
+
constructor(cause) {
|
|
9
|
+
super('WebSocket message could not be encoded.', { cause });
|
|
10
|
+
this.name = 'WebSocketMessageEncodeError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class WebSocketConnectionNotOpenError extends Error {
|
|
14
|
+
constructor(cause) {
|
|
15
|
+
super('WebSocket connection is not open.', { cause });
|
|
16
|
+
this.name = 'WebSocketConnectionNotOpenError';
|
|
17
|
+
}
|
|
18
|
+
}
|