-
-
Notifications
You must be signed in to change notification settings - Fork 954
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
Make hooks better 🦄 #523
Make hooks better 🦄 #523
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
'use strict'; | ||
const is = require('@sindresorhus/is'); | ||
|
||
module.exports = (where, properties, deep, excluded) => { | ||
const deepFreeze = (obj, parent) => { | ||
for (const [key, value] of Object.entries(obj)) { | ||
const name = parent + '.' + key; | ||
|
||
if (is.object(value) && !excluded.includes(name)) { | ||
deepFreeze(obj[key], name); | ||
} | ||
} | ||
|
||
return excluded.includes(parent) ? obj : Object.freeze(obj); | ||
}; | ||
|
||
for (const [key, value] of Object.entries(properties)) { | ||
Object.defineProperty(where, key, { | ||
value: deep ? deepFreeze(value, key) : Object.freeze(value), | ||
writable: false, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,6 @@ const urlToOptions = require('./url-to-options'); | |
const isFormData = require('./is-form-data'); | ||
|
||
const retryAfterStatusCodes = new Set([413, 429, 503]); | ||
const knownHookEvents = ['beforeRequest']; | ||
|
||
module.exports = (url, options, defaults) => { | ||
if (Reflect.has(options, 'url') || (is.object(url) && Reflect.has(url, 'url'))) { | ||
|
@@ -187,24 +186,14 @@ module.exports = (url, options, defaults) => { | |
delete options.timeout; | ||
} | ||
|
||
if (is.nullOrUndefined(options.hooks)) { | ||
options.hooks = {}; | ||
} | ||
if (is.object(options.hooks)) { | ||
for (const hookEvent of knownHookEvents) { | ||
const hooks = options.hooks[hookEvent]; | ||
if (is.nullOrUndefined(hooks)) { | ||
options.hooks[hookEvent] = []; | ||
} else if (is.array(hooks)) { | ||
hooks.forEach( | ||
(hook, index) => { | ||
if (!is.function_(hook)) { | ||
throw new TypeError( | ||
`Parameter \`hooks.${hookEvent}[${index}]\` must be a function, not ${is(hook)}` | ||
); | ||
} | ||
for (const [hookEvent, hooks] of Object.entries(options.hooks)) { | ||
if (is.array(hooks)) { | ||
for (const [index, hook] of Object.entries(hooks)) { | ||
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. This is not equivalent to the current implementation. Consider these examples: const a = [1, 2];
a.foo = 'bar';
Object.entries(a); // [ [ '0', 1 ], [ '1', 2 ], [ 'foo', 1 ] ] 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. It's not a bug. It's a feature! |
||
if (!is.function(hook)) { | ||
throw new TypeError(`Parameter \`hooks.${hookEvent}[${index}]\` must be a function, not ${is(hook)}`); | ||
} | ||
); | ||
} | ||
} else { | ||
throw new TypeError(`Parameter \`hooks.${hookEvent}\` must be an array, not ${is(hooks)}`); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,12 @@ const {CacheError, UnsupportedProtocolError, MaxRedirectsError, RequestError} = | |
const getMethodRedirectCodes = new Set([300, 301, 302, 303, 304, 305, 307, 308]); | ||
const allMethodRedirectCodes = new Set([300, 303, 307, 308]); | ||
|
||
const callAll = async (array, ...args) => { | ||
for (const func of array) { | ||
await func(...args); // eslint-disable-line no-await-in-loop | ||
} | ||
}; | ||
|
||
module.exports = (options = {}) => { | ||
const emitter = new EventEmitter(); | ||
const requestUrl = options.href || (new URLGlobal(options.path, urlLib.format(options))).toString(); | ||
|
@@ -122,8 +128,9 @@ module.exports = (options = {}) => { | |
|
||
cacheReq.once('request', req => { | ||
let aborted = false; | ||
req.once('abort', _ => { | ||
req.once('abort', () => { | ||
aborted = true; | ||
callAll(options.hooks.onAbort); | ||
}); | ||
|
||
req.once('error', error => { | ||
|
@@ -150,7 +157,9 @@ module.exports = (options = {}) => { | |
|
||
const socket = req.connection; | ||
if (socket) { | ||
const onSocketConnect = () => { | ||
const onSocketConnect = async () => { | ||
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 what the origin of this conditional block are, but I think the right way to handle this event is to remove the if block and make this: req.on('socket', () => { ... }); https://nodejs.org/dist/latest-v8.x/docs/api/http.html#http_event_socket 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. Actually, since 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.
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. Thanks for the reference to #429. Maybe I'm missing something, but I don't think that was the right fix b/c req.once('socket', (sock) => {
sock.once('connect', conn => {
// ...
}
}; I can see the convenience of low-friction hooks. Maybe there's a way to make the configuration a sort of DSL that got can use to wire these up. Something like this: {
events: {
request: {
['on|once']: {
socket: {
null: [(sock) => { console.log('request.on(socket)') }],
connect: [(conn) => { console.log('socket.on(connect)') }]
}
},
}
}
} With the above, the configuration can be traversed and the listeners can be subscribed without having to add handling code to got for every event. I used 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 don't know. Can you make another issue for that? 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.
Great! We'll discuss this later. I was gonna send a PR with rewrited hooks from scratch but I decided current implementation is better. :) |
||
await callAll(options.hooks.onSocketConnect); | ||
|
||
const uploadEventFrequency = 150; | ||
|
||
progressInterval = setInterval(() => { | ||
|
@@ -241,10 +250,7 @@ module.exports = (options = {}) => { | |
options.headers['content-length'] = uploadBodySize; | ||
} | ||
|
||
for (const hook of options.hooks.beforeRequest) { | ||
// eslint-disable-next-line no-await-in-loop | ||
await hook(options); | ||
} | ||
await callAll(options.hooks.beforeRequest, options); | ||
|
||
get(options); | ||
} catch (error) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -132,7 +132,7 @@ test('throws TypeError when known `hooks` array item is not a function', async t | |
}); | ||
|
||
test('allows extra keys in `hooks`', async t => { | ||
await t.notThrows(() => got(`${s.url}/test`, {hooks: {extra: {}}})); | ||
await t.notThrows(() => got(`${s.url}/test`, {hooks: {extra: []}})); | ||
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 point of this test is to show that extra keys are not required to pass validation b/c they should be ignored. The test title could be more clear. 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. Why would someone assign non-hook object to hooks? What's the use case? What big problem does this solve? Sorry, but I don't see that. 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. It allows for encapsulation. Here's a contrived example: class GotHooks {
constructor () {
this.message = 'Encapsulated Got Hooks';
this.beforeRequest = [
() => this.runRook()
];
}
async runRook() {
console.log(this.message);
}
} If got validates only the properties it is concerned with, then everything is fine. If got extraneously demands that the object only have properties that correspond to known hooks, then the user has to jump through some hoops to create an object that accomplishes the same thing. 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. Thank you for the detailed response, I really appreciate that! 👍 |
||
}); | ||
|
||
test.after('cleanup', async () => { | ||
|
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.
This fails to validate the known hooks if something other than an array is provided, which will result in an error upstream.
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.
Nope. https://github.com/szmarczak/got/blob/b22c3abbe8daf905aae4bed66cddfc7b9ed72170/source/normalize-arguments.js#L191-L199
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.
Ah, thanks - I got tripped up reviewing the diff.