hookified 1.15.1 → 2.0.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.
@@ -1,15 +1,15 @@
1
1
  // src/eventified.ts
2
+ var ERROR_EVENT = "error";
2
3
  var Eventified = class {
3
4
  _eventListeners;
4
5
  _maxListeners;
5
- _logger;
6
+ _eventLogger;
6
7
  _throwOnEmitError = false;
7
- _throwOnEmptyListeners = false;
8
- _errorEvent = "error";
8
+ _throwOnEmptyListeners = true;
9
9
  constructor(options) {
10
10
  this._eventListeners = /* @__PURE__ */ new Map();
11
- this._maxListeners = 100;
12
- this._logger = options?.logger;
11
+ this._maxListeners = 0;
12
+ this._eventLogger = options?.eventLogger;
13
13
  if (options?.throwOnEmitError !== void 0) {
14
14
  this._throwOnEmitError = options.throwOnEmitError;
15
15
  }
@@ -18,18 +18,18 @@ var Eventified = class {
18
18
  }
19
19
  }
20
20
  /**
21
- * Gets the logger
21
+ * Gets the event logger
22
22
  * @returns {Logger}
23
23
  */
24
- get logger() {
25
- return this._logger;
24
+ get eventLogger() {
25
+ return this._eventLogger;
26
26
  }
27
27
  /**
28
- * Sets the logger
29
- * @param {Logger} logger
28
+ * Sets the event logger
29
+ * @param {Logger} eventLogger
30
30
  */
31
- set logger(logger) {
32
- this._logger = logger;
31
+ set eventLogger(eventLogger) {
32
+ this._eventLogger = eventLogger;
33
33
  }
34
34
  /**
35
35
  * Gets whether an error should be thrown when an emit throws an error. Default is false and only emits an error event.
@@ -80,10 +80,17 @@ var Eventified = class {
80
80
  */
81
81
  listenerCount(eventName) {
82
82
  if (eventName === void 0) {
83
- return this.getAllListeners().length;
83
+ let count = 0;
84
+ for (const entry2 of this._eventListeners.values()) {
85
+ count += typeof entry2 === "function" ? 1 : entry2.length;
86
+ }
87
+ return count;
88
+ }
89
+ const entry = this._eventListeners.get(eventName);
90
+ if (entry === void 0) {
91
+ return 0;
84
92
  }
85
- const listeners = this._eventListeners.get(eventName);
86
- return listeners ? listeners.length : 0;
93
+ return typeof entry === "function" ? 1 : entry.length;
87
94
  }
88
95
  /**
89
96
  * Gets an array of event names
@@ -101,7 +108,11 @@ var Eventified = class {
101
108
  if (event === void 0) {
102
109
  return this.getAllListeners();
103
110
  }
104
- return this._eventListeners.get(event) ?? [];
111
+ const entry = this._eventListeners.get(event);
112
+ if (entry === void 0) {
113
+ return [];
114
+ }
115
+ return typeof entry === "function" ? [entry] : entry;
105
116
  }
106
117
  /**
107
118
  * Prepends a listener to the beginning of the listeners array for the specified event
@@ -110,9 +121,14 @@ var Eventified = class {
110
121
  * @returns {IEventEmitter} returns the instance of the class for chaining
111
122
  */
112
123
  prependListener(eventName, listener) {
113
- const listeners = this._eventListeners.get(eventName) ?? [];
114
- listeners.unshift(listener);
115
- this._eventListeners.set(eventName, listeners);
124
+ const existing = this._eventListeners.get(eventName);
125
+ if (existing === void 0) {
126
+ this._eventListeners.set(eventName, listener);
127
+ } else if (typeof existing === "function") {
128
+ this._eventListeners.set(eventName, [listener, existing]);
129
+ } else {
130
+ existing.unshift(listener);
131
+ }
116
132
  return this;
117
133
  }
118
134
  /**
@@ -153,17 +169,26 @@ var Eventified = class {
153
169
  * @returns {IEventEmitter} returns the instance of the class for chaining
154
170
  */
155
171
  on(event, listener) {
156
- if (!this._eventListeners.has(event)) {
157
- this._eventListeners.set(event, []);
158
- }
159
- const listeners = this._eventListeners.get(event);
160
- if (listeners) {
161
- if (listeners.length >= this._maxListeners) {
172
+ const existing = this._eventListeners.get(event);
173
+ if (existing === void 0) {
174
+ this._eventListeners.set(event, listener);
175
+ return this;
176
+ }
177
+ if (typeof existing === "function") {
178
+ const arr = [existing, listener];
179
+ this._eventListeners.set(event, arr);
180
+ if (this._maxListeners > 0 && arr.length > this._maxListeners) {
162
181
  console.warn(
163
- `MaxListenersExceededWarning: Possible event memory leak detected. ${listeners.length + 1} ${event} listeners added. Use setMaxListeners() to increase limit.`
182
+ `MaxListenersExceededWarning: Possible event memory leak detected. ${arr.length} ${event} listeners added. Use setMaxListeners() to increase limit.`
183
+ );
184
+ }
185
+ } else {
186
+ existing.push(listener);
187
+ if (this._maxListeners > 0 && existing.length > this._maxListeners) {
188
+ console.warn(
189
+ `MaxListenersExceededWarning: Possible event memory leak detected. ${existing.length} ${event} listeners added. Use setMaxListeners() to increase limit.`
164
190
  );
165
191
  }
166
- listeners.push(listener);
167
192
  }
168
193
  return this;
169
194
  }
@@ -184,13 +209,25 @@ var Eventified = class {
184
209
  * @returns {IEventEmitter} returns the instance of the class for chaining
185
210
  */
186
211
  off(event, listener) {
187
- const listeners = this._eventListeners.get(event) ?? [];
188
- const index = listeners.indexOf(listener);
189
- if (index !== -1) {
190
- listeners.splice(index, 1);
212
+ const entry = this._eventListeners.get(event);
213
+ if (entry === void 0) {
214
+ return this;
191
215
  }
192
- if (listeners.length === 0) {
193
- this._eventListeners.delete(event);
216
+ if (typeof entry === "function") {
217
+ if (entry === listener) {
218
+ this._eventListeners.delete(event);
219
+ }
220
+ return this;
221
+ }
222
+ const index = entry.indexOf(listener);
223
+ if (index !== -1) {
224
+ if (entry.length === 2) {
225
+ this._eventListeners.set(event, entry[1 - index]);
226
+ } else if (entry.length === 1) {
227
+ this._eventListeners.delete(event);
228
+ } else {
229
+ entry.splice(index, 1);
230
+ }
194
231
  }
195
232
  return this;
196
233
  }
@@ -202,24 +239,40 @@ var Eventified = class {
202
239
  */
203
240
  emit(event, ...arguments_) {
204
241
  let result = false;
205
- const listeners = this._eventListeners.get(event);
206
- if (listeners && listeners.length > 0) {
207
- for (const listener of listeners) {
208
- listener(...arguments_);
209
- result = true;
242
+ const entry = this._eventListeners.get(event);
243
+ const argumentLength = arguments_.length;
244
+ if (entry !== void 0) {
245
+ if (typeof entry === "function") {
246
+ if (argumentLength === 1) {
247
+ entry(arguments_[0]);
248
+ } else if (argumentLength === 2) {
249
+ entry(arguments_[0], arguments_[1]);
250
+ } else {
251
+ entry(...arguments_);
252
+ }
253
+ } else {
254
+ const snapshot = [...entry];
255
+ for (let i = 0; i < snapshot.length; i++) {
256
+ if (argumentLength === 1) {
257
+ snapshot[i](arguments_[0]);
258
+ } else if (argumentLength === 2) {
259
+ snapshot[i](arguments_[0], arguments_[1]);
260
+ } else {
261
+ snapshot[i](...arguments_);
262
+ }
263
+ }
210
264
  }
265
+ result = true;
266
+ }
267
+ if (this._eventLogger) {
268
+ this.sendToEventLogger(event, arguments_);
211
269
  }
212
- if (event === this._errorEvent) {
270
+ if (event === ERROR_EVENT && !result) {
213
271
  const error = arguments_[0] instanceof Error ? arguments_[0] : new Error(`${arguments_[0]}`);
214
- if (this._throwOnEmitError && !result) {
272
+ if (this._throwOnEmitError || this._throwOnEmptyListeners) {
215
273
  throw error;
216
- } else {
217
- if (this.listeners(this._errorEvent).length === 0 && this._throwOnEmptyListeners === true) {
218
- throw error;
219
- }
220
274
  }
221
275
  }
222
- this.sendLog(event, arguments_);
223
276
  return result;
224
277
  }
225
278
  /**
@@ -228,7 +281,11 @@ var Eventified = class {
228
281
  * @returns {EventListener[]} An array of listeners
229
282
  */
230
283
  listeners(event) {
231
- return this._eventListeners.get(event) ?? [];
284
+ const entry = this._eventListeners.get(event);
285
+ if (entry === void 0) {
286
+ return [];
287
+ }
288
+ return typeof entry === "function" ? [entry] : entry;
232
289
  }
233
290
  /**
234
291
  * Removes all listeners for a specific event. If no event is provided, it removes all listeners
@@ -249,21 +306,22 @@ var Eventified = class {
249
306
  * @returns {void}
250
307
  */
251
308
  setMaxListeners(n) {
252
- this._maxListeners = n;
253
- for (const listeners of this._eventListeners.values()) {
254
- if (listeners.length > n) {
255
- listeners.splice(n);
256
- }
257
- }
309
+ this._maxListeners = n < 0 ? 0 : n;
258
310
  }
259
311
  /**
260
312
  * Gets all listeners
261
313
  * @returns {EventListener[]} An array of listeners
262
314
  */
263
315
  getAllListeners() {
264
- let result = [];
265
- for (const listeners of this._eventListeners.values()) {
266
- result = [...result, ...listeners];
316
+ const result = [];
317
+ for (const entry of this._eventListeners.values()) {
318
+ if (typeof entry === "function") {
319
+ result.push(entry);
320
+ } else {
321
+ for (let i = 0; i < entry.length; i++) {
322
+ result.push(entry[i]);
323
+ }
324
+ }
267
325
  }
268
326
  return result;
269
327
  }
@@ -272,8 +330,8 @@ var Eventified = class {
272
330
  * @param {string | symbol} eventName - The event name that determines the log level
273
331
  * @param {unknown} data - The data to log
274
332
  */
275
- sendLog(eventName, data) {
276
- if (!this._logger) {
333
+ sendToEventLogger(eventName, data) {
334
+ if (!this._eventLogger) {
277
335
  return;
278
336
  }
279
337
  let message;
@@ -290,33 +348,101 @@ var Eventified = class {
290
348
  }
291
349
  switch (eventName) {
292
350
  case "error": {
293
- this._logger.error?.(message, { event: eventName, data });
351
+ this._eventLogger.error?.(message, { event: eventName, data });
294
352
  break;
295
353
  }
296
354
  case "warn": {
297
- this._logger.warn?.(message, { event: eventName, data });
355
+ this._eventLogger.warn?.(message, { event: eventName, data });
298
356
  break;
299
357
  }
300
358
  case "trace": {
301
- this._logger.trace?.(message, { event: eventName, data });
359
+ this._eventLogger.trace?.(message, { event: eventName, data });
302
360
  break;
303
361
  }
304
362
  case "debug": {
305
- this._logger.debug?.(message, { event: eventName, data });
363
+ this._eventLogger.debug?.(message, { event: eventName, data });
306
364
  break;
307
365
  }
308
366
  case "fatal": {
309
- this._logger.fatal?.(message, { event: eventName, data });
367
+ this._eventLogger.fatal?.(message, { event: eventName, data });
310
368
  break;
311
369
  }
312
370
  default: {
313
- this._logger.info?.(message, { event: eventName, data });
371
+ this._eventLogger.info?.(message, { event: eventName, data });
314
372
  break;
315
373
  }
316
374
  }
317
375
  }
318
376
  };
319
377
 
378
+ // src/hooks/hook.ts
379
+ var Hook = class {
380
+ id;
381
+ event;
382
+ handler;
383
+ /**
384
+ * Creates a new Hook instance
385
+ * @param {string} event - The event name for the hook
386
+ * @param {HookFn} handler - The handler function for the hook
387
+ * @param {string} [id] - Optional unique identifier for the hook
388
+ */
389
+ constructor(event, handler, id) {
390
+ this.id = id;
391
+ this.event = event;
392
+ this.handler = handler;
393
+ }
394
+ };
395
+
396
+ // src/hooks/waterfall-hook.ts
397
+ var WaterfallHook = class {
398
+ id;
399
+ event;
400
+ handler;
401
+ hooks;
402
+ _finalHandler;
403
+ /**
404
+ * Creates a new WaterfallHook instance
405
+ * @param {string} event - The event name for the hook
406
+ * @param {WaterfallHookFn} finalHandler - The final handler function that receives the transformed result
407
+ * @param {string} [id] - Optional unique identifier for the hook
408
+ */
409
+ constructor(event, finalHandler, id) {
410
+ this.id = id;
411
+ this.event = event;
412
+ this.hooks = [];
413
+ this._finalHandler = finalHandler;
414
+ this.handler = async (...arguments_) => {
415
+ const initialArgs = arguments_.length === 1 ? arguments_[0] : arguments_;
416
+ const results = [];
417
+ for (const hook of this.hooks) {
418
+ const result = await hook({ initialArgs, results: [...results] });
419
+ results.push({ hook, result });
420
+ }
421
+ await this._finalHandler({ initialArgs, results: [...results] });
422
+ };
423
+ }
424
+ /**
425
+ * Adds a hook function to the end of the waterfall chain
426
+ * @param {WaterfallHookFn} hook - The hook function to add
427
+ */
428
+ addHook(hook) {
429
+ this.hooks.push(hook);
430
+ }
431
+ /**
432
+ * Removes a specific hook function from the waterfall chain
433
+ * @param {WaterfallHookFn} hook - The hook function to remove
434
+ * @returns {boolean} true if the hook was found and removed
435
+ */
436
+ removeHook(hook) {
437
+ const index = this.hooks.indexOf(hook);
438
+ if (index !== -1) {
439
+ this.hooks.splice(index, 1);
440
+ return true;
441
+ }
442
+ return false;
443
+ }
444
+ };
445
+
320
446
  // src/index.ts
321
447
  var Hookified = class extends Eventified {
322
448
  _hooks;
@@ -324,9 +450,10 @@ var Hookified = class extends Eventified {
324
450
  _enforceBeforeAfter = false;
325
451
  _deprecatedHooks;
326
452
  _allowDeprecated = true;
453
+ _useHookClone = true;
327
454
  constructor(options) {
328
455
  super({
329
- logger: options?.logger,
456
+ eventLogger: options?.eventLogger,
330
457
  throwOnEmitError: options?.throwOnEmitError,
331
458
  throwOnEmptyListeners: options?.throwOnEmptyListeners
332
459
  });
@@ -334,8 +461,6 @@ var Hookified = class extends Eventified {
334
461
  this._deprecatedHooks = options?.deprecatedHooks ? new Map(options.deprecatedHooks) : /* @__PURE__ */ new Map();
335
462
  if (options?.throwOnHookError !== void 0) {
336
463
  this._throwOnHookError = options.throwOnHookError;
337
- } else if (options?.throwHookErrors !== void 0) {
338
- this._throwOnHookError = options.throwHookErrors;
339
464
  }
340
465
  if (options?.enforceBeforeAfter !== void 0) {
341
466
  this._enforceBeforeAfter = options.enforceBeforeAfter;
@@ -343,30 +468,17 @@ var Hookified = class extends Eventified {
343
468
  if (options?.allowDeprecated !== void 0) {
344
469
  this._allowDeprecated = options.allowDeprecated;
345
470
  }
471
+ if (options?.useHookClone !== void 0) {
472
+ this._useHookClone = options.useHookClone;
473
+ }
346
474
  }
347
475
  /**
348
476
  * Gets all hooks
349
- * @returns {Map<string, Hook[]>}
477
+ * @returns {Map<string, IHook[]>}
350
478
  */
351
479
  get hooks() {
352
480
  return this._hooks;
353
481
  }
354
- /**
355
- * Gets whether an error should be thrown when a hook throws an error. Default is false and only emits an error event.
356
- * @returns {boolean}
357
- * @deprecated - this will be deprecated in version 2. Please use throwOnHookError.
358
- */
359
- get throwHookErrors() {
360
- return this._throwOnHookError;
361
- }
362
- /**
363
- * Sets whether an error should be thrown when a hook throws an error. Default is false and only emits an error event.
364
- * @param {boolean} value
365
- * @deprecated - this will be deprecated in version 2. Please use throwOnHookError.
366
- */
367
- set throwHookErrors(value) {
368
- this._throwOnHookError = value;
369
- }
370
482
  /**
371
483
  * Gets whether an error should be thrown when a hook throws an error. Default is false and only emits an error event.
372
484
  * @returns {boolean}
@@ -425,157 +537,153 @@ var Hookified = class extends Eventified {
425
537
  this._allowDeprecated = value;
426
538
  }
427
539
  /**
428
- * Validates hook event name if enforceBeforeAfter is enabled
429
- * @param {string} event - The event name to validate
430
- * @throws {Error} If enforceBeforeAfter is true and event doesn't start with 'before' or 'after'
431
- */
432
- validateHookName(event) {
433
- if (this._enforceBeforeAfter) {
434
- const eventValue = event.trim().toLocaleLowerCase();
435
- if (!eventValue.startsWith("before") && !eventValue.startsWith("after")) {
436
- throw new Error(
437
- `Hook event "${event}" must start with "before" or "after" when enforceBeforeAfter is enabled`
438
- );
439
- }
440
- }
441
- }
442
- /**
443
- * Checks if a hook is deprecated and emits a warning if it is
444
- * @param {string} event - The event name to check
445
- * @returns {boolean} - Returns true if the hook should proceed, false if it should be blocked
540
+ * Gets whether hook objects are cloned before storing. Default is true.
541
+ * @returns {boolean}
446
542
  */
447
- checkDeprecatedHook(event) {
448
- if (this._deprecatedHooks.has(event)) {
449
- const message = this._deprecatedHooks.get(event);
450
- const warningMessage = `Hook "${event}" is deprecated${message ? `: ${message}` : ""}`;
451
- this.emit("warn", { hook: event, message: warningMessage });
452
- return this._allowDeprecated;
453
- }
454
- return true;
543
+ get useHookClone() {
544
+ return this._useHookClone;
455
545
  }
456
546
  /**
457
- * Adds a handler function for a specific event
458
- * @param {string} event
459
- * @param {Hook} handler - this can be async or sync
460
- * @returns {void}
547
+ * Sets whether hook objects are cloned before storing. Default is true.
548
+ * When false, the original IHook reference is stored directly.
549
+ * @param {boolean} value
461
550
  */
462
- onHook(event, handler) {
463
- this.onHookEntry({ event, handler });
551
+ set useHookClone(value) {
552
+ this._useHookClone = value;
464
553
  }
465
554
  /**
466
- * Adds a handler function for a specific event
467
- * @param {HookEntry} hookEntry
468
- * @returns {void}
555
+ * Adds a handler function for a specific event.
556
+ * If you prefer the legacy `(event, handler)` signature, use {@link addHook} instead.
557
+ * To register multiple hooks at once, use {@link onHooks}.
558
+ * @param {IHook} hook - the hook containing event name and handler
559
+ * @param {OnHookOptions} [options] - optional per-call options (e.g., useHookClone override, position)
560
+ * @returns {IHook | undefined} the stored hook, or undefined if blocked by deprecation
469
561
  */
470
- onHookEntry(hookEntry) {
471
- this.validateHookName(hookEntry.event);
472
- if (!this.checkDeprecatedHook(hookEntry.event)) {
473
- return;
562
+ onHook(hook, options) {
563
+ this.validateHookName(hook.event);
564
+ if (!this.checkDeprecatedHook(hook.event)) {
565
+ return void 0;
474
566
  }
475
- const eventHandlers = this._hooks.get(hookEntry.event);
567
+ const shouldClone = options?.useHookClone ?? this._useHookClone;
568
+ const entry = shouldClone ? { id: hook.id, event: hook.event, handler: hook.handler } : hook;
569
+ entry.id = entry.id ?? crypto.randomUUID();
570
+ const eventHandlers = this._hooks.get(hook.event);
476
571
  if (eventHandlers) {
477
- eventHandlers.push(hookEntry.handler);
572
+ const existingIndex = eventHandlers.findIndex((h) => h.id === entry.id);
573
+ if (existingIndex !== -1) {
574
+ eventHandlers[existingIndex] = entry;
575
+ } else {
576
+ const position = options?.position ?? "Bottom";
577
+ if (position === "Top") {
578
+ eventHandlers.unshift(entry);
579
+ } else if (position === "Bottom") {
580
+ eventHandlers.push(entry);
581
+ } else {
582
+ const index = Math.max(0, Math.min(position, eventHandlers.length));
583
+ eventHandlers.splice(index, 0, entry);
584
+ }
585
+ }
478
586
  } else {
479
- this._hooks.set(hookEntry.event, [hookEntry.handler]);
587
+ this._hooks.set(hook.event, [entry]);
480
588
  }
589
+ return entry;
481
590
  }
482
591
  /**
483
592
  * Alias for onHook. This is provided for compatibility with other libraries that use the `addHook` method.
484
- * @param {string} event
485
- * @param {Hook} handler - this can be async or sync
593
+ * @param {string} event - the event name
594
+ * @param {HookFn} handler - the handler function
486
595
  * @returns {void}
487
596
  */
488
597
  addHook(event, handler) {
489
- this.onHookEntry({ event, handler });
598
+ this.onHook({ event, handler });
490
599
  }
491
600
  /**
492
- * Adds a handler function for a specific event
493
- * @param {Array<HookEntry>} hooks
601
+ * Adds handler functions for specific events
602
+ * @param {Array<IHook>} hooks
603
+ * @param {OnHookOptions} [options] - optional per-call options (e.g., useHookClone override, position)
494
604
  * @returns {void}
495
605
  */
496
- onHooks(hooks) {
606
+ onHooks(hooks, options) {
497
607
  for (const hook of hooks) {
498
- this.onHook(hook.event, hook.handler);
608
+ this.onHook(hook, options);
499
609
  }
500
610
  }
501
611
  /**
502
- * Adds a handler function for a specific event that runs before all other handlers
503
- * @param {string} event
504
- * @param {Hook} handler - this can be async or sync
505
- * @returns {void}
612
+ * Adds a handler function for a specific event that runs before all other handlers.
613
+ * Equivalent to calling `onHook(hook, { position: "Top" })`.
614
+ * @param {IHook} hook - the hook containing event name and handler
615
+ * @param {PrependHookOptions} [options] - optional per-call options (e.g., useHookClone override)
616
+ * @returns {IHook | undefined} the stored hook, or undefined if blocked by deprecation
506
617
  */
507
- prependHook(event, handler) {
508
- this.validateHookName(event);
509
- if (!this.checkDeprecatedHook(event)) {
510
- return;
511
- }
512
- const eventHandlers = this._hooks.get(event);
513
- if (eventHandlers) {
514
- eventHandlers.unshift(handler);
515
- } else {
516
- this._hooks.set(event, [handler]);
517
- }
618
+ prependHook(hook, options) {
619
+ return this.onHook(hook, { ...options, position: "Top" });
518
620
  }
519
621
  /**
520
- * Adds a handler that only executes once for a specific event before all other handlers
521
- * @param event
522
- * @param handler
622
+ * Adds a handler that only executes once for a specific event before all other handlers.
623
+ * Equivalent to calling `onHook` with a self-removing wrapper and `{ position: "Top" }`.
624
+ * @param {IHook} hook - the hook containing event name and handler
625
+ * @param {PrependHookOptions} [options] - optional per-call options (e.g., useHookClone override)
626
+ * @returns {IHook | undefined} the stored hook, or undefined if blocked by deprecation
523
627
  */
524
- prependOnceHook(event, handler) {
525
- this.validateHookName(event);
526
- if (!this.checkDeprecatedHook(event)) {
527
- return;
528
- }
529
- const hook = async (...arguments_) => {
530
- this.removeHook(event, hook);
531
- return handler(...arguments_);
628
+ prependOnceHook(hook, options) {
629
+ const wrappedHandler = async (...arguments_) => {
630
+ this.removeHook({ event: hook.event, handler: wrappedHandler });
631
+ return hook.handler(...arguments_);
532
632
  };
533
- this.prependHook(event, hook);
633
+ return this.onHook(
634
+ { id: hook.id, event: hook.event, handler: wrappedHandler },
635
+ { ...options, position: "Top" }
636
+ );
534
637
  }
535
638
  /**
536
639
  * Adds a handler that only executes once for a specific event
537
- * @param event
538
- * @param handler
640
+ * @param {IHook} hook - the hook containing event name and handler
539
641
  */
540
- onceHook(event, handler) {
541
- this.validateHookName(event);
542
- if (!this.checkDeprecatedHook(event)) {
642
+ onceHook(hook) {
643
+ this.validateHookName(hook.event);
644
+ if (!this.checkDeprecatedHook(hook.event)) {
543
645
  return;
544
646
  }
545
- const hook = async (...arguments_) => {
546
- this.removeHook(event, hook);
547
- return handler(...arguments_);
647
+ const wrappedHandler = async (...arguments_) => {
648
+ this.removeHook({ event: hook.event, handler: wrappedHandler });
649
+ return hook.handler(...arguments_);
548
650
  };
549
- this.onHook(event, hook);
651
+ this.onHook({ id: hook.id, event: hook.event, handler: wrappedHandler });
550
652
  }
551
653
  /**
552
654
  * Removes a handler function for a specific event
553
- * @param {string} event
554
- * @param {Hook} handler
555
- * @returns {void}
655
+ * @param {IHook} hook - the hook containing event name and handler to remove
656
+ * @returns {IHook | undefined} the removed hook, or undefined if not found
556
657
  */
557
- removeHook(event, handler) {
558
- this.validateHookName(event);
559
- if (!this.checkDeprecatedHook(event)) {
560
- return;
561
- }
562
- const eventHandlers = this._hooks.get(event);
658
+ removeHook(hook) {
659
+ this.validateHookName(hook.event);
660
+ const eventHandlers = this._hooks.get(hook.event);
563
661
  if (eventHandlers) {
564
- const index = eventHandlers.indexOf(handler);
662
+ const index = eventHandlers.findIndex((h) => h.handler === hook.handler);
565
663
  if (index !== -1) {
566
664
  eventHandlers.splice(index, 1);
665
+ if (eventHandlers.length === 0) {
666
+ this._hooks.delete(hook.event);
667
+ }
668
+ return { event: hook.event, handler: hook.handler };
567
669
  }
568
670
  }
671
+ return void 0;
569
672
  }
570
673
  /**
571
- * Removes all handlers for a specific event
572
- * @param {Array<HookEntry>} hooks
573
- * @returns {void}
674
+ * Removes multiple hook handlers
675
+ * @param {Array<IHook>} hooks
676
+ * @returns {IHook[]} the hooks that were successfully removed
574
677
  */
575
678
  removeHooks(hooks) {
679
+ const removed = [];
576
680
  for (const hook of hooks) {
577
- this.removeHook(hook.event, hook.handler);
681
+ const result = this.removeHook(hook);
682
+ if (result) {
683
+ removed.push(result);
684
+ }
578
685
  }
686
+ return removed;
579
687
  }
580
688
  /**
581
689
  * Calls all handlers for a specific event
@@ -590,9 +698,9 @@ var Hookified = class extends Eventified {
590
698
  }
591
699
  const eventHandlers = this._hooks.get(event);
592
700
  if (eventHandlers) {
593
- for (const handler of eventHandlers) {
701
+ for (const hook of [...eventHandlers]) {
594
702
  try {
595
- await handler(...arguments_);
703
+ await hook.handler(...arguments_);
596
704
  } catch (error) {
597
705
  const message = `${event}: ${error.message}`;
598
706
  this.emit("error", new Error(message));
@@ -620,12 +728,12 @@ var Hookified = class extends Eventified {
620
728
  }
621
729
  const eventHandlers = this._hooks.get(event);
622
730
  if (eventHandlers) {
623
- for (const handler of eventHandlers) {
624
- if (handler.constructor.name === "AsyncFunction") {
731
+ for (const hook of [...eventHandlers]) {
732
+ if (hook.handler.constructor.name === "AsyncFunction") {
625
733
  continue;
626
734
  }
627
735
  try {
628
- handler(...arguments_);
736
+ hook.handler(...arguments_);
629
737
  } catch (error) {
630
738
  const message = `${event}: ${error.message}`;
631
739
  this.emit("error", new Error(message));
@@ -665,15 +773,54 @@ var Hookified = class extends Eventified {
665
773
  /**
666
774
  * Gets all hooks for a specific event
667
775
  * @param {string} event
668
- * @returns {Hook[]}
776
+ * @returns {IHook[]}
669
777
  */
670
778
  getHooks(event) {
671
779
  this.validateHookName(event);
672
- if (!this.checkDeprecatedHook(event)) {
673
- return void 0;
674
- }
675
780
  return this._hooks.get(event);
676
781
  }
782
+ /**
783
+ * Gets a specific hook by id, searching across all events
784
+ * @param {string} id - the hook id
785
+ * @returns {IHook | undefined} the hook if found, or undefined
786
+ */
787
+ getHook(id) {
788
+ for (const eventHandlers of this._hooks.values()) {
789
+ const found = eventHandlers.find((h) => h.id === id);
790
+ if (found) {
791
+ return found;
792
+ }
793
+ }
794
+ return void 0;
795
+ }
796
+ /**
797
+ * Removes one or more hooks by id, searching across all events
798
+ * @param {string | string[]} id - the hook id or array of hook ids to remove
799
+ * @returns {IHook | IHook[] | undefined} the removed hook(s), or undefined/empty array if not found
800
+ */
801
+ removeHookById(id) {
802
+ if (Array.isArray(id)) {
803
+ const removed = [];
804
+ for (const singleId of id) {
805
+ const result = this.removeHookById(singleId);
806
+ if (result && !Array.isArray(result)) {
807
+ removed.push(result);
808
+ }
809
+ }
810
+ return removed;
811
+ }
812
+ for (const [event, eventHandlers] of this._hooks.entries()) {
813
+ const index = eventHandlers.findIndex((h) => h.id === id);
814
+ if (index !== -1) {
815
+ const [removed] = eventHandlers.splice(index, 1);
816
+ if (eventHandlers.length === 0) {
817
+ this._hooks.delete(event);
818
+ }
819
+ return removed;
820
+ }
821
+ }
822
+ return void 0;
823
+ }
677
824
  /**
678
825
  * Removes all hooks
679
826
  * @returns {void}
@@ -681,9 +828,57 @@ var Hookified = class extends Eventified {
681
828
  clearHooks() {
682
829
  this._hooks.clear();
683
830
  }
831
+ /**
832
+ * Removes all hooks for a specific event and returns the removed hooks.
833
+ * @param {string} event - The event name to remove hooks for.
834
+ * @returns {IHook[]} the hooks that were removed
835
+ */
836
+ removeEventHooks(event) {
837
+ this.validateHookName(event);
838
+ const eventHandlers = this._hooks.get(event);
839
+ if (eventHandlers) {
840
+ const removed = [...eventHandlers];
841
+ this._hooks.delete(event);
842
+ return removed;
843
+ }
844
+ return [];
845
+ }
846
+ /**
847
+ * Validates hook event name if enforceBeforeAfter is enabled
848
+ * @param {string} event - The event name to validate
849
+ * @throws {Error} If enforceBeforeAfter is true and event doesn't start with 'before' or 'after'
850
+ */
851
+ validateHookName(event) {
852
+ if (this._enforceBeforeAfter) {
853
+ const eventValue = event.trim().toLocaleLowerCase();
854
+ if (!eventValue.startsWith("before") && !eventValue.startsWith("after")) {
855
+ throw new Error(
856
+ `Hook event "${event}" must start with "before" or "after" when enforceBeforeAfter is enabled`
857
+ );
858
+ }
859
+ }
860
+ }
861
+ /**
862
+ * Checks if a hook is deprecated and emits a warning if it is
863
+ * @param {string} event - The event name to check
864
+ * @returns {boolean} - Returns true if the hook should proceed, false if it should be blocked
865
+ */
866
+ checkDeprecatedHook(event) {
867
+ if (this._deprecatedHooks.has(event)) {
868
+ const message = this._deprecatedHooks.get(event);
869
+ const warningMessage = `Hook "${event}" is deprecated${message ? `: ${message}` : ""}`;
870
+ this.emit("warn", { hook: event, message: warningMessage });
871
+ return this._allowDeprecated;
872
+ }
873
+ return true;
874
+ }
684
875
  };
685
876
  export {
686
877
  Eventified,
687
- Hookified
878
+ Hook,
879
+ Hookified,
880
+ WaterfallHook
688
881
  };
882
+ /* v8 ignore start -- @preserve: single-element arrays are stored as functions */
883
+ /* v8 ignore next 3 -- @preserve: guarded by caller */
689
884
  /* v8 ignore next -- @preserve */