@runium/core 0.0.9 → 0.1.1

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/src/project.ts DELETED
@@ -1,748 +0,0 @@
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
- }