Files

177 lines
4.7 KiB
JavaScript

import {
createContext,
useState,
useContext,
useMemo,
useCallback,
useEffect,
useRef,
} from 'react';
import fetchBackend from '../db/handleBackend';
import { useKeysContext } from './keys';
import { getLocalStorageItem, setLocalStorageItem } from '../app/functions';
import { isMoreThanADayOld } from '../app/functions/rotateAddressDateChecker';
// Initiate context
const ServerTimeManager = createContext(null);
const GlobalServerTimeProvider = ({ children }) => {
const { contactsPrivateKey, publicKey } = useKeysContext();
const [serverTimeOffset, setServerTimeOffset] = useState(0);
const [isInitialized, setIsInitialized] = useState(false);
const [isSyncing, setIsSyncing] = useState(false);
const retryTimeoutRef = useRef(null);
const hasAttemptedSyncRef = useRef(false);
const syncServerTime = useCallback(
async (isRetry = false) => {
if (isSyncing) return;
setIsSyncing(true);
try {
const clientRequestTime = Date.now();
const savedServerTimeOffset = JSON.parse(
await getLocalStorageItem('savedServerTimeOffset'),
);
if (
savedServerTimeOffset?.offset !== undefined &&
!isMoreThanADayOld(savedServerTimeOffset.lastRotated)
) {
console.log(
'Using cached server time offset',
savedServerTimeOffset.offset,
);
setServerTimeOffset(savedServerTimeOffset.offset);
setIsInitialized(true);
setIsSyncing(false);
return;
}
const response = await fetchBackend(
'serverTime',
'',
contactsPrivateKey,
publicKey,
);
const clientReceiveTime = Date.now();
if (!response) {
throw new Error('No response from server');
}
const networkLatency = (clientReceiveTime - clientRequestTime) / 2;
const adjustedClientTime = clientRequestTime + networkLatency;
let serverTimeMs;
if (typeof response === 'object' && response.timestamp) {
serverTimeMs = response.timestamp;
} else if (typeof response === 'string') {
serverTimeMs = new Date(response).getTime();
} else {
serverTimeMs = Number(response);
}
const offset = serverTimeMs - adjustedClientTime;
setServerTimeOffset(offset);
await setLocalStorageItem(
'savedServerTimeOffset',
JSON.stringify({ offset, lastRotated: Date.now() }),
);
setIsInitialized(true);
setIsSyncing(false);
} catch (error) {
console.error('Failed to sync server time:', error);
if (!isRetry) {
console.log('Retrying server time sync in 30 seconds...');
retryTimeoutRef.current = setTimeout(() => {
syncServerTime(true);
}, 30000);
} else {
console.warn('Server time sync failed, falling back to device time');
setServerTimeOffset(0);
setIsInitialized(true);
}
setIsSyncing(false);
}
},
[contactsPrivateKey, publicKey],
);
const getServerTime = useCallback(() => {
if (!isInitialized) {
console.warn('Server time not initialized, using device time');
return Date.now();
}
return Date.now() + serverTimeOffset;
}, [serverTimeOffset, isInitialized]);
useEffect(() => {
hasAttemptedSyncRef.current = false;
setIsInitialized(false);
}, [contactsPrivateKey, publicKey]);
useEffect(() => {
if (!contactsPrivateKey || !publicKey) return;
if (hasAttemptedSyncRef.current) return;
console.log('Initializing server time sync...');
hasAttemptedSyncRef.current = true;
syncServerTime();
return () => {
if (retryTimeoutRef.current) {
clearTimeout(retryTimeoutRef.current);
retryTimeoutRef.current = null;
}
};
}, [contactsPrivateKey, publicKey, syncServerTime]);
const contextValue = useMemo(
() => ({
getServerTime,
isInitialized,
isSyncing,
serverTimeOffset,
isUsingServerTime: isInitialized && serverTimeOffset !== 0,
}),
[getServerTime, isInitialized, isSyncing, serverTimeOffset],
);
return (
<ServerTimeManager.Provider value={contextValue}>
{children}
</ServerTimeManager.Provider>
);
};
function useServerTime() {
const context = useContext(ServerTimeManager);
if (!context) {
throw new Error(
'useServerTime must be used within a GlobalServerTimeProvider',
);
}
return context;
}
function useServerTimeOnly() {
const { getServerTime } = useServerTime();
return getServerTime;
}
export {
ServerTimeManager,
GlobalServerTimeProvider,
useServerTime,
useServerTimeOnly,
};