Skip to content

Commit

Permalink
Install Hoist extensions on both "real" and Edge "native" Promises
Browse files Browse the repository at this point in the history
+ Fixes #1411 - maybe for real this time
  • Loading branch information
amcclain committed Oct 18, 2019
1 parent 12b9105 commit a354044
Showing 1 changed file with 170 additions and 156 deletions.
326 changes: 170 additions & 156 deletions promise/Promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
});
});
};

// 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__);
}

0 comments on commit a354044

Please sign in to comment.