hooks: Rewrite callAll and aCallAll for consistency

Rewrite the `callAll` and `aCallAll` functions to support all
reasonable hook behaviors and to report errors for unreasonable
behaviors (e.g., calling the callback twice).

Now a hook function like the following works as expected when invoked
by `aCallAll`:

```
exports.myHookFn = (hookName, context, cb) => {
  cb('some value');
  return;
};
```
This commit is contained in:
Richard Hansen 2020-10-08 01:42:49 -04:00 committed by John McLear
parent 79119baf58
commit 36aceb3aba
6 changed files with 1400 additions and 74 deletions

View file

@ -4,48 +4,114 @@ A hook function is registered with a hook via the plugin's `ep.json` file. See
the Plugins section for details. A hook may have many registered functions from
different plugins.
When a hook is invoked, its registered functions are called with three
arguments:
Some hooks call their registered functions one at a time until one of them
returns a value. Others always call all of their registered functions and
combine the results (if applicable).
1. hookName - The name of the hook being invoked.
2. context - An object with some relevant information about the context of the
## Registered hook functions
Note: The documentation in this section applies to every hook unless the
hook-specific documentation says otherwise.
### Arguments
Hook functions are called with three arguments:
1. `hookName` - The name of the hook being invoked.
2. `context` - An object with some relevant information about the context of the
call. See the hook-specific documentation for details.
3. callback - Function to call when done. This callback takes a single argument,
the meaning of which depends on the hook. See the "Return values" section for
general information that applies to most hooks. The value returned by this
callback must be returned by the hook function unless otherwise specified.
3. `cb` - For asynchronous operations this callback can be called to signal
completion and optionally provide a return value. The callback takes a single
argument, the meaning of which depends on the hook (see the "Return values"
section for general information that applies to most hooks). This callback
always returns `undefined`.
## Return values
### Expected behavior
Note: This section applies to every hook unless the hook-specific documentation
says otherwise.
The presence of a callback parameter suggests that every hook function can run
asynchronously. While that is the eventual goal, there are some legacy hooks
that expect their hook functions to provide a value synchronously. For such
hooks, the hook functions must do one of the following:
Hook functions return zero or more values to Etherpad by passing an array to the
provided callback. Hook functions typically provide a single value (array of
length one). If the function does not want to or need to provide a value, it may
pass an empty array or `undefined` (which is treated the same as an empty
array). Hook functions may also provide more than one value (array of length two
or more).
* Call the callback with a non-Promise value (`undefined` is acceptable) and
return `undefined`, in that order.
* Return a non-Promise value other than `undefined` (`null` is acceptable) and
never call the callback. Note that `async` functions *always* return a
Promise, so they must never be used for synchronous hooks.
* Only have two parameters (`hookName` and `context`) and return any non-Promise
value (`undefined` is acceptable).
Some hooks concatenate the arrays provided by its registered functions. For
example, if a hook's registered functions pass `[1, 2]`, `undefined`, `[3, 4]`,
`[]`, and `[5]` to the provided callback, then the hook's return value is `[1,
2, 3, 4, 5]`.
For hooks that permit asynchronous behavior, the hook functions must do one or
more of the following:
Other hooks only use the first non-empty array provided by a registered
function. In this case, each of the hook's registered functions is called one at
a time until one provides a non-empty array. The remaining functions are
skipped. If none of the functions provide a non-empty array, or there are no
registered functions, the hook's return value is `[]`.
* Return `undefined` and call the callback, in either order.
* Return something other than `undefined` (`null` is acceptable) and never call
the callback. Note that `async` functions *always* return a Promise, so they
must never call the callback.
* Only have two parameters (`hookName` and `context`).
Example:
Note that the acceptable behaviors for asynchronous hook functions is a superset
of the acceptable behaviors for synchronous hook functions.
```
exports.abstractHook = (hookName, context, callback) => {
if (notApplicableToThisPlugin(context)) {
return callback();
}
const value = doSomeProcessing(context);
return callback([value]);
WARNING: The number of parameters is determined by examining
[Function.length](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/length),
which does not count [default
parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters)
or ["rest"
parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters).
To avoid problems, do not use default or rest parameters when defining hook
functions.
### Return values
A hook function can provide a value to Etherpad in one of the following ways:
* Pass the desired value as the first argument to the callback.
* Return the desired value directly. The value must not be `undefined` unless
the hook function only has two parameters. (Hook functions with three
parameters that want to provide `undefined` should instead use the callback.)
* For hooks that permit asynchronous behavior, return a Promise that resolves to
the desired value.
* For hooks that permit asynchronous behavior, pass a Promise that resolves to
the desired value as the first argument to the callback.
Examples:
```javascript
exports.exampleOne = (hookName, context, callback) => {
return 'valueOne';
};
exports.exampleTwo = (hookName, context, callback) => {
callback('valueTwo');
return;
};
// ONLY FOR HOOKS THAT PERMIT ASYNCHRONOUS BEHAVIOR
exports.exampleThree = (hookName, context, callback) => {
return new Promise('valueThree');
};
// ONLY FOR HOOKS THAT PERMIT ASYNCHRONOUS BEHAVIOR
exports.exampleFour = (hookName, context, callback) => {
callback(new Promise('valueFour'));
return;
};
// ONLY FOR HOOKS THAT PERMIT ASYNCHRONOUS BEHAVIOR
exports.exampleFive = async (hookName, context) => {
// Note that this function is async, so it actually returns a Promise that
// is resolved to 'valueFive'.
return 'valueFive';
};
```
Etherpad collects the values provided by the hook functions into an array,
filters out all `undefined` values, then flattens the array one level.
Flattening one level makes it possible for a hook function to behave as if it
were multiple separate hook functions.
For example: Suppose a hook has eight registered functions that return the
following values: `1`, `[2]`, `['3a', '3b']` `[[4]]`, `undefined`,
`[undefined]`, `[]`, and `null`. The value returned to the caller of the hook is
`[1, 2, '3a', '3b', [4], undefined, null]`.