Skip to content

perf: improve performance, reduce allocations, and avoid promises #102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"prepublish": "pnpm build",
"release": "pnpm test && pnpm build && changelogen --release --push && pnpm publish",
"test": "pnpm lint && vitest run --coverage",
"test:types": "tsc --noEmit"
"test:types": "tsc --noEmit",
"benchmark": "vitest bench"
},
"devDependencies": {
"@types/node": "^18.16.1",
Expand Down
42 changes: 22 additions & 20 deletions src/hookable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
NestedHooks,
HookCallback,
HookKeys,
Thenable,
} from "./types";

type InferCallback<HT, HN extends keyof HT> = HT[HN] extends HookCallback
Expand Down Expand Up @@ -121,15 +122,17 @@ export class Hookable<
name: NameT,
function_: InferCallback<HooksT, NameT>
) {
if (this._hooks[name]) {
const index = this._hooks[name].indexOf(function_);
const hooks = this._hooks[name];

if (hooks) {
const index = hooks.indexOf(function_);

if (index !== -1) {
this._hooks[name].splice(index, 1);
hooks.splice(index, 1);
}

if (this._hooks[name].length === 0) {
delete this._hooks[name];
if (hooks.length === 0) {
this._hooks[name] = undefined;
}
}
}
Expand All @@ -141,7 +144,7 @@ export class Hookable<
this._deprecatedHooks[name] =
typeof deprecated === "string" ? { to: deprecated } : deprecated;
const _hooks = this._hooks[name] || [];
delete this._hooks[name];
this._hooks[name] = undefined;
for (const hook of _hooks) {
this.hook(name, hook as any);
}
Expand All @@ -150,7 +153,6 @@ export class Hookable<
deprecateHooks(
deprecatedHooks: Partial<Record<HookNameT, DeprecatedHook<HooksT>>>
) {
Object.assign(this._deprecatedHooks, deprecatedHooks);
for (const name in deprecatedHooks) {
this.deprecateHook(name, deprecatedHooks[name] as DeprecatedHook<HooksT>);
}
Expand All @@ -164,11 +166,13 @@ export class Hookable<
);

return () => {
// Splice will ensure that all fns are called once, and free all
// unreg functions from memory.
for (const unreg of removeFns.splice(0, removeFns.length)) {
for (const unreg of removeFns) {
unreg();
}

// Ensures that all fns are called once, and free all
// unreg functions from memory.
removeFns.length = 0;
};
}

Expand All @@ -181,25 +185,23 @@ export class Hookable<
}

removeAllHooks() {
for (const key in this._hooks) {
delete this._hooks[key];
}
this._hooks = {};
}

callHook<NameT extends HookNameT>(
name: NameT,
...arguments_: Parameters<InferCallback<HooksT, NameT>>
): Promise<any> {
arguments_.unshift(name);
return this.callHookWith(serialTaskCaller, name, ...arguments_);
): Thenable<any> {
// @ts-expect-error we always inject name
return this.callHookWith(serialTaskCaller, name, name, ...arguments_);
}

callHookParallel<NameT extends HookNameT>(
name: NameT,
...arguments_: Parameters<InferCallback<HooksT, NameT>>
): Promise<any[]> {
arguments_.unshift(name);
return this.callHookWith(parallelTaskCaller, name, ...arguments_);
): Thenable<any[]> {
// @ts-expect-error we always inject name
return this.callHookWith(parallelTaskCaller, name, name, ...arguments_);
}

callHookWith<
Expand All @@ -221,7 +223,7 @@ export class Hookable<
callEachWith(this._before, event);
}
const result = caller(
name in this._hooks ? [...this._hooks[name]] : [],
this._hooks[name] ? [...this._hooks[name]] : [],
arguments_
);
if ((result as any) instanceof Promise) {
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export type DeprecatedHook<T> = { message?: string; to: HookKeys<T> };
// eslint-disable-next-line no-unused-vars
export type DeprecatedHooks<T> = { [name in HookKeys<T>]: DeprecatedHook<T> };

export type Thenable<T> = Promise<T> | T;

// Utilities
type ValueOf<C> = C extends Record<any, any> ? C[keyof C] : never;
type Strings<T> = Exclude<keyof T, number | symbol>;
Expand Down
41 changes: 31 additions & 10 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,20 +64,41 @@ const _createTask: CreateTask = () => defaultTask;
const createTask =
typeof console.createTask !== "undefined" ? console.createTask : _createTask;

function nextDispatch(
hooks: HookCallback[],
args: any[],
task: typeof defaultTask,
startIndex: number
) {
for (let i = startIndex; i < hooks.length; i += 1) {
try {
const result = task.run(() => hooks[i](...args));

if (result instanceof Promise) {
return result.then(() => nextDispatch(hooks, args, task, i + 1));
}
} catch (error) {
return Promise.reject(error);
}
}
}

export function serialTaskCaller(hooks: HookCallback[], args: any[]) {
const name = args.shift();
const task = createTask(name);
// eslint-disable-next-line unicorn/no-array-reduce
return hooks.reduce(
(promise, hookFunction) =>
promise.then(() => task.run(() => hookFunction(...args))),
Promise.resolve()
);
if (hooks.length === 0) {
return;
}

const task = createTask(args.shift());

return nextDispatch(hooks, args, task, 0);
}

export function parallelTaskCaller(hooks: HookCallback[], args: any[]) {
const name = args.shift();
const task = createTask(name);
if (hooks.length === 0) {
return;
}

const task = createTask(args.shift());
return Promise.all(hooks.map((hook) => task.run(() => hook(...args))));
}

Expand Down
Loading