@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
475 lines (435 loc) • 14.8 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2021 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
sap.ui.define([], function () {
"use strict";
var oResolved = new SyncPromise(function (resolve, reject) {
resolve();
}); // a SyncPromise which is resolved w/o arguments
/*
* @see https://promisesaplus.com
*
* 2.3.3.3. If then is a function, call it with x as this, first argument resolvePromise, and
* second argument rejectPromise, where:
* 2.3.3.3.1. If/when resolvePromise is called with a value y, run [[Resolve]](promise, y).
* 2.3.3.3.2. If/when rejectPromise is called with a reason r, reject promise with r.
* 2.3.3.3.3. If both resolvePromise and rejectPromise are called, or multiple calls to the same
* argument are made, the first call takes precedence, and any further calls are ignored.
* 2.3.3.3.4. If calling then throws an exception e,
* 2.3.3.3.4.1. If resolvePromise or rejectPromise have been called, ignore it.
* 2.3.3.3.4.2. Otherwise, reject promise with e as the reason.
*
* @param {function} fnThen
* The "then" function
* @param {function} resolve
* The [[Resolve]](promise, .) function
* @param {function} reject
* The "reject" function
*/
function call(fnThen, resolve, reject) {
var bOnce;
/*
* @param {any} [vReason]
* The reason for rejection
*/
function rejectPromise(vReason) {
if (!bOnce) {
bOnce = true;
reject(vReason);
}
}
/*
* @param {any} [vResult]
* The thenable to wrap or the result to synchronously fulfill with
*/
function resolvePromise(vResult) {
if (!bOnce) {
bOnce = true;
resolve(vResult);
}
}
try {
fnThen(resolvePromise, rejectPromise);
} catch (e) {
rejectPromise(e);
}
}
/**
* Tells whether the given value is a function or object with a "then" property. These are the
* candidates for "thenables".
*
* @param {any} vValue
* Any value
* @returns {boolean}
* See above
*/
function hasThen(vValue) {
return vValue && (typeof vValue === "function" || typeof vValue === "object")
&& "then" in vValue;
}
/**
* Constructor for a {@link sap.ui.base.SyncPromise} which may wrap a thenable (e.g. native
* <code>Promise</code>) in order to observe settlement and later provide synchronous access to
* the result.
*
* Implements https://promisesaplus.com except "2.2.4. onFulfilled or onRejected must not be
* called until the execution context stack contains only platform code."
*
* @param {function} fnExecutor
* A function that is passed with the arguments resolve and reject...
*
* @private
* @ui5-restricted sap.ui.core,sap.ui.dt,sap.ui.model
*/
function SyncPromise(fnExecutor) {
var bCaught = false,
iState, // undefined: pending, -1: rejected, 0: resolved but pending, 1: fulfilled
fnReject,
fnResolve,
vResult,
that = this;
/*
* @param {any} [vReason]
* The reason for rejection
*/
function reject(vReason) {
vResult = vReason;
iState = -1;
if (!bCaught && SyncPromise.listener) {
SyncPromise.listener(that, false);
}
if (fnReject) {
fnReject(vReason);
fnReject = fnResolve = null; // be nice to the garbage collector
}
}
/*
* @param {any} [vResult0]
* The thenable to wrap or the result to synchronously fulfill with
*/
function resolve(vResult0) {
var fnThen;
if (vResult0 === that) {
reject(new TypeError("A promise cannot be resolved with itself."));
return;
}
if (vResult0 instanceof SyncPromise) {
if (vResult0.isFulfilled()) {
resolve(vResult0.getResult());
return;
} else if (vResult0.isRejected()) {
vResult0.caught(); // might have been uncaught so far
reject(vResult0.getResult());
return;
} else {
vResult0.caught(); // make sure it will never count as uncaught
vResult0 = vResult0.getResult(); // unwrap to access native thenable
}
}
iState = 0;
vResult = vResult0;
if (hasThen(vResult)) {
try {
fnThen = vResult.then;
} catch (e) {
// 2.3.3.2. If retrieving the property x.then results in a thrown exception e,
// reject promise with e as the reason.
reject(e);
return;
}
if (typeof fnThen === "function") {
call(fnThen.bind(vResult), resolve, reject);
return;
}
}
iState = 1;
if (fnResolve) {
fnResolve(vResult);
fnReject = fnResolve = null; // be nice to the garbage collector
}
}
/**
* Marks this {@link sap.ui.base.SyncPromise} as caught and informs the optional
* {@link sap.ui.base.SyncPromise.listener}. Basically, it has the same effect as
* {@link #catch}, but with less overhead. Use it together with {@link #isRejected} and
* {@link #getResult} in cases where the rejection is turned into <code>throw</code>; or
* simply use {@link #unwrap} instead.
*/
this.caught = function () {
if (!bCaught) {
bCaught = true; // MUST NOT become uncaught later on!
if (SyncPromise.listener && this.isRejected()) {
SyncPromise.listener(this, true);
}
}
};
/**
* Returns the current "result" of this {@link sap.ui.base.SyncPromise}.
*
* @returns {any}
* The result in case this {@link sap.ui.base.SyncPromise} is already fulfilled, the
* reason if it is already rejected, or the wrapped thenable if it is still pending
*/
this.getResult = function () {
return vResult;
};
/**
* Tells whether this {@link sap.ui.base.SyncPromise} is fulfilled.
*
* @returns {boolean}
* Whether this {@link sap.ui.base.SyncPromise} is fulfilled
*/
this.isFulfilled = function () {
return iState === 1;
};
/**
* Tells whether this {@link sap.ui.base.SyncPromise} is still pending.
*
* @returns {boolean}
* Whether this {@link sap.ui.base.SyncPromise} is still pending
*/
this.isPending = function () {
return !iState;
};
/**
* Tells whether this {@link sap.ui.base.SyncPromise} is rejected.
*
* @returns {boolean}
* Whether this {@link sap.ui.base.SyncPromise} is rejected
*/
this.isRejected = function () {
return iState === -1;
};
call(fnExecutor, resolve, reject);
if (iState === undefined) {
// make sure we wrap a native Promise while pending
vResult = new Promise(function (resolve, reject) {
fnResolve = resolve;
fnReject = reject;
});
vResult.catch(function () {}); // avoid "Uncaught (in promise)"
}
}
/**
* Returns a {@link sap.ui.base.SyncPromise} and deals with rejected cases only. Same as
* <code>then(undefined, fnOnRejected)</code>.
*
* @param {function} [fnOnRejected]
* Callback function if this {@link sap.ui.base.SyncPromise} is rejected
* @returns {sap.ui.base.SyncPromise}
* A new {@link sap.ui.base.SyncPromise}, or <code>this</code> in case it is settled and no
* corresponding callback function is given
*
* @see #then
*/
SyncPromise.prototype.catch = function (fnOnRejected) {
return this.then(undefined, fnOnRejected);
};
/**
* Returns a {@link sap.ui.base.SyncPromise} and calls the given handler, like
* <code>Promise.prototype.finally</code>.
*
* @param {function} [fnOnFinally]
* Callback function if this {@link sap.ui.base.SyncPromise} is settled
* @returns {sap.ui.base.SyncPromise}
* A new {@link sap.ui.base.SyncPromise}, or <code>this</code> in case it is settled and no
* callback function is given
*
* @see #then
*/
SyncPromise.prototype.finally = function (fnOnFinally) {
if (typeof fnOnFinally === "function") {
return this.then(function (vResult) {
return SyncPromise.resolve(fnOnFinally()).then(function () {
return vResult;
}).unwrap(); // Note: avoids unnecessary micro task
}, function (vReason) {
return SyncPromise.resolve(fnOnFinally()).then(function () {
throw vReason;
}).unwrap(); // Note: avoids unnecessary micro task
});
}
return this.then(fnOnFinally, fnOnFinally);
};
/**
* Returns a {@link sap.ui.base.SyncPromise} and calls the given handler as applicable, like
* <code>Promise.prototype.then</code>. This {@link sap.ui.base.SyncPromise} is marked as
* {@link #caught} unless <code>this</code> is returned. Note that a new
* {@link sap.ui.base.SyncPromise} returned from this method may already be rejected, but not
* yet caught.
*
* @param {function} [fnOnFulfilled]
* Callback function if this {@link sap.ui.base.SyncPromise} is fulfilled
* @param {function} [fnOnRejected]
* Callback function if this {@link sap.ui.base.SyncPromise} is rejected
* @returns {sap.ui.base.SyncPromise}
* A new {@link sap.ui.base.SyncPromise}, or <code>this</code> in case it is settled and no
* corresponding callback function is given
*/
SyncPromise.prototype.then = function (fnOnFulfilled, fnOnRejected) {
var fnCallback = this.isFulfilled() ? fnOnFulfilled : fnOnRejected,
bCallbackIsFunction = typeof fnCallback === "function",
bPending = this.isPending(),
that = this;
if (bPending || bCallbackIsFunction) {
this.caught();
} // else: returns this
if (!bPending) {
return bCallbackIsFunction
? new SyncPromise(function (resolve, reject) {
resolve(fnCallback(that.getResult())); // Note: try/catch is present in c'tor!
})
: this;
}
return SyncPromise.resolve(this.getResult().then(fnOnFulfilled, fnOnRejected));
};
/**
* Returns a string representation of this {@link sap.ui.base.SyncPromise}. If it is resolved, a
* string representation of the result is returned; if it is rejected, a string representation
* of the reason is returned.
*
* @return {string} A string description of this {@link sap.ui.base.SyncPromise}
*/
SyncPromise.prototype.toString = function () {
if (this.isPending()) {
return "SyncPromise: pending";
}
return String(this.getResult());
};
/**
* Unwraps this {@link sap.ui.base.SyncPromise} by returning the current result if this promise
* is already fulfilled, returning the wrapped thenable if this promise is still pending, or
* throwing the reason if this promise is already rejected. This {@link sap.ui.base.SyncPromise}
* is marked as {@link #caught}.
*
* @returns {any|Promise}
* The result in case this {@link sap.ui.base.SyncPromise} is already fulfilled, or the
* wrapped thenable if this promise is still pending
* @throws {any}
* The reason if this promise is already rejected
*
* @see #getResult
*/
SyncPromise.prototype.unwrap = function () {
this.caught(); // make sure it will never count as uncaught
if (this.isRejected()) {
throw this.getResult();
}
return this.getResult();
};
/**
* Returns a new {@link sap.ui.base.SyncPromise} for the given array of values just like
* <code>Promise.all(aValues)</code>.
*
* @param {any[]} aValues
* The values as an iterable object such as an <code>Array</code> or <code>String</code>
* which is supported by <code>Array.prototype.slice</code>
* @returns {sap.ui.base.SyncPromise}
* The {@link sap.ui.base.SyncPromise}
*/
SyncPromise.all = function (aValues) {
return new SyncPromise(function (resolve, reject) {
var bDone = false,
iPending = 0; // number of pending promises
function checkFulfilled() {
if (bDone && iPending === 0) {
resolve(aValues); // Note: 1st reject/resolve wins!
}
}
aValues = Array.prototype.slice.call(aValues);
aValues.forEach(function (vValue, i) {
if (vValue !== aValues[i + 1] && hasThen(vValue)) { // do s.th. at end of run only
iPending += 1;
vValue.then(function (vResult0) {
do {
aValues[i] = vResult0;
i -= 1;
} while (i >= 0 && vValue === aValues[i]);
iPending -= 1;
checkFulfilled();
}, function (vReason) {
reject(vReason); // Note: 1st reject/resolve wins!
});
}
});
bDone = true;
checkFulfilled();
});
};
/**
* Tells whether the given value is a function or object with a "then" property which can be
* retrieved without an exception being thrown and which is a function.
*
* @param {any} vValue
* Any value
* @returns {boolean}
* See above
*
* @see step 2.3.3. of https://promisesaplus.com
*/
SyncPromise.isThenable = function (vValue) {
// "typeof vValue.then" returns "unknown" in IE if the getter for "then" throws an error; to
// get 100% code coverage also in IE assign vValue.then to a variable before using typeof
var fnThen;
try {
return !!hasThen(vValue) && (fnThen = vValue.then) && typeof fnThen === "function";
} catch (e) {
// "2.3.3.2. If retrieving the property x.then results in a thrown exception e,..."
// ...we should not call this a proper "thenable"
return false;
}
};
/**
* Optional listener function which is called with a {@link sap.ui.base.SyncPromise} instance
* and a boolean flag telling whether that instance became "caught" or not. An instance becomes
* "uncaught" as soon as it is rejected and not yet "caught". It becomes "caught" as soon as an
* "fnOnRejected" handler is given to {@link #then} for the first time.
*
* @abstract
* @function
* @name sap.ui.base.SyncPromise.listener
* @param {sap.ui.base.SyncPromise} oSyncPromise
* A rejected {@link sap.ui.base.SyncPromise}
* @param {boolean} bCaught
* <code>false</code> if the {@link sap.ui.base.SyncPromise} instance just became "uncaught",
* <code>true</code> if it just became "caught"
*/
/**
* Returns a new {@link sap.ui.base.SyncPromise} that is rejected with the given reason.
*
* @param {any} [vReason]
* The reason for rejection
* @returns {sap.ui.base.SyncPromise}
* The {@link sap.ui.base.SyncPromise}
*/
SyncPromise.reject = function (vReason) {
return new SyncPromise(function (resolve, reject) {
reject(vReason);
});
};
/**
* Returns <code>vResult</code> if it is already a {@link sap.ui.base.SyncPromise}, or a new
* {@link sap.ui.base.SyncPromise} wrapping the given thenable <code>vResult</code> or
* fulfilling with the given result. In case <code>vResult === undefined</code>, the same
* instance is reused to improve performance.
*
* @param {any} [vResult]
* The thenable to wrap or the result to synchronously fulfill with
* @returns {sap.ui.base.SyncPromise}
* The {@link sap.ui.base.SyncPromise}
*/
SyncPromise.resolve = function (vResult) {
if (vResult === undefined) {
return oResolved;
}
if (vResult instanceof SyncPromise) {
return vResult;
}
return new SyncPromise(function (resolve, reject) {
resolve(vResult);
});
};
return SyncPromise;
}, /* bExport= */ true);