diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index c02d200a7c83..1a10eb03a00e 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -71,6 +71,12 @@ Onyx.connect({ }, }); +let networkTimeSkew = 0; +Onyx.connect({ + key: ONYXKEYS.NETWORK, + callback: (value) => (networkTimeSkew = value?.timeSkew ?? 0), +}); + /** * Get the day of the week that the week starts on */ @@ -359,6 +365,16 @@ function getDBTime(timestamp: string | number = ''): string { return datetime.toISOString().replace('T', ' ').replace('Z', ''); } +/** + * Returns the current time plus skew in milliseconds in the format expected by the database + */ +function getDBTimeWithSkew(): string { + if (networkTimeSkew > 0) { + return getDBTime(new Date().valueOf() + networkTimeSkew); + } + return getDBTime(); +} + function subtractMillisecondsFromDateTime(dateTime: string, milliseconds: number): string { const date = zonedTimeToUtc(dateTime, 'UTC'); const newTimestamp = subMilliseconds(date, milliseconds).valueOf(); @@ -728,6 +744,7 @@ const DateUtils = { setTimezoneUpdated, getMicroseconds, getDBTime, + getDBTimeWithSkew, setLocale, subtractMillisecondsFromDateTime, getDateStringFromISOTimestamp, diff --git a/src/libs/HttpUtils.ts b/src/libs/HttpUtils.ts index e40c8148c923..22e342ac847b 100644 --- a/src/libs/HttpUtils.ts +++ b/src/libs/HttpUtils.ts @@ -5,6 +5,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {RequestType} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; +import * as NetworkActions from './actions/Network'; import * as ApiUtils from './ApiUtils'; import HttpsError from './Errors/HttpsError'; @@ -25,17 +26,41 @@ 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 APICommandRegex = /[?&]command=([^&]+)/; + /** * Send an HTTP request, and attempt to resolve the json response. * If there is a network error, we'll set the application offline. */ function processHTTPRequest(url: string, method: RequestType = 'get', body: FormData | null = null, canCancel = true): Promise { + 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) => { + // We are calculating the skew to minimize the delay when posting the messages + const match = url.match(APICommandRegex)?.[1]; + if (match && addSkewList.includes(match) && response.headers) { + const dateHeaderValue = response.headers.get('Date'); + const serverTime = dateHeaderValue ? new Date(dateHeaderValue).valueOf() : new Date().valueOf(); + const endTime = new Date().valueOf(); + const latency = (endTime - startTime) / 2; + const skew = serverTime - startTime + latency; + NetworkActions.setTimeSkew(dateHeaderValue ? skew : 0); + } + 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.ts b/src/libs/ReportUtils.ts index e619cb3c80dd..1010f8bd82e0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2406,7 +2406,7 @@ function buildOptimisticAddCommentReportAction(text?: string, file?: File): Opti ], automatic: false, avatar: allPersonalDetails?.[currentUserAccountID ?? -1]?.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 17580c214376..e71094eded05 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.ts b/src/libs/actions/Report.ts index 55e91834a803..19f6fa3f36b4 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -306,7 +306,7 @@ function addActions(reportID: string, text = '', file?: File) { // Always prefer the file as the last action over text const lastAction = attachmentAction ?? reportCommentAction; - const currentTime = DateUtils.getDBTime(); + const currentTime = DateUtils.getDBTimeWithSkew(); const lastComment = lastAction?.message?.[0]; const lastCommentText = ReportUtils.formatReportLastMessageText(lastComment?.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;