@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
265 lines (255 loc) • 9.81 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.
*/
/*
* IMPORTANT: This is a private module, its API must not be used and is subject to change.
* Code other than the OpenUI5 libraries must not introduce dependencies to this module.
*/
/*global Proxy */
sap.ui.define([], function() {
"use strict";
/**
* @function
* @since 1.58
* @alias module:sap/ui/base/syncXHRFix
* @private
*/
var fnXHRFix = function() {
// Firefox has an issue with synchronous and asynchronous requests running in parallel,
// where callbacks of the asynchronous call are executed while waiting on the synchronous
// response, see https://bugzilla.mozilla.org/show_bug.cgi?id=697151
// In UI5 in some cases it happens that application code is running, while the class loading
// is still in process, so classes cannot be found. To overcome this issue we create a proxy
// of the XHR object, which delays execution of the asynchronous event handlers, until
// the synchronous request is completed.
(function() {
var bSyncRequestOngoing = false,
bPromisesQueued = false;
// Overwrite setTimeout and Promise handlers to delay execution after
// synchronous request is completed
var _then = Promise.prototype.then,
_catch = Promise.prototype.catch,
_timeout = window.setTimeout,
_interval = window.setInterval,
aQueue = [];
function addPromiseHandler(fnHandler) {
// Collect all promise handlers and execute within the same timeout,
// to avoid them to be split among several tasks
if (!bPromisesQueued) {
bPromisesQueued = true;
_timeout(function() {
var aCurrentQueue = aQueue;
aQueue = [];
bPromisesQueued = false;
aCurrentQueue.forEach(function(fnQueuedHandler) {
fnQueuedHandler();
});
}, 0);
}
aQueue.push(fnHandler);
}
function wrapPromiseHandler(fnHandler, oScope, bCatch) {
if (typeof fnHandler !== "function") {
return fnHandler;
}
return function() {
var aArgs = Array.prototype.slice.call(arguments);
// If a sync request is ongoing or other promises are still queued,
// the execution needs to be delayed
if (bSyncRequestOngoing || bPromisesQueued) {
return new Promise(function(resolve, reject) {
// The try catch is needed to differentiate whether resolve or
// reject needs to be called.
addPromiseHandler(function() {
var oResult;
try {
oResult = fnHandler.apply(window, aArgs);
resolve(oResult);
} catch (oException) {
reject(oException);
}
});
});
}
return fnHandler.apply(window, aArgs);
};
}
/*eslint-disable no-extend-native*/
Promise.prototype.then = function(fnThen, fnCatch) {
var fnWrappedThen = wrapPromiseHandler(fnThen),
fnWrappedCatch = wrapPromiseHandler(fnCatch);
return _then.call(this, fnWrappedThen, fnWrappedCatch);
};
Promise.prototype.catch = function(fnCatch) {
var fnWrappedCatch = wrapPromiseHandler(fnCatch);
return _catch.call(this, fnWrappedCatch);
};
/*eslint-enable no-extend-native*/
// If there are promise handlers waiting for execution at the time the
// timeout fires, start another timeout to postpone timer execution after
// promise execution.
function wrapTimerHandler(vHandler) {
var fnWrappedHandler = function() {
var aArgs, fnHandler;
if (bPromisesQueued) {
aArgs = [fnWrappedHandler, 0].concat(arguments);
_timeout.apply(window, aArgs);
} else {
fnHandler = typeof vHandler !== "function" ? new Function(vHandler) : vHandler; // eslint-disable-line no-new-func
fnHandler.apply(window, arguments);
}
};
return fnWrappedHandler;
}
// setTimeout and setInterval can have arbitrary number of additional
// parameters, which are passed to the handler function when invoked.
window.setTimeout = function(vHandler) {
var aArgs = Array.prototype.slice.call(arguments);
if (aArgs.length !== 0) {
aArgs[0] = wrapTimerHandler(vHandler);
}
return _timeout.apply(window, aArgs);
};
window.setInterval = function(vHandler) {
var aArgs = Array.prototype.slice.call(arguments);
if (aArgs.length !== 0) {
aArgs[0] = wrapTimerHandler(vHandler);
}
return _interval.apply(window, aArgs);
};
// Replace the XMLHttpRequest object with a proxy, that overrides the constructor to
// return a proxy of the XHR instance
window.XMLHttpRequest = new Proxy(window.XMLHttpRequest, {
construct: function(oTargetClass, aArguments, oNewTarget) {
var oXHR = new oTargetClass(),
bSync = false,
bDelay = false,
iReadyState = 0,
oProxy;
// Return a wrapped handler function for the given function, which checks
// whether a synchronous request is currently in progress.
function wrapHandler(fnHandler) {
var fnWrappedHandler = function(oEvent) {
// The ready state at the time the event is occurring needs to
// be preserved, to restore it when the handler is called delayed
var iCurrentState = oXHR.readyState;
function callHandler() {
iReadyState = iCurrentState;
// Only if the event has not been removed in the meantime
// the handler needs to be called after the timeout
if (fnWrappedHandler.active) {
return fnHandler.call(oProxy, oEvent);
}
}
// If this is an asynchronous request and a sync request is ongoing,
// the execution of all following handler calls needs to be delayed
if (!bSync && bSyncRequestOngoing) {
bDelay = true;
}
if (bDelay) {
_timeout(callHandler, 0);
return true;
}
return callHandler();
};
fnHandler.wrappedHandler = fnWrappedHandler;
fnWrappedHandler.active = true;
return fnWrappedHandler;
}
// To be able to remove an event listener, we need to get access to the
// wrapped handler, which has been used to add the listener internally
// in the XHR.
function unwrapHandler(fnHandler) {
return deactivate(fnHandler.wrappedHandler);
}
// When an event handler is removed synchronously, it needs to be deactivated
// to avoid the situation, where the handler has been triggered while
// the sync request was ongoing, but removed afterwards.
function deactivate(fnWrappedHandler) {
if (typeof fnWrappedHandler === "function") {
fnWrappedHandler.active = false;
}
return fnWrappedHandler;
}
// Create a proxy of the XHR instance, which overrides the necessary functions
// to deal with event handlers and readyState
oProxy = new Proxy(oXHR, {
get: function(oTarget, sPropName, oReceiver) {
var vProp = oTarget[sPropName];
switch (sPropName) {
// When an event handler is called with setTimeout, the readyState
// of the internal XHR is already completed, but we need to have
// have the readyState at the time the event was fired.
case "readyState":
return iReadyState;
// When events are added, the handler function needs to be wrapped
case "addEventListener":
return function(sName, fnHandler, bCapture) {
vProp.call(oTarget, sName, wrapHandler(fnHandler), bCapture);
};
// When events are removed, the wrapped handler function must be used,
// to remove it on the internal XHR object
case "removeEventListener":
return function(sName, fnHandler, bCapture) {
vProp.call(oTarget, sName, unwrapHandler(fnHandler), bCapture);
};
// Whether a request is asynchronous or synchronous is defined when
// calling the open method.
case "open":
return function(sMethod, sUrl, bAsync) {
bSync = bAsync === false;
vProp.apply(oTarget, arguments);
iReadyState = oTarget.readyState;
};
// The send method is where the actual request is triggered. For sync
// requests we set a boolean flag to detect a request is in progress
// in the wrapped handlers.
case "send":
return function() {
bSyncRequestOngoing = bSync;
try {
vProp.apply(oTarget, arguments);
} finally {
iReadyState = oTarget.readyState;
bSyncRequestOngoing = false;
}
};
}
// All functions need to be wrapped, so they are called on the correct object
// instance
if (typeof vProp === "function") {
return function() {
return vProp.apply(oTarget, arguments);
};
}
// All other properties can just be returned
return vProp;
},
set: function(oTarget, sPropName, vValue) {
// All properties starting with "on" (event handler functions) need to be wrapped
// when they are set
if (sPropName.indexOf("on") === 0) {
// In case there already is a function set on this property, it needs to be
// deactivated
deactivate(oTarget[sPropName]);
if (typeof vValue === "function") {
oTarget[sPropName] = wrapHandler(vValue);
return true;
}
}
// All other properties can just be set on the inner XHR object
oTarget[sPropName] = vValue;
return true;
}
});
// add dummy readyStateChange listener to make sure readyState is updated properly
oProxy.addEventListener("readystatechange", function() {});
return oProxy;
}
});
})();
};
return fnXHRFix;
});