Test Failed
Pull Request — main (#34)
by Lorenzo
02:02
created

ExecutorImpl.execute   B

Complexity

Conditions 5

Size

Total Lines 49
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 5

Importance

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