diff --git a/promise/Promise.js b/promise/Promise.js index 1718462b5e..5e9596d397 100644 --- a/promise/Promise.js +++ b/promise/Promise.js @@ -62,160 +62,174 @@ export async function never() { //-------------------------------- // Promise prototype extensions //-------------------------------- -Object.assign(Promise.prototype, { - - /** - * Version of then() that wraps the callback in a MobX action. - * This should be used in a promise chain that modifies MobX observables. - * - * @param {function} [fn] - function appropriate as an argument to `then()`. - */ - thenAction(fn) { - return this.then(action(fn)); - }, - - /** - * Version of catch() that will only catch certain exceptions. - * @see Promise.catch() - * - * @param {function} selector - closure that takes an exception and returns a boolean. - * May also be specified as a list of exceptions names to be handled. - * Only exceptions passing this selector will be handled by this method. - * @param {function} [fn] - */ - catchWhen(selector, fn) { - return this.catch(e => { - this.throwIfFailsSelector(e, selector); - return fn ? fn(e) : undefined; - }); - }, - - /** - * Version of catch() that will invoke default application exception handling. - * Typically called in last line in promise chain. - * - * @param {Object} [options] - options suitable for passing to XH.handleException(). - */ - catchDefault(options) { - return this.catch(e => XH.handleException(e, options)); - }, - - /** - * Version of catchDefault() that will only catch certain exceptions. - * - * @param {function} selector - see catchWhen(). - * @param {Object} [options] - options suitable for passing to XH.handleException(). - */ - catchDefaultWhen(selector, options) { - return this.catch(e => { - this.throwIfFailsSelector(e, selector); - return XH.handleException(e, options); - }); - }, - - /** - * Track a Promise. - * @see TrackService.track() - * - * @param {Object} [trackCfg] - valid options object for TrackService.track(). - * @param {boolean} [trackCfg.omit] - optional to indicate when no tracking will be performed - * (useful when trackCfg conditionally generated - i.e. to suppress tracking for - * auto-refresh calls triggered by a Timer). - */ - track(trackCfg) { - if (!trackCfg || trackCfg.omit) return this; - - if (typeof trackCfg === 'string') { - trackCfg = {message: trackCfg}; +const enhancePromise = (promisePrototype) => { + Object.assign(promisePrototype, { + + /** + * Version of then() that wraps the callback in a MobX action. + * This should be used in a promise chain that modifies MobX observables. + * + * @param {function} [fn] - function appropriate as an argument to `then()`. + */ + thenAction(fn) { + return this.then(action(fn)); + }, + + /** + * Version of catch() that will only catch certain exceptions. + * @see Promise.catch() + * + * @param {function} selector - closure that takes an exception and returns a boolean. + * May also be specified as a list of exceptions names to be handled. + * Only exceptions passing this selector will be handled by this method. + * @param {function} [fn] + */ + catchWhen(selector, fn) { + return this.catch(e => { + this.throwIfFailsSelector(e, selector); + return fn ? fn(e) : undefined; + }); + }, + + /** + * Version of catch() that will invoke default application exception handling. + * Typically called in last line in promise chain. + * + * @param {Object} [options] - options suitable for passing to XH.handleException(). + */ + catchDefault(options) { + return this.catch(e => XH.handleException(e, options)); + }, + + /** + * Version of catchDefault() that will only catch certain exceptions. + * + * @param {function} selector - see catchWhen(). + * @param {Object} [options] - options suitable for passing to XH.handleException(). + */ + catchDefaultWhen(selector, options) { + return this.catch(e => { + this.throwIfFailsSelector(e, selector); + return XH.handleException(e, options); + }); + }, + + /** + * Track a Promise. + * @see TrackService.track() + * + * @param {Object} [trackCfg] - valid options object for TrackService.track(). + * @param {boolean} [trackCfg.omit] - optional to indicate when no tracking will be performed + * (useful when trackCfg conditionally generated - i.e. to suppress tracking for + * auto-refresh calls triggered by a Timer). + */ + track(trackCfg) { + if (!trackCfg || trackCfg.omit) return this; + + if (typeof trackCfg === 'string') { + trackCfg = {message: trackCfg}; + } + + const startTime = Date.now(); + + return this.finally(() => { + trackCfg.elapsed = Date.now() - startTime; + XH.track(trackCfg); + }); + }, + + /** + * Wait on a potentially async function, before passing the originally received value through. + * Useful when we want to block and do something on the promise chain, but do not want to + * manipulate the values being passed through. + * + * @param {function} onFulfillment - function to receive the pass-through value when ready. + */ + tap(onFulfillment) { + let ret = null; + const resolveFn = (data) => { + ret = data; + return onFulfillment(data); + }; + + return this + .then(resolveFn) + .then(() => ret); + }, + + /** + * Return a promise that will resolve a specified delay after this promise resolves. + * + * @param {number} interval - delay in milliseconds. + */ + wait(interval) { + return this.finally(() => wait(interval)); + }, + + /** + * Return a promise that will reject if this promise has not been settled after the specified + * interval has passed. + * + * @param {(Object|number)} [config] - object as per below, or interval in ms (if number). + * If null, no timeout enforced. + * @param {number} [config.interval] - interval value in ms. + * @param {string} [config.message] - message for Exception thrown on timeout. + */ + timeout(config) { + if (config == null) return this; + if (isNumber(config)) config = {interval: config}; + config.message = config.message || 'Operation timed out'; + + let completed = false; + const promise = this.finally(() => completed = true); + + const deadline = wait(config.interval).then(() => { + throwIf(!completed, config.message); + }); + + return Promise.race([deadline, promise]); + }, + + + /** + * Link this promise to an instance of a PendingTaskModel. See that class for details on what + * PendingTaskModels provide and how they can be used to coordinate masking and progress + * messages on one or more async operations. + * + * @param {Object|PendingTaskModel} cfg -- Configuration object, or PendingTaskModel + * @param {PendingTaskModel} cfg.model - PendingTaskModel to link to. + * @param {String} [cfg.message] - Optional custom message for use by PendingTaskModel. + * @param {boolean} [cfg.omit] - optional flag to indicate linkage should be skipped + * If true, this method is no-op. Provided as convenience for conditional masking. + */ + linkTo(cfg) { + if (!isPlainObject(cfg)) { + cfg = {model: cfg}; + } + if (cfg.model && !cfg.omit) { + cfg.model.link(this, cfg.message); + } + return this; + }, + + //-------------------------------- + // Implementation + //-------------------------------- + throwIfFailsSelector(e, selector) { + const fn = isFunction(selector) ? selector : (e) => castArray(selector).includes(e.name); + if (!fn(e)) throw e; } - - const startTime = Date.now(); - - return this.finally(() => { - trackCfg.elapsed = Date.now() - startTime; - XH.track(trackCfg); - }); - }, - - /** - * Wait on a potentially async function, before passing the originally received value through. - * Useful when we want to block and do something on the promise chain, but do not want to - * manipulate the values being passed through. - * - * @param {function} onFulfillment - function to receive the pass-through value when ready. - */ - tap(onFulfillment) { - let ret = null; - const resolveFn = (data) => { - ret = data; - return onFulfillment(data); - }; - - return this - .then(resolveFn) - .then(() => ret); - }, - - /** - * Return a promise that will resolve a specified delay after this promise resolves. - * - * @param {number} interval - delay in milliseconds. - */ - wait(interval) { - return this.finally(() => wait(interval)); - }, - - /** - * Return a promise that will reject if this promise has not been settled after the specified - * interval has passed. - * - * @param {(Object|number)} [config] - object as per below, or interval in ms (if number). - * If null, no timeout enforced. - * @param {number} [config.interval] - interval value in ms. - * @param {string} [config.message] - message for Exception thrown on timeout. - */ - timeout(config) { - if (config == null) return this; - if (isNumber(config)) config = {interval: config}; - config.message = config.message || 'Operation timed out'; - - let completed = false; - const promise = this.finally(() => completed = true); - - const deadline = wait(config.interval).then(() => { - throwIf(!completed, config.message); - }); - - return Promise.race([deadline, promise]); - }, - - - /** - * Link this promise to an instance of a PendingTaskModel. See that class for details on what - * PendingTaskModels provide and how they can be used to coordinate masking and progress - * messages on one or more async operations. - * - * @param {Object|PendingTaskModel} cfg -- Configuration object, or PendingTaskModel - * @param {PendingTaskModel} cfg.model - PendingTaskModel to link to. - * @param {String} [cfg.message] - Optional custom message for use by PendingTaskModel. - * @param {boolean} [cfg.omit] - optional flag to indicate linkage should be skipped - * If true, this method is no-op. Provided as convenience for conditional masking. - */ - linkTo(cfg) { - if (!isPlainObject(cfg)) { - cfg = {model: cfg}; - } - if (cfg.model && !cfg.omit) { - cfg.model.link(this, cfg.message); - } - return this; - }, - - //-------------------------------- - // Implementation - //-------------------------------- - throwIfFailsSelector(e, selector) { - const fn = isFunction(selector) ? selector : (e) => castArray(selector).includes(e.name); - if (!fn(e)) throw e; - } -}); \ No newline at end of file + }); +}; + +// Enhance canonical Promises. +enhancePromise(Promise.prototype); + +// MS Edge returns a "native Promise" from async functions that won't get the enhancements above. +// Check to see if we're in such an environment and enhance that prototype as well. +// @see https://github.com/xh/hoist-react/issues/1411 +const asyncFnReturn = (async () => {})(); +if (!(asyncFnReturn instanceof Promise)) { + console.debug('"Native" Promise return detected as return from async function - enhancing prototype'); + enhancePromise(asyncFnReturn.__proto__); +}