Skip to content

Commit

Permalink
Merge pull request #401 from evershopcommerce/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
treoden authored Nov 25, 2023
2 parents 98f8212 + 9b5eb26 commit c873272
Show file tree
Hide file tree
Showing 56 changed files with 1,798 additions and 1,682 deletions.
120 changes: 120 additions & 0 deletions packages/evershop/src/lib/util/hookable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
const beforeHooks = new Map();
const afterHooks = new Map();
let locked = false;

function isAsyncFunction(func) {
return func.constructor.name === 'AsyncFunction';
}

function hook(funcName, callback, priority = 10, position = 'before') {
if (locked) {
throw new Error(
'Hooks are locked. You should consider adding hooks using the bootstrap function'
);
}
if (typeof callback !== 'function') {
throw new Error('Callback must be a function');
}

if (typeof priority !== 'number') {
throw new Error('Priority must be a number');
}

const storage = position === 'before' ? beforeHooks : afterHooks;

if (!storage.has(funcName)) {
storage.set(funcName, []);
}

const hooks = storage.get(funcName);
hooks.push({ callback, priority });
hooks.sort((a, b) => a.priority - b.priority);
}

function hookAfter(funcName, callback, priority = 10) {
hook(funcName, callback, priority, 'after');
}

function hookBefore(funcName, callback, priority = 10) {
hook(funcName, callback, priority, 'before');
}

function hookable(originalFunction, context) {
// Make sure the original function is a named function
const funcName = originalFunction.name;
if (!funcName) {
throw new Error('The original function must be a named function');
}
return new Proxy(originalFunction, {
apply: isAsyncFunction(originalFunction)
? async function (target, thisArg, argumentsList) {
const beforeHookFunctions = beforeHooks.get(funcName) || [];
const afterHookFunctions = afterHooks.get(funcName) || [];

for (
let index = 0;
index < beforeHookFunctions.length;
index += 1
) {
const callbackFunc = beforeHookFunctions[index].callback;
await callbackFunc.call(context);
}
const result = await Reflect.apply(target, thisArg, argumentsList);

for (
let index = 0;
index < afterHookFunctions.length;
index += 1
) {
const callbackFunc = afterHookFunctions[index].callback;
await callbackFunc.call({
...context,
[funcName]: result
});
}

return result;
}
: function (target, thisArg, argumentsList) {
const beforeHookFunctions = beforeHooks.get(funcName) || [];
const afterHookFunctions = afterHooks.get(funcName) || [];

beforeHookFunctions.forEach((hook) => {
hook.callback.call(context);
});

const result = Reflect.apply(target, thisArg, argumentsList);

afterHookFunctions.forEach((hook) => {
hook.callback.call({ ...context, [funcName]: result });
});

return result;
}
});
}

function getHooks() {
return {
beforeHooks,
afterHooks
};
}

function clearHooks() {
beforeHooks.clear();
afterHooks.clear();
}

function lockHooks() {
locked = true;
}

module.exports = {
hookBefore,
hookAfter,
hookable,
getHooks,
clearHooks,
lockHooks
};
4 changes: 2 additions & 2 deletions packages/evershop/src/lib/util/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ module.exports = {
* @param {Object} context
* @param {Function} validator
*/
async get(name, initValue, context, validator) {
async getValue(name, initValue, context, validator) {
const val = await registry.get(name, initValue, context, validator);
return val;
},
Expand All @@ -159,7 +159,7 @@ module.exports = {
* @param {Object} context
* @param {Function} validator
*/
getSync(name, initValue, context, validator) {
getValueSync(name, initValue, context, validator) {
const val = registry.getSync(name, initValue, context, validator);
return val;
},
Expand Down
188 changes: 188 additions & 0 deletions packages/evershop/src/lib/util/tests/unit/util.hookable.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
const {
hookable,
hookBefore,
getHooks,
clearHooks,
lockHooks
} = require('../../hookable');

describe('hookBefore', () => {
it('It should add before hook to the registry', () => {
const callback = () => {};
hookBefore('test', callback);
const { beforeHooks } = getHooks();
expect(beforeHooks.get('test')).toEqual([
{
callback,
priority: 10
}
]);
});

it('It should throw error if priority is not a number', () => {
const callback = () => {};
expect(() => hookBefore('test', callback, 'abc')).toThrow(Error);
});

it('It should add before hook to the registry with priority', () => {
const negativeCallback = () => {};
const beforeCallback = () => {};
const afterCallback = () => {};

hookBefore('test2', beforeCallback, 5);
hookBefore('test2', negativeCallback, -5);
hookBefore('test2', afterCallback, 20);
const { beforeHooks } = getHooks();
expect(JSON.stringify(beforeHooks.get('test2'))).toEqual(
JSON.stringify([
{
callback: negativeCallback,
priority: -5
},
{
callback: beforeCallback,
priority: 5
},
{
callback: afterCallback,
priority: 20
}
])
);
});
});

describe('hookable', () => {
it('It should throw error if the original function is not a named function', () => {
expect(() => hookable(() => {})).toThrow(Error);
});

it('It should return a function', () => {
const func = function test() {};
expect(typeof hookable(func)).toEqual('function');
});

it('It should call the original function', () => {
const func = jest.fn();
const hookedFunc = hookable(func);
hookedFunc();
expect(func).toHaveBeenCalled();
});

it('It should throw error if one of the callback throws error', () => {
const test = jest.fn();
hookBefore('mockConstructor', () => {
throw new Error('Error');
});
const hookedFunc = hookable(test);
expect(() => hookedFunc()).toThrow(Error);
});

it('It should throw error if one of the callback throws error', async () => {
const test = jest.fn();
hookBefore('mockConstructor', async () => {
throw new Error('Error');
});
const hookedFunc = hookable(test);
await expect(async () => await hookedFunc()).rejects.toThrow('Error');
});

it('It should call the before hook in correct order', () => {
clearHooks();
const data = [];
const test = jest.fn();
const beforeCallback1 = jest.fn(() => data.push(1));
const beforeCallback2 = jest.fn(() => data.push(2));
const beforeCallback3 = jest.fn(() => data.push(3));
hookBefore('mockConstructor', beforeCallback1);
hookBefore('mockConstructor', beforeCallback2);
hookBefore('mockConstructor', beforeCallback3, 1);
const hookedFunc = hookable(test);
hookedFunc();
expect(beforeCallback1).toHaveBeenCalled();
expect(beforeCallback2).toHaveBeenCalled();
expect(beforeCallback3).toHaveBeenCalled();
expect(data).toEqual([3, 1, 2]);
});

it('It should call the before hook in correct order async', () => {
clearHooks();
const data = [];
const test = jest.fn(async () => new Promise((resolve) => resolve()));
const beforeCallback1 = jest.fn(
async () => new Promise((resolve) => resolve(data.push(1)))
);
const beforeCallback2 = jest.fn(
async () => new Promise((resolve) => resolve(data.push(2)))
);
const beforeCallback3 = jest.fn(
async () => new Promise((resolve) => resolve(data.push(3)))
);
hookBefore('mockConstructor', beforeCallback1);
hookBefore('mockConstructor', beforeCallback2);
hookBefore('mockConstructor', beforeCallback3, 1);
const hookedFunc = hookable(test);
hookedFunc();
expect(beforeCallback1).toHaveBeenCalled();
expect(beforeCallback2).toHaveBeenCalled();
expect(beforeCallback3).toHaveBeenCalled();
expect(data).toEqual([3, 1, 2]);
});

it('It should call the before hook in correct order async', async () => {
clearHooks();
const data = [];
const test = jest.fn(
async () =>
new Promise((resolve) => {
data.push(0);
setTimeout(() => {
data.push(4);
resolve();
}, 1000);
})
);
const beforeCallback1 = jest.fn(
async () => new Promise((resolve) => resolve(data.push(1)))
);
const beforeCallback2 = jest.fn(
async () => new Promise((resolve) => resolve(data.push(2)))
);
const beforeCallback3 = jest.fn(() => data.push(3));
hookBefore('mockConstructor', beforeCallback1);
hookBefore('mockConstructor', beforeCallback2);
hookBefore('mockConstructor', beforeCallback3);
const hookedFunc = hookable(test);
await hookedFunc();
expect(beforeCallback1).toHaveBeenCalled();
expect(beforeCallback2).toHaveBeenCalled();
expect(beforeCallback3).toHaveBeenCalled();
expect(data).toEqual([1, 2, 3, 0, 4]);
});

it('It should call the original function with correct argument', () => {
const test = jest.fn();
const hookedFunc = hookable(test);
hookedFunc(1, 2, 3);
expect(test).toHaveBeenCalledWith(1, 2, 3);
});

it('It should call the callback with correct context', () => {
const test = jest.fn();
const beforeCallback = jest.fn(function () {
expect(this).toEqual({ test: 1 });
});
hookBefore('mockConstructor', beforeCallback);
const hookedFunc = hookable(test, { test: 1 });
hookedFunc();
expect(beforeCallback).toHaveBeenCalled();
});
});

describe('lockHooks', () => {
it('It should throw error if the hook is locked', () => {
lockHooks();
const callback = () => {};
expect(() => hookBefore('test', callback)).toThrow(Error);
});
});
Loading

0 comments on commit c873272

Please sign in to comment.