-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
New hooks API (replaces monkey-patching for currency) #1683
Changes from all commits
77ac232
c380a19
0e09dd5
7b97b2a
25502d3
38d17a4
9565c91
37b291e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
|
||
/** | ||
* @typedef {function} HookedFunction | ||
* @property {function(function(), [number])} addHook A method that takes a new function to attach as a hook | ||
* to the HookedFunction | ||
* @property {function(function())} removeHook A method to remove attached hooks | ||
*/ | ||
|
||
/** | ||
* A map of global hook methods to allow easy extension of hooked functions that are intended to be extended globally | ||
* @type {{}} | ||
*/ | ||
export const hooks = {}; | ||
|
||
/** | ||
* A utility function for allowing a regular function to be extensible with additional hook functions | ||
* @param {string} type The method for applying all attached hooks when this hooked function is called | ||
* @param {function()} fn The function to make hookable | ||
* @param {string} hookName If provided this allows you to register a name for a global hook to have easy access to | ||
* the addHook and removeHook methods for that hook (which are usually accessed as methods on the function itself) | ||
* @returns {HookedFunction} A new function that implements the HookedFunction interface | ||
*/ | ||
export function createHook(type, fn, hookName) { | ||
let _hooks = [{fn, priority: 0}]; | ||
|
||
let types = { | ||
sync: function(...args) { | ||
_hooks.forEach(hook => { | ||
hook.fn.apply(this, args); | ||
}); | ||
}, | ||
asyncSeries: function(...args) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure exactly what the issue(s) are... but I uncovered some weird bugs in The following test passes, as expected: it('should allow context to be passed to hooks, but keep bound contexts', () => {
let context;
let fn = function() {
context = this;
};
let boundContext1 = {};
let calledBoundContext1;
let hook1 = function(next) {
calledBoundContext1 = this;
next()
}.bind(boundContext1);
let hookFn = createHook('asyncSeries', fn);
hookFn.addHook(hook1);
hookFn();
expect(calledBoundContext1).to.equal(boundContext1);
}); However, it begins failing if you add a second hook, like so: it('should allow context to be passed to hooks, but keep bound contexts', () => {
let context;
let fn = function() {
context = this;
};
let boundContext1 = {};
let calledBoundContext1;
let hook1 = function(next) {
calledBoundContext1 = this;
next()
}.bind(boundContext1);
let boundContext2 = {};
let calledBoundContext2;
let hook2 = function(next) {
calledBoundContext2 = this;
next()
}.bind(boundContext2);
let hookFn = createHook('asyncSeries', fn);
hookFn.addHook(hook1);
// let newContext = {};
// hookFn = hookFn.bind(newContext);
hookFn();
// expect(context).to.equal(newContext);
expect(calledBoundContext1).to.equal(boundContext1);
expect(calledBoundContext2).to.equal(boundContext2);
}); It also fails with a single hook if the hookable function is bound: it('should allow context to be passed to hooks, but keep bound contexts', () => {
let context;
let fn = function() {
context = this;
};
let boundContext1 = {};
let calledBoundContext1;
let hook1 = function(next) {
calledBoundContext1 = this;
next()
}.bind(boundContext1);
let hookFn = createHook('asyncSeries', fn);
hookFn.addHook(hook1);
let newContext = {};
hookFn = hookFn.bind(newContext);
hookFn();
expect(context).to.equal(newContext);
expect(calledBoundContext1).to.equal(boundContext1);
}); The error messages aren't very helpful... just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The first test case you list I think is failing because you never added the second hook. I think adding The second test case is a bug. I wasn't properly maintaining the |
||
let curr = 0; | ||
|
||
const asyncSeriesNext = (...args) => { | ||
let hook = _hooks[++curr]; | ||
if (typeof hook === 'object' && typeof hook.fn === 'function') { | ||
return hook.fn.apply(this, args.concat(asyncSeriesNext)) | ||
} | ||
}; | ||
|
||
return _hooks[curr].fn.apply(this, args.concat(asyncSeriesNext)); | ||
} | ||
}; | ||
|
||
if (!types[type]) { | ||
throw 'invalid hook type'; | ||
} | ||
|
||
let methods = { | ||
addHook: function(fn, priority = 10) { | ||
if (typeof fn === 'function') { | ||
_hooks.push({ | ||
fn, | ||
priority: priority | ||
}); | ||
|
||
_hooks.sort((a, b) => b.priority - a.priority); | ||
} | ||
}, | ||
removeHook: function(removeFn) { | ||
_hooks = _hooks.filter(hook => hook.fn === fn || hook.fn !== removeFn); | ||
} | ||
}; | ||
|
||
if (typeof hookName === 'string') { | ||
hooks[hookName] = methods; | ||
} | ||
|
||
function hookedFn(...args) { | ||
if (_hooks.length === 0) { | ||
return fn.apply(this, args); | ||
} | ||
return types[type].apply(this, args); | ||
} | ||
|
||
return Object.assign(hookedFn, methods); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
|
||
import { expect } from 'chai'; | ||
import { createHook, hooks } from 'src/hook'; | ||
|
||
describe('the hook module', () => { | ||
let sandbox; | ||
|
||
beforeEach(() => { | ||
sandbox = sinon.sandbox.create(); | ||
}); | ||
|
||
afterEach(() => { | ||
sandbox.restore(); | ||
}); | ||
|
||
it('should call all sync hooks attached to a function', () => { | ||
let called = []; | ||
let calledWith; | ||
|
||
let testFn = () => { | ||
called.push(testFn); | ||
}; | ||
let testHook = (...args) => { | ||
called.push(testHook); | ||
calledWith = args; | ||
}; | ||
let testHook2 = () => { | ||
called.push(testHook2); | ||
}; | ||
let testHook3 = () => { | ||
called.push(testHook3); | ||
}; | ||
|
||
let hookedTestFn = createHook('sync', testFn, 'testHook'); | ||
|
||
hookedTestFn.addHook(testHook, 50); | ||
hookedTestFn.addHook(testHook2, 100); | ||
|
||
// make sure global test hooks work as well (with default priority) | ||
hooks['testHook'].addHook(testHook3); | ||
|
||
hookedTestFn(1, 2, 3); | ||
|
||
expect(called).to.deep.equal([ | ||
testHook2, | ||
testHook, | ||
testHook3, | ||
testFn | ||
]); | ||
|
||
expect(calledWith).to.deep.equal([1, 2, 3]); | ||
|
||
called = []; | ||
|
||
hookedTestFn.removeHook(testHook); | ||
hooks['testHook'].removeHook(testHook3); | ||
|
||
hookedTestFn(1, 2, 3); | ||
|
||
expect(called).to.deep.equal([ | ||
testHook2, | ||
testFn | ||
]); | ||
}); | ||
|
||
it('should allow context to be passed to hooks, but keep bound contexts', () => { | ||
let context; | ||
let fn = function() { | ||
context = this; | ||
}; | ||
|
||
let boundContext = {}; | ||
let calledBoundContext; | ||
let hook = function() { | ||
calledBoundContext = this; | ||
}.bind(boundContext); | ||
|
||
let hookFn = createHook('sync', fn); | ||
hookFn.addHook(hook); | ||
|
||
let newContext = {}; | ||
hookFn.bind(newContext)(); | ||
|
||
expect(context).to.equal(newContext); | ||
expect(calledBoundContext).to.equal(boundContext); | ||
}); | ||
|
||
describe('asyncSeries', () => { | ||
it('should call function as normal if no hooks attached', () => { | ||
let fn = sandbox.spy(); | ||
let hookFn = createHook('asyncSeries', fn); | ||
|
||
hookFn(1); | ||
|
||
expect(fn.calledOnce).to.equal(true); | ||
expect(fn.firstCall.args[0]).to.equal(1); | ||
}); | ||
|
||
it('should call hooks correctly applied in asyncSeries', () => { | ||
let called = []; | ||
|
||
let testFn = (called) => { | ||
called.push(testFn); | ||
}; | ||
let testHook = (called, next) => { | ||
called.push(testHook); | ||
next(called); | ||
}; | ||
let testHook2 = (called, next) => { | ||
called.push(testHook2); | ||
next(called); | ||
}; | ||
|
||
let hookedTestFn = createHook('asyncSeries', testFn); | ||
hookedTestFn.addHook(testHook); | ||
hookedTestFn.addHook(testHook2); | ||
|
||
hookedTestFn(called); | ||
|
||
expect(called).to.deep.equal([ | ||
testHook, | ||
testHook2, | ||
testFn | ||
]); | ||
}); | ||
|
||
it('should allow context to be passed to hooks, but keep bound contexts', () => { | ||
let context; | ||
let fn = function() { | ||
context = this; | ||
}; | ||
|
||
let boundContext1 = {}; | ||
let calledBoundContext1; | ||
let hook1 = function(next) { | ||
calledBoundContext1 = this; | ||
next() | ||
}.bind(boundContext1); | ||
|
||
let hookFn = createHook('asyncSeries', fn); | ||
hookFn.addHook(hook1); | ||
|
||
let newContext = {}; | ||
hookFn = hookFn.bind(newContext); | ||
hookFn(); | ||
|
||
expect(context).to.equal(newContext); | ||
expect(calledBoundContext1).to.equal(boundContext1); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there any value in setting a priority when no hooks compete yet?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. If others add hooks later this one will still run first. I specifically wanted currency running first as it buffers responses (if the currency file isn't back yet).