From 5740620d454298be30b8d70842d5f50e1b5500fb Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Sun, 4 Jan 2026 17:50:29 -0500 Subject: [PATCH 1/2] feat: adding in hookSync function --- README.md | 88 ++++++++++++ src/index.ts | 76 ++++++++++ test/index.test.ts | 335 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 499 insertions(+) diff --git a/README.md b/README.md index ccb750b..ee54aa7 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,10 @@ - [.callHook(eventName, ...args)](#callhookeventname-args) - [.beforeHook(eventName, ...args)](#beforehookeventname-args) - [.afterHook(eventName, ...args)](#afterhookeventname-args) + - [.hookSync(eventName, ...args)](#hooksync-eventname-args) + - [.callHookSync(eventName, ...args)](#callhooksync-eventname-args) + - [.beforeHookSync(eventName, ...args)](#beforehooksync-eventname-args) + - [.afterHookSync(eventName, ...args)](#afterhooksync-eventname-args) - [.hooks](#hooks) - [.getHooks(eventName)](#gethookseventname) - [.clearHooks(eventName)](#clearhookeventname) @@ -801,6 +805,90 @@ class MyClass extends Hookified { } ``` +## .hookSync(eventName, ...args) + +Run a hook event synchronously. Async handlers (functions declared with `async` keyword) are silently skipped and only synchronous handlers are executed. + +```javascript +import { Hookified } from 'hookified'; + +class MyClass extends Hookified { + constructor() { + super(); + } + + myMethodWithSyncHooks() { + let data = { some: 'data' }; + // Only synchronous handlers will execute + this.hookSync('before:myMethod', data); + + return data; + } +} + +const myClass = new MyClass(); + +// This sync handler will execute +myClass.onHook('before:myMethod', (data) => { + data.some = 'modified'; +}); + +// This async handler will be silently skipped +myClass.onHook('before:myMethod', async (data) => { + data.some = 'will not run'; +}); + +myClass.myMethodWithSyncHooks(); // Only sync handler runs +``` + +## .callHookSync(eventName, ...args) + +This is an alias for `.hookSync(eventName, ...args)` for backwards compatibility. + +## .beforeHookSync(eventName, ...args) + +This is a helper function that will prepend a hook name with `before:` and execute synchronously. Async handlers are silently skipped. + +```javascript +import { Hookified } from 'hookified'; + +class MyClass extends Hookified { + constructor() { + super(); + } + + myMethodWithSyncHooks() { + let data = { some: 'data' }; + // the event name will be `before:myMethod2` + this.beforeHookSync('myMethod2', data); + + return data; + } +} +``` + +## .afterHookSync(eventName, ...args) + +This is a helper function that will prepend a hook name with `after:` and execute synchronously. Async handlers are silently skipped. + +```javascript +import { Hookified } from 'hookified'; + +class MyClass extends Hookified { + constructor() { + super(); + } + + myMethodWithSyncHooks() { + let data = { some: 'data' }; + // the event name will be `after:myMethod2` + this.afterHookSync('myMethod2', data); + + return data; + } +} +``` + ## .hooks Get all hooks. diff --git a/src/index.ts b/src/index.ts index 6ba796c..66fad54 100644 --- a/src/index.ts +++ b/src/index.ts @@ -328,6 +328,44 @@ export class Hookified extends Eventified { } } + /** + * Calls all synchronous handlers for a specific event. + * Async handlers (declared with `async` keyword) are silently skipped. + * + * Note: The `hook` method is preferred as it executes both sync and async functions. + * Use `hookSync` only when you specifically need synchronous execution. + * @param {string} event + * @param {T[]} arguments_ + * @returns {void} + */ + public hookSync(event: string, ...arguments_: T[]): void { + this.validateHookName(event); + if (!this.checkDeprecatedHook(event)) { + return; + } + + const eventHandlers = this._hooks.get(event); + if (eventHandlers) { + for (const handler of eventHandlers) { + // Skip async functions silently + if (handler.constructor.name === "AsyncFunction") { + continue; + } + + try { + handler(...arguments_); + } catch (error) { + const message = `${event}: ${(error as Error).message}`; + this.emit("error", new Error(message)); + + if (this._throwOnHookError) { + throw new Error(message); + } + } + } + } + } + /** * Prepends the word `before` to your hook. Example is event is `test`, the before hook is `before:test`. * @param {string} event - The event name @@ -337,6 +375,18 @@ export class Hookified extends Eventified { await this.hook(`before:${event}`, ...arguments_); } + /** + * Prepends the word `before` to your hook and executes synchronously. + * Async handlers are silently skipped. + * + * Note: The `beforeHook` method is preferred as it executes both sync and async functions. + * @param {string} event - The event name + * @param {T[]} arguments_ - The arguments to pass to the hook + */ + public beforeHookSync(event: string, ...arguments_: T[]): void { + this.hookSync(`before:${event}`, ...arguments_); + } + /** * Prepends the word `after` to your hook. Example is event is `test`, the after hook is `after:test`. * @param {string} event - The event name @@ -346,6 +396,18 @@ export class Hookified extends Eventified { await this.hook(`after:${event}`, ...arguments_); } + /** + * Prepends the word `after` to your hook and executes synchronously. + * Async handlers are silently skipped. + * + * Note: The `afterHook` method is preferred as it executes both sync and async functions. + * @param {string} event - The event name + * @param {T[]} arguments_ - The arguments to pass to the hook + */ + public afterHookSync(event: string, ...arguments_: T[]): void { + this.hookSync(`after:${event}`, ...arguments_); + } + /** * Calls all handlers for a specific event. This is an alias for `hook` and is provided for * compatibility with other libraries that use the `callHook` method. @@ -357,6 +419,20 @@ export class Hookified extends Eventified { await this.hook(event, ...arguments_); } + /** + * Calls all synchronous handlers for a specific event. This is an alias for `hookSync` + * and is provided for compatibility with other libraries that use the `callHook` method. + * Async handlers are silently skipped. + * + * Note: The `callHook` method is preferred as it executes both sync and async functions. + * @param {string} event + * @param {T[]} arguments_ + * @returns {void} + */ + public callHookSync(event: string, ...arguments_: T[]): void { + this.hookSync(event, ...arguments_); + } + /** * Gets all hooks for a specific event * @param {string} event diff --git a/test/index.test.ts b/test/index.test.ts index 3fb56ae..e051756 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1678,4 +1678,339 @@ describe("Hookified", () => { expect(hookified.getHooks("oldHook")).toBeUndefined(); }); }); + + describe("hookSync", () => { + test("should execute synchronous handlers", () => { + const hookified = new Hookified(); + const data = { key: "value" }; + + const handler = (data: any) => { + data.key = "modified"; + }; + + hookified.onHook("event", handler); + hookified.hookSync("event", data); + expect(data.key).toBe("modified"); + }); + + test("should skip async handlers silently", () => { + const hookified = new Hookified(); + const executionOrder: string[] = []; + + const syncHandler = () => { + executionOrder.push("sync"); + }; + + const asyncHandler = async () => { + executionOrder.push("async"); + }; + + hookified.onHook("event", syncHandler); + hookified.onHook("event", asyncHandler); + hookified.onHook("event", syncHandler); + + hookified.hookSync("event"); + + // Only sync handlers should execute + expect(executionOrder).toEqual(["sync", "sync"]); + }); + + test("should execute handlers in order", () => { + const hookified = new Hookified(); + const executionOrder: number[] = []; + + hookified.onHook("event", () => executionOrder.push(1)); + hookified.onHook("event", () => executionOrder.push(2)); + hookified.onHook("event", () => executionOrder.push(3)); + + hookified.hookSync("event"); + + expect(executionOrder).toEqual([1, 2, 3]); + }); + + test("should emit error event on handler error", () => { + const hookified = new Hookified(); + let errorMessage: string | undefined; + + hookified.on("error", (error: Error) => { + errorMessage = error.message; + }); + + const handler = () => { + throw new Error("sync error"); + }; + + hookified.onHook("event", handler); + hookified.hookSync("event"); + + expect(errorMessage).toBe("event: sync error"); + }); + + test("should throw error when throwOnHookError is true", () => { + const hookified = new Hookified({ throwOnHookError: true }); + + const handler = () => { + throw new Error("sync error"); + }; + + hookified.onHook("event", handler); + + expect(() => hookified.hookSync("event")).toThrow("event: sync error"); + }); + + test("should validate hook name when enforceBeforeAfter is true", () => { + const hookified = new Hookified({ enforceBeforeAfter: true }); + + expect(() => hookified.hookSync("invalidName")).toThrow( + 'Hook event "invalidName" must start with "before" or "after" when enforceBeforeAfter is enabled', + ); + }); + + test("should respect deprecated hooks setting", () => { + const deprecatedHooks = new Map([["oldHook", "Use newHook instead"]]); + const hookified = new Hookified({ + deprecatedHooks, + allowDeprecated: false, + }); + const handler = vi.fn(); + + hookified.onHook("oldHook", handler); + hookified.hookSync("oldHook"); + + expect(handler).not.toHaveBeenCalled(); + }); + + test("should pass multiple arguments to handlers", () => { + const hookified = new Hookified(); + const data1 = { key: "value1" }; + const data2 = { key: "value2" }; + let capturedArgs: any[] = []; + + const handler = (...args: any[]) => { + capturedArgs = args; + }; + + hookified.onHook("event", handler); + hookified.hookSync("event", data1, data2); + + expect(capturedArgs).toEqual([data1, data2]); + }); + + test("should handle no handlers gracefully", () => { + const hookified = new Hookified(); + + // Should not throw + expect(() => hookified.hookSync("nonexistent")).not.toThrow(); + }); + + test("should detect async function correctly", () => { + const hookified = new Hookified(); + const results: string[] = []; + + // Regular function - should execute + hookified.onHook("event", () => { + results.push("regular"); + }); + + // Arrow function - should execute + hookified.onHook("event", () => { + results.push("arrow"); + }); + + // Async function - should skip + hookified.onHook("event", async () => { + results.push("async-regular"); + }); + + // Async arrow function - should skip + hookified.onHook("event", async () => { + results.push("async-arrow"); + }); + + hookified.hookSync("event"); + + expect(results).toEqual(["regular", "arrow"]); + }); + + test("should handle function that returns Promise synchronously", () => { + const hookified = new Hookified(); + const results: string[] = []; + + // This is a sync function that returns a Promise + // hookSync will call it, but won't await the Promise + const promiseReturningHandler = () => { + results.push("sync-part"); + return Promise.resolve().then(() => results.push("async-part")); + }; + + hookified.onHook("event", promiseReturningHandler); + hookified.hookSync("event"); + + // Only the sync part executes immediately + expect(results).toEqual(["sync-part"]); + }); + }); + + describe("beforeHookSync", () => { + test("should call before:event hooks synchronously", () => { + const hookified = new Hookified(); + const handlerData: string[] = []; + + const handler = () => { + handlerData.push("before"); + }; + + hookified.onHook("before:test", handler); + hookified.beforeHookSync("test"); + + expect(handlerData).toEqual(["before"]); + }); + + test("should skip async handlers", () => { + const hookified = new Hookified(); + const handlerData: string[] = []; + + hookified.onHook("before:test", () => handlerData.push("sync")); + hookified.onHook("before:test", async () => handlerData.push("async")); + + hookified.beforeHookSync("test"); + + expect(handlerData).toEqual(["sync"]); + }); + + test("should pass arguments to handlers", () => { + const hookified = new Hookified(); + const data = { key: "value" }; + + const handler = (data: any) => { + data.key = "modified"; + }; + + hookified.onHook("before:test", handler); + hookified.beforeHookSync("test", data); + + expect(data.key).toBe("modified"); + }); + }); + + describe("afterHookSync", () => { + test("should call after:event hooks synchronously", () => { + const hookified = new Hookified(); + const handlerData: string[] = []; + + const handler = () => { + handlerData.push("after"); + }; + + hookified.onHook("after:test", handler); + hookified.afterHookSync("test"); + + expect(handlerData).toEqual(["after"]); + }); + + test("should skip async handlers", () => { + const hookified = new Hookified(); + const handlerData: string[] = []; + + hookified.onHook("after:test", () => handlerData.push("sync")); + hookified.onHook("after:test", async () => handlerData.push("async")); + + hookified.afterHookSync("test"); + + expect(handlerData).toEqual(["sync"]); + }); + + test("should pass arguments to handlers", () => { + const hookified = new Hookified(); + const data = { key: "value" }; + + const handler = (data: any) => { + data.key = "modified"; + }; + + hookified.onHook("after:test", handler); + hookified.afterHookSync("test", data); + + expect(data.key).toBe("modified"); + }); + }); + + describe("callHookSync", () => { + test("should be an alias for hookSync", () => { + const hookified = new Hookified(); + const data = { key: "value" }; + + const handler = (data: any) => { + data.key = "modified"; + }; + + hookified.onHook("event", handler); + hookified.callHookSync("event", data); + + expect(data.key).toBe("modified"); + }); + + test("should skip async handlers", () => { + const hookified = new Hookified(); + const executionOrder: string[] = []; + + hookified.onHook("event", () => executionOrder.push("sync")); + hookified.onHook("event", async () => executionOrder.push("async")); + + hookified.callHookSync("event"); + + expect(executionOrder).toEqual(["sync"]); + }); + }); + + describe("hookSync and async hook integration", () => { + test("should work with before and after sync hooks together", () => { + const hookified = new Hookified(); + const executionOrder: string[] = []; + + const beforeHandler = () => { + executionOrder.push("before"); + }; + + const mainHandler = () => { + executionOrder.push("main"); + }; + + const afterHandler = () => { + executionOrder.push("after"); + }; + + hookified.onHook("before:operation", beforeHandler); + hookified.onHook("operation", mainHandler); + hookified.onHook("after:operation", afterHandler); + + hookified.beforeHookSync("operation"); + hookified.hookSync("operation"); + hookified.afterHookSync("operation"); + + expect(executionOrder).toEqual(["before", "main", "after"]); + }); + + test("should maintain data integrity across sync hooks", () => { + const hookified = new Hookified(); + const data = { value: 0 }; + + const beforeHandler = (data: any) => { + data.value = 10; + }; + + const afterHandler = (data: any) => { + data.value = data.value * 2; + }; + + hookified.onHook("before:transform", beforeHandler); + hookified.onHook("after:transform", afterHandler); + + hookified.beforeHookSync("transform", data); + expect(data.value).toBe(10); + + hookified.afterHookSync("transform", data); + expect(data.value).toBe(20); + }); + }); }); From ae4de3c2cd1bdadc4c036df49e88b9a41b3c6767 Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Sun, 4 Jan 2026 23:25:48 -0500 Subject: [PATCH 2/2] adding in hookSync --- README.md | 53 +-------------- src/index.ts | 38 ----------- test/index.test.ts | 163 --------------------------------------------- 3 files changed, 2 insertions(+), 252 deletions(-) diff --git a/README.md b/README.md index ee54aa7..5f45916 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,6 @@ - [.beforeHook(eventName, ...args)](#beforehookeventname-args) - [.afterHook(eventName, ...args)](#afterhookeventname-args) - [.hookSync(eventName, ...args)](#hooksync-eventname-args) - - [.callHookSync(eventName, ...args)](#callhooksync-eventname-args) - - [.beforeHookSync(eventName, ...args)](#beforehooksync-eventname-args) - - [.afterHookSync(eventName, ...args)](#afterhooksync-eventname-args) - [.hooks](#hooks) - [.getHooks(eventName)](#gethookseventname) - [.clearHooks(eventName)](#clearhookeventname) @@ -809,6 +806,8 @@ class MyClass extends Hookified { Run a hook event synchronously. Async handlers (functions declared with `async` keyword) are silently skipped and only synchronous handlers are executed. +> **Note:** The `.hook()` method is preferred as it executes both sync and async functions. Use `.hookSync()` only when you specifically need synchronous execution. + ```javascript import { Hookified } from 'hookified'; @@ -841,54 +840,6 @@ myClass.onHook('before:myMethod', async (data) => { myClass.myMethodWithSyncHooks(); // Only sync handler runs ``` -## .callHookSync(eventName, ...args) - -This is an alias for `.hookSync(eventName, ...args)` for backwards compatibility. - -## .beforeHookSync(eventName, ...args) - -This is a helper function that will prepend a hook name with `before:` and execute synchronously. Async handlers are silently skipped. - -```javascript -import { Hookified } from 'hookified'; - -class MyClass extends Hookified { - constructor() { - super(); - } - - myMethodWithSyncHooks() { - let data = { some: 'data' }; - // the event name will be `before:myMethod2` - this.beforeHookSync('myMethod2', data); - - return data; - } -} -``` - -## .afterHookSync(eventName, ...args) - -This is a helper function that will prepend a hook name with `after:` and execute synchronously. Async handlers are silently skipped. - -```javascript -import { Hookified } from 'hookified'; - -class MyClass extends Hookified { - constructor() { - super(); - } - - myMethodWithSyncHooks() { - let data = { some: 'data' }; - // the event name will be `after:myMethod2` - this.afterHookSync('myMethod2', data); - - return data; - } -} -``` - ## .hooks Get all hooks. diff --git a/src/index.ts b/src/index.ts index 66fad54..87c4690 100644 --- a/src/index.ts +++ b/src/index.ts @@ -375,18 +375,6 @@ export class Hookified extends Eventified { await this.hook(`before:${event}`, ...arguments_); } - /** - * Prepends the word `before` to your hook and executes synchronously. - * Async handlers are silently skipped. - * - * Note: The `beforeHook` method is preferred as it executes both sync and async functions. - * @param {string} event - The event name - * @param {T[]} arguments_ - The arguments to pass to the hook - */ - public beforeHookSync(event: string, ...arguments_: T[]): void { - this.hookSync(`before:${event}`, ...arguments_); - } - /** * Prepends the word `after` to your hook. Example is event is `test`, the after hook is `after:test`. * @param {string} event - The event name @@ -396,18 +384,6 @@ export class Hookified extends Eventified { await this.hook(`after:${event}`, ...arguments_); } - /** - * Prepends the word `after` to your hook and executes synchronously. - * Async handlers are silently skipped. - * - * Note: The `afterHook` method is preferred as it executes both sync and async functions. - * @param {string} event - The event name - * @param {T[]} arguments_ - The arguments to pass to the hook - */ - public afterHookSync(event: string, ...arguments_: T[]): void { - this.hookSync(`after:${event}`, ...arguments_); - } - /** * Calls all handlers for a specific event. This is an alias for `hook` and is provided for * compatibility with other libraries that use the `callHook` method. @@ -419,20 +395,6 @@ export class Hookified extends Eventified { await this.hook(event, ...arguments_); } - /** - * Calls all synchronous handlers for a specific event. This is an alias for `hookSync` - * and is provided for compatibility with other libraries that use the `callHook` method. - * Async handlers are silently skipped. - * - * Note: The `callHook` method is preferred as it executes both sync and async functions. - * @param {string} event - * @param {T[]} arguments_ - * @returns {void} - */ - public callHookSync(event: string, ...arguments_: T[]): void { - this.hookSync(event, ...arguments_); - } - /** * Gets all hooks for a specific event * @param {string} event diff --git a/test/index.test.ts b/test/index.test.ts index e051756..95d39bc 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1850,167 +1850,4 @@ describe("Hookified", () => { expect(results).toEqual(["sync-part"]); }); }); - - describe("beforeHookSync", () => { - test("should call before:event hooks synchronously", () => { - const hookified = new Hookified(); - const handlerData: string[] = []; - - const handler = () => { - handlerData.push("before"); - }; - - hookified.onHook("before:test", handler); - hookified.beforeHookSync("test"); - - expect(handlerData).toEqual(["before"]); - }); - - test("should skip async handlers", () => { - const hookified = new Hookified(); - const handlerData: string[] = []; - - hookified.onHook("before:test", () => handlerData.push("sync")); - hookified.onHook("before:test", async () => handlerData.push("async")); - - hookified.beforeHookSync("test"); - - expect(handlerData).toEqual(["sync"]); - }); - - test("should pass arguments to handlers", () => { - const hookified = new Hookified(); - const data = { key: "value" }; - - const handler = (data: any) => { - data.key = "modified"; - }; - - hookified.onHook("before:test", handler); - hookified.beforeHookSync("test", data); - - expect(data.key).toBe("modified"); - }); - }); - - describe("afterHookSync", () => { - test("should call after:event hooks synchronously", () => { - const hookified = new Hookified(); - const handlerData: string[] = []; - - const handler = () => { - handlerData.push("after"); - }; - - hookified.onHook("after:test", handler); - hookified.afterHookSync("test"); - - expect(handlerData).toEqual(["after"]); - }); - - test("should skip async handlers", () => { - const hookified = new Hookified(); - const handlerData: string[] = []; - - hookified.onHook("after:test", () => handlerData.push("sync")); - hookified.onHook("after:test", async () => handlerData.push("async")); - - hookified.afterHookSync("test"); - - expect(handlerData).toEqual(["sync"]); - }); - - test("should pass arguments to handlers", () => { - const hookified = new Hookified(); - const data = { key: "value" }; - - const handler = (data: any) => { - data.key = "modified"; - }; - - hookified.onHook("after:test", handler); - hookified.afterHookSync("test", data); - - expect(data.key).toBe("modified"); - }); - }); - - describe("callHookSync", () => { - test("should be an alias for hookSync", () => { - const hookified = new Hookified(); - const data = { key: "value" }; - - const handler = (data: any) => { - data.key = "modified"; - }; - - hookified.onHook("event", handler); - hookified.callHookSync("event", data); - - expect(data.key).toBe("modified"); - }); - - test("should skip async handlers", () => { - const hookified = new Hookified(); - const executionOrder: string[] = []; - - hookified.onHook("event", () => executionOrder.push("sync")); - hookified.onHook("event", async () => executionOrder.push("async")); - - hookified.callHookSync("event"); - - expect(executionOrder).toEqual(["sync"]); - }); - }); - - describe("hookSync and async hook integration", () => { - test("should work with before and after sync hooks together", () => { - const hookified = new Hookified(); - const executionOrder: string[] = []; - - const beforeHandler = () => { - executionOrder.push("before"); - }; - - const mainHandler = () => { - executionOrder.push("main"); - }; - - const afterHandler = () => { - executionOrder.push("after"); - }; - - hookified.onHook("before:operation", beforeHandler); - hookified.onHook("operation", mainHandler); - hookified.onHook("after:operation", afterHandler); - - hookified.beforeHookSync("operation"); - hookified.hookSync("operation"); - hookified.afterHookSync("operation"); - - expect(executionOrder).toEqual(["before", "main", "after"]); - }); - - test("should maintain data integrity across sync hooks", () => { - const hookified = new Hookified(); - const data = { value: 0 }; - - const beforeHandler = (data: any) => { - data.value = 10; - }; - - const afterHandler = (data: any) => { - data.value = data.value * 2; - }; - - hookified.onHook("before:transform", beforeHandler); - hookified.onHook("after:transform", afterHandler); - - hookified.beforeHookSync("transform", data); - expect(data.value).toBe(10); - - hookified.afterHookSync("transform", data); - expect(data.value).toBe(20); - }); - }); });