Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
- [.callHook(eventName, ...args)](#callhookeventname-args)
- [.beforeHook(eventName, ...args)](#beforehookeventname-args)
- [.afterHook(eventName, ...args)](#afterhookeventname-args)
- [.hookSync(eventName, ...args)](#hooksync-eventname-args)
- [.hooks](#hooks)
- [.getHooks(eventName)](#gethookseventname)
- [.clearHooks(eventName)](#clearhookeventname)
Expand Down Expand Up @@ -801,6 +802,44 @@ 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.

> **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';

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
```

## .hooks

Get all hooks.
Expand Down
38 changes: 38 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(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
Expand Down
172 changes: 172 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1678,4 +1678,176 @@ 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"]);
});
});
});
Loading