hooks: New `callAllSerial()` function

This is necessary to migrate away from `callAll()` (which only
supports synchronous hook functions).
pull/4698/head
Richard Hansen 2021-02-01 01:18:44 -05:00 committed by John McLear
parent 763fe6fc26
commit 05e0e8dbf7
2 changed files with 102 additions and 1 deletions

View File

@ -175,6 +175,8 @@ const callHookFnSync = (hook, context) => {
return outcome.val;
};
// DEPRECATED: Use `callAllSerial()` or `aCallAll()` instead.
//
// Invokes all registered hook functions synchronously.
//
// Arguments:
@ -317,7 +319,10 @@ const callHookFnAsync = async (hook, context) => {
});
};
// Invokes all registered hook functions asynchronously.
// Invokes all registered hook functions asynchronously and concurrently. This is NOT the async
// equivalent of `callAll()`: `callAll()` calls the hook functions serially (one at a time) but this
// function calls them concurrently. Use `callAllSerial()` if the hook functions must be called one
// at a time.
//
// Arguments:
// * hookName: Name of the hook to invoke.
@ -344,6 +349,19 @@ exports.aCallAll = async (hookName, context, cb = null) => {
return flatten1(results);
};
// Like `aCallAll()` except the hook functions are called one at a time instead of concurrently.
// Only use this function if the hook functions must be called one at a time, otherwise use
// `aCallAll()`.
exports.callAllSerial = async (hookName, context) => {
if (context == null) context = {};
const hooks = pluginDefs.hooks[hookName] || [];
const results = [];
for (const hook of hooks) {
results.push(normalizeValue(await callHookFnAsync(hook, context)));
}
return flatten1(results);
};
// DEPRECATED: Use `aCallFirst()` instead.
//
// Like `aCallFirst()`, but synchronous. Hook functions must provide their values synchronously.

View File

@ -968,6 +968,89 @@ describe(__filename, function () {
});
});
describe('hooks.callAllSerial', function () {
describe('basic behavior', function () {
it('calls all asynchronously, serially, in order', async function () {
const gotCalls = [];
testHooks.length = 0;
for (let i = 0; i < 3; i++) {
const hook = makeHook();
hook.hook_fn = async () => {
gotCalls.push(i);
// Check gotCalls asynchronously to ensure that the next hook function does not start
// executing before this hook function has resolved.
return await new Promise((resolve) => {
setImmediate(() => {
assert.deepEqual(gotCalls, [...Array(i + 1).keys()]);
resolve(i);
});
});
};
testHooks.push(hook);
}
assert.deepEqual(await hooks.callAllSerial(hookName), [0, 1, 2]);
assert.deepEqual(gotCalls, [0, 1, 2]);
});
it('passes hook name', async function () {
hook.hook_fn = async (hn) => { assert.equal(hn, hookName); };
await hooks.callAllSerial(hookName);
});
it('undefined context -> {}', async function () {
hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); };
await hooks.callAllSerial(hookName);
});
it('null context -> {}', async function () {
hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); };
await hooks.callAllSerial(hookName, null);
});
it('context unmodified', async function () {
const wantContext = {};
hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); };
await hooks.callAllSerial(hookName, wantContext);
});
});
describe('result processing', function () {
it('no registered hooks (undefined) -> []', async function () {
delete plugins.hooks[hookName];
assert.deepEqual(await hooks.callAllSerial(hookName), []);
});
it('no registered hooks (empty list) -> []', async function () {
testHooks.length = 0;
assert.deepEqual(await hooks.callAllSerial(hookName), []);
});
it('flattens one level', async function () {
testHooks.length = 0;
testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]]));
assert.deepEqual(await hooks.callAllSerial(hookName), [1, 2, [3]]);
});
it('filters out undefined', async function () {
testHooks.length = 0;
testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]), makeHook(Promise.resolve()));
assert.deepEqual(await hooks.callAllSerial(hookName), [2, [3]]);
});
it('preserves null', async function () {
testHooks.length = 0;
testHooks.push(makeHook(null), makeHook([2]), makeHook(Promise.resolve(null)));
assert.deepEqual(await hooks.callAllSerial(hookName), [null, 2, null]);
});
it('all undefined -> []', async function () {
testHooks.length = 0;
testHooks.push(makeHook(), makeHook(Promise.resolve()));
assert.deepEqual(await hooks.callAllSerial(hookName), []);
});
});
});
describe('hooks.aCallFirst', function () {
it('no registered hooks (undefined) -> []', async function () {
delete plugins.hooks.testHook;