diff --git a/src/libs/DateUtils.js b/src/libs/DateUtils.js index 5c0171067870..11d7e0c0673f 100644 --- a/src/libs/DateUtils.js +++ b/src/libs/DateUtils.js @@ -46,6 +46,12 @@ Onyx.connect({ }, }); +let networkTimeSkew = 0; +Onyx.connect({ + key: ONYXKEYS.NETWORK, + callback: (val) => (networkTimeSkew = lodashGet(val, 'timeSkew', 0)), +}); + /** * Gets the locale string and setting default locale for date-fns * @@ -307,6 +313,15 @@ function getDBTime(timestamp = '') { return datetime.toISOString().replace('T', ' ').replace('Z', ''); } +/** + * Returns the current time plus skew in milliseconds in the format expected by the database + * + * @returns {String} + */ +function getDBTimeWithSkew() { + return getDBTime(new Date().valueOf() + networkTimeSkew); +} + /** * @param {String} dateTime * @param {Number} milliseconds @@ -383,6 +398,7 @@ const DateUtils = { setTimezoneUpdated, getMicroseconds, getDBTime, + getDBTimeWithSkew, subtractMillisecondsFromDateTime, getDateStringFromISOTimestamp, getStatusUntilDate, diff --git a/src/libs/HttpUtils.js b/src/libs/HttpUtils.js index 5a8185a03038..465d22760837 100644 --- a/src/libs/HttpUtils.js +++ b/src/libs/HttpUtils.js @@ -5,6 +5,7 @@ import ONYXKEYS from '../ONYXKEYS'; import HttpsError from './Errors/HttpsError'; import * as ApiUtils from './ApiUtils'; import alert from '../components/Alert'; +import * as NetworkActions from './actions/Network'; let shouldFailAllRequests = false; let shouldForceOffline = false; @@ -22,6 +23,16 @@ Onyx.connect({ // We use the AbortController API to terminate pending request in `cancelPendingRequests` let cancellationController = new AbortController(); +/** + * The API commands that require the skew calculation + */ +const addSkewList = ['OpenReport', 'ReconnectApp', 'OpenApp']; + +/** + * Regex to get API command from the command + */ +const regex = /[?&]command=([^&]+)/; + /** * Send an HTTP request, and attempt to resolve the json response. * If there is a network error, we'll set the application offline. @@ -33,12 +44,25 @@ let cancellationController = new AbortController(); * @returns {Promise} */ function processHTTPRequest(url, method = 'get', body = null, canCancel = true) { + const startTime = new Date().valueOf(); + return fetch(url, { // We hook requests to the same Controller signal, so we can cancel them all at once signal: canCancel ? cancellationController.signal : undefined, method, body, }) + .then((response) => { + const match = url.match(regex)[1]; + if (addSkewList.includes(match) && response.headers) { + const serverTime = new Date(response.headers.get('Date')).valueOf(); + const endTime = new Date().valueOf(); + const latency = (endTime - startTime) / 2; + const skew = serverTime - startTime + latency; + NetworkActions.setTimeSkew(skew); + } + return response; + }) .then((response) => { // Test mode where all requests will succeed in the server, but fail to return a response if (shouldFailAllRequests || shouldForceOffline) { diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 475c1a8bcb8a..19969a803382 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1921,7 +1921,7 @@ function buildOptimisticAddCommentReportAction(text, file) { ], automatic: false, avatar: lodashGet(allPersonalDetails, [currentUserAccountID, 'avatar'], UserUtils.getDefaultAvatarURL(currentUserAccountID)), - created: DateUtils.getDBTime(), + created: DateUtils.getDBTimeWithSkew(), message: [ { translationKey: isAttachment ? CONST.TRANSLATION_KEYS.ATTACHMENT : '', diff --git a/src/libs/actions/Network.ts b/src/libs/actions/Network.ts index 212e44f6782d..fc83d23ac4a7 100644 --- a/src/libs/actions/Network.ts +++ b/src/libs/actions/Network.ts @@ -5,6 +5,10 @@ function setIsOffline(isOffline: boolean) { Onyx.merge(ONYXKEYS.NETWORK, {isOffline}); } +function setTimeSkew(skew: number) { + Onyx.merge(ONYXKEYS.NETWORK, {timeSkew: skew}); +} + function setShouldForceOffline(shouldForceOffline: boolean) { Onyx.merge(ONYXKEYS.NETWORK, {shouldForceOffline}); } @@ -16,4 +20,4 @@ function setShouldFailAllRequests(shouldFailAllRequests: boolean) { Onyx.merge(ONYXKEYS.NETWORK, {shouldFailAllRequests}); } -export {setIsOffline, setShouldForceOffline, setShouldFailAllRequests}; +export {setIsOffline, setShouldForceOffline, setShouldFailAllRequests, setTimeSkew}; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 296abc6a9cfa..fde9758af0d5 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -301,7 +301,7 @@ function addActions(reportID, text = '', file) { // Always prefer the file as the last action over text const lastAction = attachmentAction || reportCommentAction; - const currentTime = DateUtils.getDBTime(); + const currentTime = DateUtils.getDBTimeWithSkew(); const lastCommentText = ReportUtils.formatReportLastMessageText(lastAction.message[0].text); diff --git a/src/types/onyx/Network.ts b/src/types/onyx/Network.ts index 5af4c1170c3f..32b084bbf2f7 100644 --- a/src/types/onyx/Network.ts +++ b/src/types/onyx/Network.ts @@ -7,6 +7,9 @@ type Network = { /** Whether we should fail all network requests */ shouldFailAllRequests?: boolean; + + /** Skew between the client and server clocks */ + timeSkew?: number; }; export default Network;