/**
* @module shared/extendPromise
*/
/**
* Helper class that creates a proxy for a {@link Promise} in order to add custom
* properties.
*
* The only reason this class exists is so it can "scope" the necessary methods to extend
* {@link Promise} and avoid workarounds in order to declare them, as they need to call
* themselves recursively.
*
* @parent module:shared/extendPromise
* @tutorial extendPromise
*/
class PromiseExtender {
/**
* @param {Promise} promise The promise to extend.
* @param {Object.<string, any>} properties A dictionary of custom properties to
* _inject_ in the promise chain.
*/
constructor(promise, properties) {
/**
* The proxied promise.
*
* @type {Proxy<Promise>}
* @access private
* @ignore
*/
this._promise = this._extend(promise, properties);
}
/**
* The extended promise.
*
* @type {Proxy<Promise>}
*/
get promise() {
return this._promise;
}
/**
* The method that actually extends a promise: it creates a proxy of the promise in
* order to intercept the getters so it can return the custom properties if requested,
* and return new proxies when `then`, `catch` and `finally` are called; the reason new
* proxies are created is because those methods return new promises, and without being
* proxied, the custom properties would be lost.
*
* @param {Promise} promise The promise to proxy.
* @param {Object} properties A dictionary of custom properties to _inject_ in the
* promise chain.
* @returns {Proxy<Promise>}
* @throws {Error} If `promise` is not a valid instance of {@link Promise}.
* @throws {Error} If `properties` is not an object or if it doesn't have any
* properties.
* @access private
* @ignore
*/
_extend(promise, properties) {
if (!(promise instanceof Promise)) {
throw new Error("'promise' must be a valid Promise instance");
} else if (!properties || !Object.keys(properties).length) {
throw new Error("'properties' must be an object with at least one key");
}
return new Proxy(promise, {
/**
* This is a trap for when something is trying to read/access a property for the
* promise.
* The function first validates if it's one of the functions
* (`then`/`catch`/`finally`) in order to return a proxied function; then, if it's
* another function (one that doesn't return another promise), it just calls the
* original; finally, before doing a fallback to the original promise, it checks if
* it's one of the custom properties that exented the promise.
*
* @param {Promise} target The original promise.
* @param {string} name The name of the property.
* @returns {*}
*/
get: (target, name) => {
let result;
if (['then', 'catch', 'finally'].includes(name)) {
result = this._extendFunction(target[name].bind(target), properties);
} else if (target[name] && typeof target[name].bind === 'function') {
result = target[name].bind(target);
} else if (properties[name]) {
result = properties[name];
} else {
result = target[name];
}
return result;
},
});
}
/**
* Creates a proxy for a promise function (`then`/`catch`/`finally`) so the returned
* promise can also be extended.
*
* @param {Function} fn The promise function to proxy.
* @param {Object} properties A dictionary of custom properties to _inject_ in the
* promise chain.
* @returns {Proxy<Function>}
* @access private
* @ignore
*/
_extendFunction(fn, properties) {
return new Proxy(fn, {
/**
* This is a trap for when a function gets called (remember this gets used for
* `then`/
* `catch`/`finally`); it processes the result using the oringinal function and
* since promise methods return a promise, instead of returning the original result,
* it returns an _extended_ version of it.
*
* @param {Function} target The original function.
* @param {Promise} thisArg The promise the function belongs to.
* @param {Array} args The list of arguments sent to the trap.
* @returns {Promise}
*/
apply: (target, thisArg, args) => {
const value = target.bind(thisArg)(...args);
return this._extend(value, properties);
},
});
}
}
/**
* Extends a {@link Promise} by injecting custom properties using a {@link Proxy}. The
* custom properties will be available on the promise chain no matter how many `then`s,
* `catch`s or `finally`s are added.
*
* @param {Promise} promise The promise to extend.
* @param {Object} properties A dictionary of custom properties to _inject_ in the
* promise chain.
* @returns {Proxy<Promise>}
* @throws {Error} If `promise` is not a valid instance of {@link Promise}.
* @throws {Error} If `properties` is not an object or if it doesn't have any
* properties.
* @tutorial extendPromise
*/
const extendPromise = (promise, properties) =>
new PromiseExtender(promise, properties).promise;
module.exports = extendPromise;