import { isNumber, round } from './numberUtils' export const ONE_SECOND = 1000 export const ONE_MINUTE = 60 * ONE_SECOND export const ONE_HOUR = 60 * ONE_MINUTE export const ONE_DAY = 24 * ONE_HOUR export const ONE_YEAR = 365 * ONE_DAY export type Duration = number & { d: 'Duration in ms' } export type ServerDuration = number & { s: 'Duration in ns' } export type TimeStamp = number & { t: 'Epoch time' } export type RelativeTime = number & { r: 'Time relative to navigation start' } & { d: 'Duration in ms' } export interface ClocksState { relative: RelativeTime timeStamp: TimeStamp } export function relativeToClocks(relative: RelativeTime) { return { relative, timeStamp: getCorrectedTimeStamp(relative) } } export function timeStampToClocks(timeStamp: TimeStamp) { return { relative: getRelativeTime(timeStamp), timeStamp } } function getCorrectedTimeStamp(relativeTime: RelativeTime) { const correctedOrigin = (dateNow() - performance.now()) as TimeStamp // apply correction only for positive drift if (correctedOrigin > getNavigationStart()) { return Math.round(addDuration(correctedOrigin, relativeTime)) as TimeStamp } return getTimeStamp(relativeTime) } export function currentDrift() { return Math.round(dateNow() - addDuration(getNavigationStart(), performance.now() as Duration)) } export function toServerDuration(duration: Duration): ServerDuration export function toServerDuration(duration: Duration | undefined): ServerDuration | undefined export function toServerDuration(duration: Duration | undefined) { if (!isNumber(duration)) { return duration } return round(duration * 1e6, 0) as ServerDuration } export function dateNow() { // Do not use `Date.now` because sometimes websites are wrongly "polyfilling" it. For example, we // had some users using a very old version of `datejs`, which patched `Date.now` to return a Date // instance instead of a timestamp[1]. Those users are unlikely to fix this, so let's handle this // case ourselves. // [1]: https://github.com/datejs/Datejs/blob/97f5c7c58c5bc5accdab8aa7602b6ac56462d778/src/core-debug.js#L14-L16 return new Date().getTime() } export function timeStampNow() { return dateNow() as TimeStamp } export function relativeNow() { return performance.now() as RelativeTime } export function clocksNow() { return { relative: relativeNow(), timeStamp: timeStampNow() } } export function clocksOrigin() { return { relative: 0 as RelativeTime, timeStamp: getNavigationStart() } } export function elapsed(start: TimeStamp, end: TimeStamp): Duration export function elapsed(start: RelativeTime, end: RelativeTime): Duration export function elapsed(start: number, end: number) { return (end - start) as Duration } export function addDuration(a: TimeStamp, b: Duration): TimeStamp export function addDuration(a: RelativeTime, b: Duration): RelativeTime export function addDuration(a: Duration, b: Duration): Duration export function addDuration(a: number, b: number) { return a + b } // Get the time since the navigation was started. export function getRelativeTime(timestamp: TimeStamp) { return (timestamp - getNavigationStart()) as RelativeTime } export function getTimeStamp(relativeTime: RelativeTime) { return Math.round(addDuration(getNavigationStart(), relativeTime)) as TimeStamp } export function looksLikeRelativeTime(time: RelativeTime | TimeStamp): time is RelativeTime { return time < ONE_YEAR } /** * Navigation start slightly change on some rare cases */ let navigationStart: TimeStamp | undefined /** * Notes: this does not use `performance.timeOrigin` because: * - It doesn't seem to reflect the actual time on which the navigation has started: it may be much farther in the past, * at least in Firefox 71. (see: https://bugzilla.mozilla.org/show_bug.cgi?id=1429926) * - It is not supported in Safari <15 */ function getNavigationStart() { if (navigationStart === undefined) { // ServiceWorkers do not support navigationStart (it's deprecated), so we fallback to timeOrigin navigationStart = (performance.timing?.navigationStart ?? performance.timeOrigin) as TimeStamp } return navigationStart }