/*1Time sync -- relies on a hub running a time sync server.23IMPORTANT: Our realtime sync algorithm does NOT depend on an accurate clock anymore.4We may use time to compute logical timestamps for convenience, but they will always be5increasing and fall back to a non-time sequence for a while in case a clock is out of sync.6We do use the time for displaying edit times to users, which is one reason why syncing7the clock is useful.89To use this, call the default export, which is a sync10function that returns the current sync'd time (in ms since epoch), or11throws an error if the first time sync hasn't succeeded.12This gets initialized by default on load of your process.13If you want to await until the clock is sync'd, call "await getSkew()".1415In unit testing mode this just falls back to Date.now().1617DEVELOPMENT:1819See src/packages/backend/conat/test/time.test.ts for relevant unit test, though20in test mode this is basically disabled.2122Also do this, noting the directory and import of @cocalc/backend/conat.2324~/cocalc/src/packages/backend$ node25Welcome to Node.js v18.17.1.26Type ".help" for more information.27> a = require('@cocalc/conat/time'); require('@cocalc/backend/conat')28{29getConnection: [Function: debounced],30init: [Function: init],31getCreds: [AsyncFunction: getCreds]32}33> await a.default()341741643178722.53536*/3738import { timeClient } from "@cocalc/conat/service/time";39import { reuseInFlight } from "@cocalc/util/reuse-in-flight";40import { getClient } from "@cocalc/conat/client";41import { delay } from "awaiting";4243// we use exponential backoff starting with a short interval44// then making it longer45const INTERVAL_START = 5 * 1000;46const INTERVAL_GOOD = 1000 * 120;47const TOLERANCE = 3000;4849export function init() {50syncLoop();51}5253let state = "running";54export function close() {55state = "closed";56}5758let syncLoopStarted = false;59async function syncLoop() {60if (syncLoopStarted) {61return;62}63syncLoopStarted = true;64const client = getClient();65let d = INTERVAL_START;66while (state != "closed" && client.state != "closed") {67try {68const lastSkew = skew ?? 0;69await getSkew();70if (state == "closed") return;71if (Math.abs((skew ?? 0) - lastSkew) >= TOLERANCE) {72// changing a lot so check again soon73d = INTERVAL_START;74} else {75d = Math.min(INTERVAL_GOOD, d * 2);76}77await delay(d);78} catch (err) {79// console.log(`WARNING: failed to sync clock -- ${err}`);80// reset delay81d = INTERVAL_START;82await delay(d);83}84}85}8687// skew = amount in ms to subtract from our clock to get sync'd clock88export let skew: number | null = null;89let rtt: number | null = null;90export const getSkew = reuseInFlight(async (): Promise<number> => {91if (process.env.COCALC_TEST_MODE || process.env.COCALC_PROJECT_ID) {92// projects and test mode assumed to have correct time93skew = 0;94return skew;95}96try {97const start = Date.now();98const client = getClient();99const tc = timeClient(client);100const serverTime = await tc.time();101const end = Date.now();102rtt = end - start;103skew = start + rtt / 2 - serverTime;104return skew;105} catch (err) {106// console.log("WARNING: temporary issue syncing time", err);107skew = 0;108return 0;109}110});111112export async function waitUntilTimeAvailable() {113if (skew != null) {114return;115}116await getSkew();117}118119// get last measured round trip time120export function getLastPingTime(): number | null {121return rtt;122}123export function getLastSkew(): number | null {124return skew;125}126127export default function getTime({128noError,129}: { noError?: boolean } = {}): number {130if (skew == null) {131init();132if (noError) {133return Date.now();134}135throw Error("clock skew not known");136}137return Date.now() - skew;138}139140141