Passed
Push — main ( d13a4d...a77d33 )
by Lorenzo
01:16 queued 14s
created

ExecutorImpl.setExecution   A

Complexity

Conditions 3

Size

Total Lines 13
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 13
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 3
crap 3
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
} 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 10
  static readonly PHASES = phases;
24
25 8
  tasks: Map<ExecutorPhase, Array<() => Promise<void> | void>> = new Map();
26
27 8
  eventEmitter = new EventEmitter<ExecutorEventMap>();
28
29 8
  phasePromises: Map<ExecutorPhase, Promise<Result<void>[]>> = new Map();
30
31 8
  started = false;
32
33 8
  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 54
    if (!this.tasks.has(phase)) {
44 50
      this.tasks.set(phase, []);
45
    }
46 54
    this.tasks.get(phase)?.push(task);
47
  }
48
49
  getExecutionPhase(phase: ExecutorPhase) {
50 41
    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 47
    this.started = true;
62 47
    return Object.entries(phases)
63 188
      .map(([idx, phase]) => [Number(idx), phase] as const)
64 141
      .sort(([a], [b]) => a - b)
65 188
      .map(([, phase]) => phase)
66
      .reduce((
67
        previous: {
68
          promise: Promise<Result<void>[]>, phase: ExecutorPhase
69
        },
70
        currentPhase,
71
        currentIndex,
72 188
      ) => ({
73
        promise: previous.promise.then(
74 188
          async () => {
75 188
            this.eventEmitter.emit(previous.phase, await previous.promise);
76 188
            const tasksToExecute = this.tasks.get(currentPhase) ?? [];
77 188
            logger.debug(`Executing phase ${currentPhase} with ${tasksToExecute.length} tasks`);
78 188
            const wrappedTasks = tasksToExecute.map(async (task) => {
79 50
              const result = wrap(task);
80 50
              return Promise.resolve(result);
81
            });
82
83 188
            const resultMap = async (result: Result<void>, index: number) => {
84 50
              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 50
              return result;
89
            };
90
91 188
            const phasePromise = Promise.all(wrappedTasks)
92 188
              .then((results) => Promise.all(results.map(resultMap)));
93
94 188
            this.phasePromises.set(currentPhase, phasePromise);
95 188
            if (currentIndex === Object.keys(phases).length - 1) {
96 47
              this.eventEmitter.emit(currentPhase, await phasePromise);
97
            }
98 188
            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 26
    setImmediate(() => {
114 26
      if (this.started) {
115 1
        return;
116
      }
117 25
      logger.debug('Starting lifecycle');
118 25
      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 56
    logger.debug('Stopping lifecycle');
130 56
    this.phasePromises.clear();
131 56
    this.tasks.clear();
132 56
    this.started = false;
133 56
    this.execution = null;
134 56
    this.eventEmitter.removeAllListeners();
135 56
    this.eventEmitter = new EventEmitter<ExecutorEventMap>();
136 56
    logger.debug('Lifecycle stopped');
137
  }
138
}
139
140
type ExecutorType = typeof ExecutorImpl & ExecutorImpl;
141 10
let instance: ExecutorImpl | null = null;
142
143
function getInstance() {
144 225
  if (!instance) {
145 8
    instance = new ExecutorImpl();
146
  }
147 225
  return instance;
148
}
149
150 10
export const Executor: ExecutorType = new Proxy(ExecutorImpl, {
151
  get(target, prop, receiver) {
152 225
    if (prop in target) {
153
      return Reflect.get(target, prop, receiver);
154
    }
155
156 225
    const inst = getInstance();
157 225
    const value = Reflect.get(inst, prop, inst);
158
159 225
    if (typeof value === 'function') {
160 199
      return value.bind(inst);
161
    }
162
163 26
    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