@runium/core 0.0.7 → 0.0.9
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/.eslintrc.json +30 -0
- package/.prettierrc.json +10 -0
- package/README.md +1 -0
- package/package.json +23 -11
- package/src/error.ts +22 -0
- package/src/file.ts +53 -0
- package/src/index.ts +8 -0
- package/src/macros.ts +91 -0
- package/src/project-config.ts +483 -0
- package/src/project-schema.ts +767 -0
- package/src/project.ts +748 -0
- package/src/task.ts +381 -0
- package/src/trigger.ts +170 -0
- package/tsconfig.json +29 -0
- package/types/index.d.ts +510 -0
- package/types/package.json +22 -0
- package/vite.config.ts +123 -0
- package/index.js +0 -586
package/src/project.ts
ADDED
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import expression from 'bcx-expression-evaluator';
|
|
3
|
+
import {
|
|
4
|
+
isCustomAction,
|
|
5
|
+
isCustomTrigger,
|
|
6
|
+
ProjectAction,
|
|
7
|
+
ProjectActionType,
|
|
8
|
+
ProjectConfig,
|
|
9
|
+
ProjectTaskConfig,
|
|
10
|
+
ProjectTaskDependency,
|
|
11
|
+
ProjectTaskHandler,
|
|
12
|
+
ProjectTaskRestartPolicyOnFailure,
|
|
13
|
+
ProjectTaskRestartPolicyType,
|
|
14
|
+
ProjectTaskStartMode,
|
|
15
|
+
ProjectTaskStateCondition,
|
|
16
|
+
ProjectTaskType,
|
|
17
|
+
ProjectTriggerType,
|
|
18
|
+
validateProject,
|
|
19
|
+
} from './project-config';
|
|
20
|
+
import {
|
|
21
|
+
SILENT_EXIT_CODE,
|
|
22
|
+
RuniumTask,
|
|
23
|
+
RuniumTaskState,
|
|
24
|
+
RuniumTaskConstructor,
|
|
25
|
+
Task,
|
|
26
|
+
TaskEvent,
|
|
27
|
+
TaskState,
|
|
28
|
+
TaskStatus,
|
|
29
|
+
} from './task';
|
|
30
|
+
import {
|
|
31
|
+
extendProjectSchema,
|
|
32
|
+
getProjectSchema,
|
|
33
|
+
ProjectSchemaExtension,
|
|
34
|
+
} from './project-schema';
|
|
35
|
+
import { RuniumError } from './error';
|
|
36
|
+
import {
|
|
37
|
+
EventTrigger,
|
|
38
|
+
IntervalTrigger,
|
|
39
|
+
RuniumTrigger,
|
|
40
|
+
RuniumTriggerConstructor,
|
|
41
|
+
RuniumTriggerOptions,
|
|
42
|
+
RuniumTriggerProjectAccessible,
|
|
43
|
+
TimeoutTrigger,
|
|
44
|
+
} from './trigger';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Project events
|
|
48
|
+
*/
|
|
49
|
+
export enum ProjectEvent {
|
|
50
|
+
STATE_CHANGE = 'state-change',
|
|
51
|
+
START_TASK = 'start-task',
|
|
52
|
+
RESTART_TASK = 'restart-task',
|
|
53
|
+
STOP_TASK = 'stop-task',
|
|
54
|
+
PROCESS_ACTION = 'process-action',
|
|
55
|
+
ENABLE_TRIGGER = 'enable-trigger',
|
|
56
|
+
DISABLE_TRIGGER = 'disable-trigger',
|
|
57
|
+
TASK_STATE_CHANGE = 'task-state-change',
|
|
58
|
+
TASK_STDOUT = 'task-stdout',
|
|
59
|
+
TASK_STDERR = 'task-stderr',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Project status
|
|
64
|
+
*/
|
|
65
|
+
export enum ProjectStatus {
|
|
66
|
+
IDLE = 'idle',
|
|
67
|
+
STARTING = 'starting',
|
|
68
|
+
STARTED = 'started',
|
|
69
|
+
STOPPING = 'stopping',
|
|
70
|
+
STOPPED = 'stopped',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Project state
|
|
75
|
+
*/
|
|
76
|
+
export interface ProjectState {
|
|
77
|
+
status: ProjectStatus;
|
|
78
|
+
timestamp: number;
|
|
79
|
+
reason?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Project error codes
|
|
84
|
+
*/
|
|
85
|
+
export enum ProjectErrorCode {
|
|
86
|
+
ACTION_PROCESSOR_ALREADY_REGISTERED = 'project-action-processor-already-registered',
|
|
87
|
+
ACTION_PROCESSOR_INCORRECT = 'project-action-processor-incorrect',
|
|
88
|
+
TASK_PROCESSOR_NOT_FOUND = 'project-task-processor-not-found',
|
|
89
|
+
TASK_PROCESSOR_ALREADY_REGISTERED = 'project-task-processor-already-registered',
|
|
90
|
+
TASK_PROCESSOR_INCORRECT = 'project-task-processor-incorrect',
|
|
91
|
+
TRIGGER_PROCESSOR_ALREADY_REGISTERED = 'project-trigger-processor-already-registered',
|
|
92
|
+
TRIGGER_PROCESSOR_INCORRECT = 'project-trigger-processor-incorrect',
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Task data
|
|
97
|
+
*/
|
|
98
|
+
interface TaskData {
|
|
99
|
+
instance: RuniumTask;
|
|
100
|
+
config: ProjectTaskConfig;
|
|
101
|
+
dependencies: ProjectTaskDependency[];
|
|
102
|
+
dependents: string[] | null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type RuniumActionProcessor = (payload: unknown) => void;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Project
|
|
109
|
+
*/
|
|
110
|
+
export class Project extends EventEmitter {
|
|
111
|
+
/**
|
|
112
|
+
* Project schema
|
|
113
|
+
*/
|
|
114
|
+
private schema: object = getProjectSchema();
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Task processors
|
|
118
|
+
*/
|
|
119
|
+
private taskProcessors: Map<string, RuniumTaskConstructor> = new Map([
|
|
120
|
+
[ProjectTaskType.DEFAULT, Task],
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Action processors
|
|
125
|
+
*/
|
|
126
|
+
private actionProcessors: Map<string, RuniumActionProcessor> = new Map();
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Trigger processors
|
|
130
|
+
*/
|
|
131
|
+
private triggerProcessors: Map<
|
|
132
|
+
string,
|
|
133
|
+
RuniumTriggerConstructor<RuniumTriggerOptions>
|
|
134
|
+
> = new Map();
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Tasks
|
|
138
|
+
*/
|
|
139
|
+
private tasks: Map<string, TaskData> = new Map();
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Triggers
|
|
143
|
+
*/
|
|
144
|
+
private triggers: Map<string, RuniumTrigger<RuniumTriggerOptions>> =
|
|
145
|
+
new Map();
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Project state
|
|
149
|
+
*/
|
|
150
|
+
private state: ProjectState = {
|
|
151
|
+
timestamp: Date.now(),
|
|
152
|
+
status: ProjectStatus.IDLE,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
constructor(private config: ProjectConfig) {
|
|
156
|
+
super();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Validate project configuration
|
|
161
|
+
*/
|
|
162
|
+
validate(): void {
|
|
163
|
+
validateProject(this.config, this.schema);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get project configuration
|
|
168
|
+
*/
|
|
169
|
+
getConfig(): ProjectConfig {
|
|
170
|
+
return { ...this.config };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Set project configuration
|
|
175
|
+
* @param config
|
|
176
|
+
*/
|
|
177
|
+
setConfig(config: ProjectConfig): void {
|
|
178
|
+
this.config = { ...config };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get project state
|
|
183
|
+
*/
|
|
184
|
+
getState(): ProjectState {
|
|
185
|
+
return { ...this.state };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Start project
|
|
190
|
+
*/
|
|
191
|
+
async start(): Promise<void> {
|
|
192
|
+
// can not start not idel or stopped
|
|
193
|
+
if (
|
|
194
|
+
this.state.status !== ProjectStatus.IDLE &&
|
|
195
|
+
this.state.status !== ProjectStatus.STOPPED
|
|
196
|
+
) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this.validate();
|
|
201
|
+
|
|
202
|
+
this.initTasks();
|
|
203
|
+
this.initTriggers();
|
|
204
|
+
|
|
205
|
+
this.updateState({
|
|
206
|
+
status: ProjectStatus.STARTING,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const taskIds = this.getTasksStartOrder();
|
|
210
|
+
for (const taskId of taskIds) {
|
|
211
|
+
if (this.tasks.has(taskId)) {
|
|
212
|
+
const { instance, config } = this.tasks.get(taskId)!;
|
|
213
|
+
const { mode = ProjectTaskStartMode.IMMEDIATE, dependencies = [] } =
|
|
214
|
+
config;
|
|
215
|
+
if (
|
|
216
|
+
mode === ProjectTaskStartMode.IMMEDIATE &&
|
|
217
|
+
dependencies.length === 0
|
|
218
|
+
) {
|
|
219
|
+
instance.start();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.updateState({
|
|
225
|
+
status: ProjectStatus.STARTED,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Stop project
|
|
231
|
+
* @param reason
|
|
232
|
+
*/
|
|
233
|
+
async stop(reason: string = ''): Promise<void> {
|
|
234
|
+
// can not stop not started or starting
|
|
235
|
+
if (
|
|
236
|
+
this.state.status !== ProjectStatus.STARTED &&
|
|
237
|
+
this.state.status !== ProjectStatus.STARTING
|
|
238
|
+
) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.updateState({
|
|
243
|
+
status: ProjectStatus.STOPPING,
|
|
244
|
+
reason,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
this.cleanupTriggers();
|
|
248
|
+
|
|
249
|
+
const taskIds = this.getTasksStartOrder().reverse();
|
|
250
|
+
const promises = [];
|
|
251
|
+
for (const taskId of taskIds) {
|
|
252
|
+
if (this.tasks.has(taskId)) {
|
|
253
|
+
const { instance } = this.tasks.get(taskId)!;
|
|
254
|
+
const { status } = instance.getState();
|
|
255
|
+
if (status === TaskStatus.STARTED) {
|
|
256
|
+
promises.push(instance.stop('project-stop'));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
await Promise.allSettled(promises);
|
|
261
|
+
|
|
262
|
+
this.updateState({
|
|
263
|
+
status: ProjectStatus.STOPPED,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Extend project validation schema
|
|
269
|
+
* @param extensions
|
|
270
|
+
*/
|
|
271
|
+
extendValidationSchema(extensions: ProjectSchemaExtension): void {
|
|
272
|
+
this.schema = extendProjectSchema(this.schema, extensions);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Register action
|
|
277
|
+
* @param type
|
|
278
|
+
* @param processor
|
|
279
|
+
*/
|
|
280
|
+
registerAction(type: string, processor: RuniumActionProcessor): void {
|
|
281
|
+
if (this.actionProcessors.has(type)) {
|
|
282
|
+
throw new RuniumError(
|
|
283
|
+
`Action processor for type "${type}" already registered`,
|
|
284
|
+
ProjectErrorCode.ACTION_PROCESSOR_ALREADY_REGISTERED,
|
|
285
|
+
{
|
|
286
|
+
type,
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (typeof processor !== 'function') {
|
|
292
|
+
throw new RuniumError(
|
|
293
|
+
`Action processor for type "${type}" must be a function`,
|
|
294
|
+
ProjectErrorCode.ACTION_PROCESSOR_INCORRECT,
|
|
295
|
+
{
|
|
296
|
+
type,
|
|
297
|
+
}
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
this.actionProcessors.set(type, processor);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Register task
|
|
306
|
+
* @param type
|
|
307
|
+
* @param processor
|
|
308
|
+
*/
|
|
309
|
+
registerTask(
|
|
310
|
+
type: string,
|
|
311
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
312
|
+
processor: RuniumTaskConstructor<unknown, any>
|
|
313
|
+
): void {
|
|
314
|
+
if (this.taskProcessors.has(type)) {
|
|
315
|
+
throw new RuniumError(
|
|
316
|
+
`Task processor for type "${type}" already registered`,
|
|
317
|
+
ProjectErrorCode.TASK_PROCESSOR_ALREADY_REGISTERED,
|
|
318
|
+
{
|
|
319
|
+
type,
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!(processor.prototype instanceof RuniumTask)) {
|
|
325
|
+
throw new RuniumError(
|
|
326
|
+
`Task processor for type "${type}" must be a subclass of "RuniumTask"`,
|
|
327
|
+
ProjectErrorCode.TASK_PROCESSOR_INCORRECT,
|
|
328
|
+
{
|
|
329
|
+
type,
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
this.taskProcessors.set(type, processor);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Register trigger
|
|
339
|
+
* @param type
|
|
340
|
+
* @param processor
|
|
341
|
+
*/
|
|
342
|
+
registerTrigger(
|
|
343
|
+
type: string,
|
|
344
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
345
|
+
processor: RuniumTriggerConstructor<any>
|
|
346
|
+
): void {
|
|
347
|
+
if (this.triggerProcessors.has(type)) {
|
|
348
|
+
throw new RuniumError(
|
|
349
|
+
`Trigger processor for type "${type}" already registered`,
|
|
350
|
+
ProjectErrorCode.TRIGGER_PROCESSOR_ALREADY_REGISTERED,
|
|
351
|
+
{
|
|
352
|
+
type,
|
|
353
|
+
}
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!(processor.prototype instanceof RuniumTrigger)) {
|
|
358
|
+
throw new RuniumError(
|
|
359
|
+
`Trigger processor for type "${type}" must be a subclass of "RuniumTrigger"`,
|
|
360
|
+
ProjectErrorCode.TRIGGER_PROCESSOR_INCORRECT,
|
|
361
|
+
{
|
|
362
|
+
type,
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
this.triggerProcessors.set(type, processor);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Update project state
|
|
372
|
+
* @param state
|
|
373
|
+
*/
|
|
374
|
+
private updateState(state: Partial<ProjectState>): void {
|
|
375
|
+
this.state = { ...this.state, ...state, timestamp: Date.now() };
|
|
376
|
+
this.emit(ProjectEvent.STATE_CHANGE, this.getState());
|
|
377
|
+
this.emit(this.state.status, this.getState());
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Initialize tasks
|
|
382
|
+
*/
|
|
383
|
+
private initTasks(): void {
|
|
384
|
+
this.tasks.clear();
|
|
385
|
+
|
|
386
|
+
for (const taskConfig of this.config.tasks) {
|
|
387
|
+
const type = taskConfig.type || ProjectTaskType.DEFAULT;
|
|
388
|
+
const TaskConstructor = this.taskProcessors.get(type);
|
|
389
|
+
|
|
390
|
+
if (!TaskConstructor) {
|
|
391
|
+
throw new RuniumError(
|
|
392
|
+
`Task processor for type "${type}" not found`,
|
|
393
|
+
ProjectErrorCode.TASK_PROCESSOR_NOT_FOUND,
|
|
394
|
+
{
|
|
395
|
+
type,
|
|
396
|
+
}
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const task = new TaskConstructor(taskConfig.options);
|
|
401
|
+
|
|
402
|
+
task.on(TaskEvent.STATE_CHANGE, (state: TaskState) => {
|
|
403
|
+
this.emit(ProjectEvent.TASK_STATE_CHANGE, taskConfig.id, state);
|
|
404
|
+
this.onTaskStateChange(taskConfig.id, state);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
task.on(TaskEvent.STDOUT, (data: string) => {
|
|
408
|
+
this.emit(ProjectEvent.TASK_STDOUT, taskConfig.id, data);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
task.on(TaskEvent.STDERR, (data: string) => {
|
|
412
|
+
this.emit(ProjectEvent.TASK_STDERR, taskConfig.id, data);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
this.tasks.set(taskConfig.id, {
|
|
416
|
+
instance: task,
|
|
417
|
+
config: taskConfig,
|
|
418
|
+
dependencies: [...(taskConfig.dependencies || [])],
|
|
419
|
+
dependents: null,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Get tasks start order
|
|
426
|
+
*/
|
|
427
|
+
private getTasksStartOrder(): string[] {
|
|
428
|
+
const result: string[] = [];
|
|
429
|
+
const visited = new Set<string>();
|
|
430
|
+
|
|
431
|
+
const visit = (taskId: string): void => {
|
|
432
|
+
if (visited.has(taskId)) return;
|
|
433
|
+
|
|
434
|
+
visited.add(taskId);
|
|
435
|
+
|
|
436
|
+
const { dependencies = [] } = this.tasks.get(taskId) || {};
|
|
437
|
+
for (const dep of dependencies) {
|
|
438
|
+
if (this.tasks.has(dep.taskId)) {
|
|
439
|
+
visit(dep.taskId);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
result.push(taskId);
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
for (const taskId of this.tasks.keys()) {
|
|
447
|
+
visit(taskId);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return result;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* On task state change
|
|
455
|
+
* @param taskId
|
|
456
|
+
* @param state
|
|
457
|
+
*/
|
|
458
|
+
private onTaskStateChange(taskId: string, state: TaskState): void {
|
|
459
|
+
const { config, instance } = this.tasks.get(taskId)!;
|
|
460
|
+
|
|
461
|
+
// start dependent tasks
|
|
462
|
+
const dependents = this.getDependentTasks(taskId);
|
|
463
|
+
for (const dependentTaskId of dependents) {
|
|
464
|
+
if (this.isDependentTaskReady(dependentTaskId)) {
|
|
465
|
+
this.startTask(dependentTaskId);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// check restart
|
|
470
|
+
if (
|
|
471
|
+
state.status === TaskStatus.COMPLETED ||
|
|
472
|
+
state.status === TaskStatus.FAILED
|
|
473
|
+
) {
|
|
474
|
+
const { restart } = config;
|
|
475
|
+
if (restart && state.exitCode !== SILENT_EXIT_CODE) {
|
|
476
|
+
const { policy } = restart;
|
|
477
|
+
const needRestart =
|
|
478
|
+
policy === ProjectTaskRestartPolicyType.ALWAYS ||
|
|
479
|
+
(policy === ProjectTaskRestartPolicyType.ON_FAILURE &&
|
|
480
|
+
state.exitCode !== 0);
|
|
481
|
+
|
|
482
|
+
if (needRestart) {
|
|
483
|
+
const { maxRetries = Infinity, delay = 0 } =
|
|
484
|
+
restart as ProjectTaskRestartPolicyOnFailure;
|
|
485
|
+
if (state.iteration <= maxRetries) {
|
|
486
|
+
setTimeout(instance.restart.bind(instance), delay);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// process handlers
|
|
493
|
+
(config.handlers || []).forEach((handler: ProjectTaskHandler) => {
|
|
494
|
+
if (this.checkTaskStateCondition(handler.condition, state)) {
|
|
495
|
+
this.processAction(handler.action);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Check task state condition
|
|
502
|
+
* @param condition
|
|
503
|
+
* @param state
|
|
504
|
+
*/
|
|
505
|
+
private checkTaskStateCondition(
|
|
506
|
+
condition: ProjectTaskStateCondition,
|
|
507
|
+
state: RuniumTaskState
|
|
508
|
+
): boolean {
|
|
509
|
+
if (typeof condition === 'string') {
|
|
510
|
+
return expression.evaluate(condition, state) === true;
|
|
511
|
+
}
|
|
512
|
+
if (typeof condition === 'object') {
|
|
513
|
+
return Object.entries(condition).every(([key, value]) => {
|
|
514
|
+
return state[key as keyof RuniumTaskState] === value;
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
if (typeof condition === 'boolean') {
|
|
518
|
+
return condition;
|
|
519
|
+
}
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Start task
|
|
525
|
+
* @param taskId
|
|
526
|
+
*/
|
|
527
|
+
async startTask(taskId: string): Promise<void> {
|
|
528
|
+
if (this.tasks.has(taskId)) {
|
|
529
|
+
const { instance } = this.tasks.get(taskId)!;
|
|
530
|
+
const { status } = instance.getState();
|
|
531
|
+
if (
|
|
532
|
+
status !== TaskStatus.STARTED &&
|
|
533
|
+
status !== TaskStatus.STARTING &&
|
|
534
|
+
status !== TaskStatus.STOPPING
|
|
535
|
+
) {
|
|
536
|
+
this.emit(ProjectEvent.START_TASK, taskId);
|
|
537
|
+
return instance.start();
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Stop task
|
|
544
|
+
* @param taskId
|
|
545
|
+
*/
|
|
546
|
+
async stopTask(taskId: string): Promise<void> {
|
|
547
|
+
if (this.tasks.has(taskId)) {
|
|
548
|
+
const { instance } = this.tasks.get(taskId)!;
|
|
549
|
+
const { status } = instance.getState();
|
|
550
|
+
if (status === TaskStatus.STARTED) {
|
|
551
|
+
this.emit(ProjectEvent.STOP_TASK, taskId);
|
|
552
|
+
return instance.stop('action-stop');
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Restart task
|
|
559
|
+
* @param taskId
|
|
560
|
+
*/
|
|
561
|
+
async restartTask(taskId: string): Promise<void> {
|
|
562
|
+
if (this.tasks.has(taskId)) {
|
|
563
|
+
const { instance } = this.tasks.get(taskId)!;
|
|
564
|
+
this.emit(ProjectEvent.RESTART_TASK, taskId);
|
|
565
|
+
return instance.restart();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Get task state
|
|
571
|
+
* @param taskId
|
|
572
|
+
*/
|
|
573
|
+
getTaskState<T extends RuniumTaskState = RuniumTaskState>(
|
|
574
|
+
taskId: string
|
|
575
|
+
): T | null {
|
|
576
|
+
if (this.tasks.has(taskId)) {
|
|
577
|
+
const { instance } = this.tasks.get(taskId)!;
|
|
578
|
+
return instance.getState() as T;
|
|
579
|
+
}
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Check if dependent task is ready
|
|
585
|
+
* @param taskId
|
|
586
|
+
*/
|
|
587
|
+
private isDependentTaskReady(taskId: string): boolean {
|
|
588
|
+
if (this.tasks.has(taskId)) {
|
|
589
|
+
const { dependencies = [], config } = this.tasks.get(taskId)!;
|
|
590
|
+
|
|
591
|
+
if (config.mode === ProjectTaskStartMode.IGNORE) {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
for (const { taskId, condition } of dependencies) {
|
|
596
|
+
const { instance } = this.tasks.get(taskId)!;
|
|
597
|
+
if (!this.checkTaskStateCondition(condition, instance.getState())) {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Get dependent tasks
|
|
609
|
+
* @param taskId
|
|
610
|
+
*/
|
|
611
|
+
private getDependentTasks(taskId: string): string[] {
|
|
612
|
+
let result: string[] = [];
|
|
613
|
+
|
|
614
|
+
if (this.tasks.has(taskId)) {
|
|
615
|
+
const { dependents } = this.tasks.get(taskId)!;
|
|
616
|
+
if (dependents) {
|
|
617
|
+
result = dependents;
|
|
618
|
+
} else {
|
|
619
|
+
for (const [dependentTaskId, { dependencies }] of this.tasks) {
|
|
620
|
+
for (const dep of dependencies) {
|
|
621
|
+
if (dep.taskId === taskId) {
|
|
622
|
+
result.push(dependentTaskId);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
this.tasks.get(taskId)!.dependents = result;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return result;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Process action
|
|
634
|
+
* @param action
|
|
635
|
+
*/
|
|
636
|
+
processAction(action: ProjectAction): void {
|
|
637
|
+
this.emit(ProjectEvent.PROCESS_ACTION, action);
|
|
638
|
+
if (!isCustomAction(action)) {
|
|
639
|
+
switch (action.type) {
|
|
640
|
+
case ProjectActionType.START_TASK:
|
|
641
|
+
this.startTask(action.taskId);
|
|
642
|
+
break;
|
|
643
|
+
case ProjectActionType.RESTART_TASK:
|
|
644
|
+
this.restartTask(action.taskId);
|
|
645
|
+
break;
|
|
646
|
+
case ProjectActionType.STOP_TASK:
|
|
647
|
+
this.stopTask(action.taskId);
|
|
648
|
+
break;
|
|
649
|
+
case ProjectActionType.EMIT_EVENT:
|
|
650
|
+
this.emit(action.event);
|
|
651
|
+
break;
|
|
652
|
+
case ProjectActionType.STOP_PROJECT:
|
|
653
|
+
break;
|
|
654
|
+
case ProjectActionType.ENABLE_TRIGGER:
|
|
655
|
+
this.enableTrigger(action.triggerId);
|
|
656
|
+
break;
|
|
657
|
+
case ProjectActionType.DISABLE_TRIGGER:
|
|
658
|
+
this.disableTrigger(action.triggerId);
|
|
659
|
+
break;
|
|
660
|
+
default:
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
const processCustomAction = this.actionProcessors.get(action.type);
|
|
665
|
+
if (processCustomAction) {
|
|
666
|
+
processCustomAction(action.payload || {});
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Init triggers
|
|
673
|
+
*/
|
|
674
|
+
private initTriggers(): void {
|
|
675
|
+
const projectAccessible = {
|
|
676
|
+
processAction: this.processAction.bind(this),
|
|
677
|
+
on: this.on.bind(this),
|
|
678
|
+
off: this.off.bind(this),
|
|
679
|
+
} as RuniumTriggerProjectAccessible;
|
|
680
|
+
|
|
681
|
+
let trigger: RuniumTrigger<RuniumTriggerOptions> | null = null;
|
|
682
|
+
for (const triggerConfig of this.config.triggers || []) {
|
|
683
|
+
if (!isCustomTrigger(triggerConfig)) {
|
|
684
|
+
switch (triggerConfig.type) {
|
|
685
|
+
case ProjectTriggerType.EVENT:
|
|
686
|
+
trigger = new EventTrigger(triggerConfig, projectAccessible);
|
|
687
|
+
break;
|
|
688
|
+
case ProjectTriggerType.INTERVAL:
|
|
689
|
+
trigger = new IntervalTrigger(triggerConfig, projectAccessible);
|
|
690
|
+
break;
|
|
691
|
+
case ProjectTriggerType.TIMEOUT:
|
|
692
|
+
trigger = new TimeoutTrigger(triggerConfig, projectAccessible);
|
|
693
|
+
break;
|
|
694
|
+
default:
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
} else {
|
|
698
|
+
const TriggerConstructor = this.triggerProcessors.get(
|
|
699
|
+
triggerConfig.type
|
|
700
|
+
);
|
|
701
|
+
if (TriggerConstructor) {
|
|
702
|
+
trigger = new TriggerConstructor(triggerConfig, projectAccessible);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (trigger) {
|
|
706
|
+
this.triggers.set(triggerConfig.id, trigger);
|
|
707
|
+
if (triggerConfig.disabled !== true) {
|
|
708
|
+
trigger.enable();
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Cleanup triggers
|
|
716
|
+
*/
|
|
717
|
+
private cleanupTriggers(): void {
|
|
718
|
+
for (const id in this.triggers) {
|
|
719
|
+
const trigger = this.triggers.get(id);
|
|
720
|
+
trigger && trigger.disable();
|
|
721
|
+
}
|
|
722
|
+
this.triggers.clear();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Enable trigger
|
|
727
|
+
* @param triggerId
|
|
728
|
+
*/
|
|
729
|
+
enableTrigger(triggerId: string): void {
|
|
730
|
+
const trigger = this.triggers.get(triggerId);
|
|
731
|
+
if (trigger) {
|
|
732
|
+
this.emit(ProjectEvent.ENABLE_TRIGGER, triggerId);
|
|
733
|
+
trigger.enable();
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Disable trigger
|
|
739
|
+
* @param triggerId
|
|
740
|
+
*/
|
|
741
|
+
disableTrigger(triggerId: string): void {
|
|
742
|
+
const trigger = this.triggers.get(triggerId);
|
|
743
|
+
if (trigger) {
|
|
744
|
+
this.emit(ProjectEvent.DISABLE_TRIGGER, triggerId);
|
|
745
|
+
trigger.disable();
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|