• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gregreindel / llm-exe / 19871373724

02 Dec 2025 07:44PM UTC coverage: 98.511%. First build
19871373724

Pull #125

github

web-flow
Merge 89ecb46b7 into f4b25ce06
Pull Request #125: Draft PR for release version v2.3.2

1035 of 1063 branches covered (97.37%)

Branch coverage included in aggregate %.

200 of 223 new or added lines in 33 files covered. (89.69%)

2405 of 2429 relevant lines covered (99.01%)

56.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.39
/src/executor/_base.ts
1
import {
2
  PlainObject,
3
  ExecutorMetadata,
4
  ListenerFunction,
5
  CoreExecutorHookInput,
6
  ExecutorExecutionMetadata,
7
  CoreExecutorExecuteOptions,
8
  BaseExecutorHooks,
9
} from "@/types";
10
import { pick } from "@/utils/modules/pick";
8✔
11
import { ensureInputIsObject } from "@/utils/modules/ensureInputIsObject";
8✔
12
import { uuid } from "@/utils/modules/uuid";
8✔
13
import { createMetadataState } from "./_metadata";
8✔
14
import { hookOnComplete, hookOnError, hookOnSuccess } from "@/utils/const";
8✔
15

16
/**
17
 * BaseExecutor
18
 * @template I - Input type.
19
 * @template O - Output type.
20
 * @template H - Hooks type.
21
 */
22
export abstract class BaseExecutor<
8✔
23
  I extends PlainObject,
24
  O = any,
25
  H extends BaseExecutorHooks = BaseExecutorHooks,
26
> {
27
  /**
28
   * @property id - internal id of the executor
29
   */
30
  readonly id;
71✔
31

32
  /**
33
   * @property type - type of executor
34
   */
35
  public type;
71✔
36

37
  /**
38
   * @property created - timestamp date created
39
   */
40
  readonly created;
71✔
41

42
  /**
43
   * @property name - name of executor
44
   */
45
  public name;
71✔
46

47
  /**
48
   * @property executions -
49
   */
50
  public executions;
71✔
51

52
  public traceId: string | null = null;
71✔
53
  /**
54
   * @property hooks - hooks to be ran during execution
55
   */
56
  public hooks: any;
71✔
57
  readonly allowedHooks: any[] = [hookOnComplete, hookOnError, hookOnSuccess];
71✔
58

59
  /**
60
   * @property maxHooksPerEvent - Maximum number of hooks allowed per event
61
   */
62
  readonly maxHooksPerEvent: number = 100;
71✔
63

64
  constructor(
65
    name: string,
66
    type: string,
67
    options?: CoreExecutorExecuteOptions<H>
68
  ) {
69
    this.id = uuid();
71✔
70
    this.type = type;
71✔
71
    this.name = name;
71✔
72
    this.created = new Date().getTime();
71✔
73
    this.executions = 0;
71✔
74
    this.hooks = {
71✔
75
      onSuccess: [],
76
      onError: [],
77
      onComplete: [],
78
    };
79

80
    if (options?.hooks) {
71✔
81
      this.setHooks(options.hooks);
26✔
82
    }
83
  }
84

85
  abstract handler(input: I, _options?: any): Promise<any>;
86

87
  /**
88
   *
89
   * Used to filter the input of the handler
90
   * @param _input
91
   * @returns original input formatted for handler
92
   */
93
  async getHandlerInput(
94
    _input: I,
95
    _metadata: ExecutorExecutionMetadata<I, any>,
96
    _options?: any
97
  ): Promise<any> {
98
    return ensureInputIsObject(_input);
12✔
99
  }
100

101
  /**
102
   *
103
   * Used to filter the output of the handler
104
   * @param _input
105
   * @returns output O
106
   */
107
  getHandlerOutput(
108
    out: any,
109
    _metadata: ExecutorExecutionMetadata<any, O>,
110
    _options?: any
111
  ): O {
112
    return out as O;
9✔
113
  }
114
  /**
115
   *
116
   * execute - Runs the executor
117
   * @param _input
118
   * @returns handler output
119
   */
120
  async execute(_input: I, _options?: any): Promise<O> {
121
    this.executions++;
30✔
122
    const _metadata = createMetadataState({
30✔
123
      start: new Date().getTime(),
124
      input: _input,
125
    });
126

127
    try {
30✔
128
      const input = await this.getHandlerInput(
30✔
129
        _input,
130
        _metadata.asPlainObject(),
131
        _options
132
      );
133

134
      _metadata.setItem({ handlerInput: input });
29✔
135

136
      let result = await this.handler(input, _options);
29✔
137

138
      _metadata.setItem({ handlerOutput: result });
26✔
139

140
      const output = this.getHandlerOutput(
26✔
141
        result,
142
        _metadata.asPlainObject(),
143
        _options
144
      );
145
      _metadata.setItem({ output });
26✔
146

147
      this.runHook("onSuccess", _metadata.asPlainObject());
26✔
148
      return output;
26✔
149
    } catch (error: any) {
150
      _metadata.setItem({ error, errorMessage: error.message });
4✔
151
      this.runHook("onError", _metadata.asPlainObject());
4✔
152
      throw error;
4✔
153
    } finally {
154
      _metadata.setItem({ end: new Date().getTime() });
30✔
155
      this.runHook("onComplete", _metadata.asPlainObject());
30✔
156
    }
157
  }
158

159
  metadata(): Record<string, any> {
160
    return {};
8✔
161
  }
162

163
  getMetadata(metadata?: Record<string, any>): ExecutorMetadata {
164
    return Object.assign({}, this.metadata(), {
26✔
165
      traceId: this.getTraceId(),
166
      id: this.id,
167
      type: this.type,
168
      name: this.name,
169
      created: this.created,
170
      executions: this.executions,
171
      metadata,
172
    });
173
  }
174

175
  runHook(hook: keyof H, _metadata: ExecutorExecutionMetadata) {
176
    /* istanbul ignore next */
177
    const { [hook]: hooks = [] } = pick(this.hooks, this.allowedHooks);
178
    for (const hookFn of [...hooks] /** clone hooks */) {
60✔
179
      if (typeof hookFn === "function") {
4✔
180
        try {
4✔
181
          hookFn(_metadata, this.getMetadata());
4✔
182
        } catch (error) {
183
          /** TODO: We should call an error handler */
184
        }
185
      }
186
    }
187
  }
188
  setHooks(hooks: CoreExecutorHookInput<H> = {}) {
1✔
189
    const hookKeys = Object.keys(hooks) as (keyof typeof hooks)[];
250✔
190
    for (const hookKey of hookKeys.filter((k) =>
250✔
191
      this.allowedHooks.includes(k)
235✔
192
    )) {
193
      const hookInput = hooks[hookKey];
232✔
194
      if (hookInput) {
232✔
195
        const _hooks = Array.isArray(hookInput) ? hookInput : [hookInput];
232✔
196
        for (const hook of _hooks) {
232✔
197
          if (
232✔
198
            hook &&
693✔
199
            typeof hook === "function" &&
200
            !this.hooks[hookKey].find((h: ListenerFunction) => h === hook)
10,011✔
201
          ) {
202
            // Enforce hook limit to prevent unbounded memory growth
203
            if (this.hooks[hookKey].length >= this.maxHooksPerEvent) {
220✔
204
              throw new Error(
1✔
205
                `Maximum number of hooks (${this.maxHooksPerEvent}) reached for event "${String(hookKey)}". ` +
206
                  `Consider removing unused hooks or increasing the limit.`
207
              );
208
            }
209
            this.hooks[hookKey].push(hook);
219✔
210
          }
211
        }
212
      }
213
    }
214
    return this;
249✔
215
  }
216

217
  removeHook(eventName: keyof H, fn: ListenerFunction) {
218
    if (typeof fn !== "function") return this;
6✔
219
    const lis = this.hooks[eventName];
5✔
220
    if (!lis) return this;
5✔
221
    for (let i = lis.length; i >= 0; i--) {
4✔
222
      if (lis[i] === fn) {
8✔
223
        lis.splice(i, 1);
4✔
224
        break;
4✔
225
      }
226
    }
227
    return this;
4✔
228
  }
229

230
  on(eventName: keyof H, fn: ListenerFunction) {
231
    return this.setHooks({ [eventName]: fn } as CoreExecutorHookInput<H>);
221✔
232
  }
233

234
  off(eventName: keyof H, fn: ListenerFunction) {
235
    return this.removeHook(eventName, fn);
3✔
236
  }
237

238
  once(eventName: keyof H, fn: ListenerFunction) {
239
    if (typeof fn !== "function") return this;
104✔
240

241
    // Enforce hook limit to prevent unbounded memory growth
242
    if (
103✔
243
      this.hooks[eventName] &&
206✔
244
      this.hooks[eventName].length >= this.maxHooksPerEvent
245
    ) {
246
      throw new Error(
1✔
247
        `Maximum number of hooks (${this.maxHooksPerEvent}) reached for event "${String(eventName)}". ` +
248
          `Consider removing unused hooks or increasing the limit.`
249
      );
250
    }
251

252
    // Wrapper that removes itself after first execution
253
    const onceWrapper: ListenerFunction = (...args: any[]) => {
102✔
254
      fn(...args);
2✔
255
      this.off(eventName, onceWrapper);
2✔
256
    };
257

258
    if (!this.hooks[eventName]) {
102!
NEW
259
      this.hooks[eventName] = [];
×
260
    }
261
    this.hooks[eventName].push(onceWrapper);
102✔
262
    return this;
102✔
263
  }
264
  withTraceId(traceId: string) {
265
    this.traceId = traceId;
2✔
266
    return this;
2✔
267
  }
268
  getTraceId() {
269
    return this.traceId;
9✔
270
  }
271

272
  /**
273
   * Clear all hooks for a specific event or all events
274
   * Useful for preventing memory leaks in long-running processes
275
   * @param eventName - The event name to clear hooks for
276
   */
277
  clearHooks(eventName?: keyof H) {
278
    if (eventName) {
4✔
279
      if (this.hooks[eventName]) {
3✔
280
        this.hooks[eventName] = [];
2✔
281
      }
282
    } else {
283
      // Clear all hooks across all allowed events
284
      for (const key of this.allowedHooks) {
1✔
285
        if (this.hooks[key]) {
3✔
286
          this.hooks[key] = [];
3✔
287
        }
288
      }
289
    }
290
    return this;
4✔
291
  }
292

293
  /**
294
   * Get the count of hooks for monitoring memory usage
295
   * @param eventName - Optional event name to get count for
296
   * @returns Hook count for specific event or all events
297
   */
298
  getHookCount(eventName?: keyof H): number | Record<string, number> {
299
    if (eventName) {
14✔
300
      return this.hooks[eventName]?.length || 0;
11✔
301
    }
302

303
    // Return counts for all events
304
    const counts: Record<string, number> = {};
3✔
305
    for (const key of this.allowedHooks) {
3✔
306
      counts[key] = this.hooks[key]?.length || 0;
9✔
307
    }
308
    return counts;
3✔
309
  }
310
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc