Passed
Push — develop ( 753300...72aeea )
by Lorenzo
01:40 queued 13s
created

ExecutorImpl.beforeExit   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 3
ccs 1
cts 1
cp 1
rs 10
c 0
b 0
f 0
cc 1
crap 1
1 10
import EventEmitter from 'events';
2 10
import { logger } from '@/core';
3 10
import {
4
  isError, Result, wrap,
5
} from '@/core/errors';
6
7 10
const phases = {
8
  0: 'start',
9
  1: 'register',
10
  2: 'routing',
11
  3: 'init',
12
  4: 'exit',
13
} as const;
14
export type ExecutorPhase = typeof phases[keyof typeof phases];
15
16
type ExecutorEventMap = {
17
  [phase in ExecutorPhase]: [Result<void>[]];
18
} & {
19
  error: [Error];
20
  finished: [];
21
};
22
23
class ExecutorImpl {
24 10
  static readonly PHASES = phases;
25
26 9
  tasks: Map<ExecutorPhase, Array<() => Promise<void> | void>> = new Map();
27
28 9
  eventEmitter = new EventEmitter<ExecutorEventMap>();
29
30 9
  phasePromises: Map<ExecutorPhase, Promise<Result<void>[]>> = new Map();
31
32 9
  started = false;
33
34 9
  execution: Promise<Result<void>[]> | null = null;
35
36
37
38
  /**
39
 * Registers a task to be executed in a given phase.
40
 * @param task {() => Promise<void> | void} function to be executed, can return a Promise or void
41
 * @param phase {ExecutorPhase} phase in which the task should be executed
42
 */
43
  setExecution(phase: ExecutorPhase, task: () => Promise<void> | void) {
44 77
    let phaseTasks = this.tasks.get(phase);
45 77
    if (!phaseTasks) {
46 73
      phaseTasks = [];
47
    }
48 77
    phaseTasks.push(task);
49 77
    this.tasks.set(phase, phaseTasks);
50
  }
51
52
  getExecutionPhase(phase: ExecutorPhase) {
53 48
    return this.phasePromises.get(phase) ?? new Promise((resolve) => {
54 11
      this.eventEmitter.once(phase, resolve);
55
    });
56
  }
57
58
  /**
59
 * Executes all registered tasks in a defined sequence based on phases.
60
 * Tasks are sorted by their phase index and executed concurrently within each phase.
61
 * Debug logs are generated for each phase indicating the number of tasks being executed.
62
 */
63
  async execute(): Promise<Result<void, Error>[]> {
64 56
    this.started = true;
65 56
    return Object.entries(phases)
66 280
      .filter(([, phase]) => phase !== 'exit')
67 224
      .map(([idx, phase]) => [Number(idx), phase] as const)
68 168
      .sort(([a], [b]) => a - b)
69 224
      .map(([, phase]) => phase)
70
      .reduce((
71
        previous: {
72
          promise: Promise<Result<void>[]>, phase: ExecutorPhase
73
        },
74
        currentPhase,
75 224
      ) => ({
76
        promise: previous.promise.then(
77 224
          async () => {
78 224
            await previous.promise;
79 224
            return this.executePhase(currentPhase);
80
          },
81
        ),
82
        phase: currentPhase,
83
      }), { promise: Promise.resolve<Result<void, Error>[]>([]), phase: 'start' })
84
      .promise;
85
  }
86
87
  /**
88
   * Executes all tasks in a given phase
89
   * @param phase {ExecutorPhase}
90
   * @returns {Promise<Result<void>[]>}
91
   */
92
  private async executePhase(phase: ExecutorPhase) {
93 313
    const tasksToExecute = this.tasks.get(phase) ?? [];
94 313
    logger.debug(`Executing phase ${phase} with ${tasksToExecute.length} tasks`);
95 313
    const wrappedTasks = tasksToExecute.map(async (task) => {
96 84
      const result = wrap(task);
97 84
      return Promise.resolve(result);
98
    });
99
100 313
    const resultMap = async (result: Result<void>, index: number) => {
101 84
      if (isError(result)) {
102 6
        logger.error(`Task ${index} in phase ${phase} failed`, { cause: result.error });
103 6
        this.eventEmitter.emit('error', result.error);
104
      }
105 84
      return result;
106
    };
107
108 313
    const phasePromise = Promise.all(wrappedTasks)
109 313
      .then((results) => Promise.all(results.map(resultMap)));
110
111 313
    this.phasePromises.set(phase, phasePromise);
112 313
    return phasePromise
113
      .then((result) => {
114 313
        this.eventEmitter.emit(phase, result);
115 313
        return result;
116
      });;
117
  }
118
119
  private beforeExit() {
120 18
    this.stopLifecycle();
121
  }
122
123
  /**
124
 * Starts the lifecycle of the ExpressBeans application.
125
 * If there are tasks to execute, they are executed in the defined sequence.
126
 * If execution is already in progress, the function does nothing.
127
 * @returns {void}
128
 */
129
  startLifecycle(): void {
130 33
    setImmediate(() => {
131 33
      if (this.started) {
132 1
        return;
133
      }
134 32
      logger.debug('Starting lifecycle');
135 32
      process.on('beforeExit', this.beforeExit.bind(this));
136 32
      this.execution = this.execute();
137
    });
138
  }
139
140
  /**
141
 * Stops the lifecycle of the ExpressBeans application.
142
 * All tasks are cleared and the lifecycle is stopped.
143
 * USE ONLY IF YOU KNOW WHAT YOU ARE DOING
144
 * @returns {Promise<void>}
145
 */
146
  async stopLifecycle(): Promise<void> {
147 89
    logger.debug('Stopping lifecycle');
148 89
    await this.executePhase('exit').then(() => {
149 89
      this.phasePromises.clear();
150 89
      this.tasks.clear();
151 89
      this.started = false;
152 89
      this.execution = null;
153 89
      this.eventEmitter.removeAllListeners();
154 89
      this.eventEmitter = new EventEmitter<ExecutorEventMap>();
155 89
      logger.debug('Lifecycle stopped');
156 89
      process.removeListener('beforeExit', this.beforeExit.bind(this));
157
    });
158
  }
159
}
160
161
type ExecutorType = typeof ExecutorImpl & ExecutorImpl;
162 10
let instance: ExecutorImpl | null = null;
163
164
function getInstance() {
165 284
  if (!instance) {
166 9
    instance = new ExecutorImpl();
167
  }
168 284
  return instance;
169
}
170
171 10
export const Executor: ExecutorType = new Proxy(ExecutorImpl, {
172
  get(target, prop, receiver) {
173 285
    if (prop in target) {
174 1
      return Reflect.get(target, prop, receiver);
175
    }
176
177 284
    const inst = getInstance();
178 284
    const value = Reflect.get(inst, prop, inst);
179
180 284
    if (typeof value === 'function') {
181 253
      return value.bind(inst);
182
    }
183
184 31
    return value;
185
  },
186
}) as ExecutorType;
187