Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/conat/time.ts
1452 views
1
/*
2
Time sync -- relies on a hub running a time sync server.
3
4
IMPORTANT: Our realtime sync algorithm does NOT depend on an accurate clock anymore.
5
We may use time to compute logical timestamps for convenience, but they will always be
6
increasing and fall back to a non-time sequence for a while in case a clock is out of sync.
7
We do use the time for displaying edit times to users, which is one reason why syncing
8
the clock is useful.
9
10
To use this, call the default export, which is a sync
11
function that returns the current sync'd time (in ms since epoch), or
12
throws an error if the first time sync hasn't succeeded.
13
This gets initialized by default on load of your process.
14
If you want to await until the clock is sync'd, call "await getSkew()".
15
16
In unit testing mode this just falls back to Date.now().
17
18
DEVELOPMENT:
19
20
See src/packages/backend/conat/test/time.test.ts for relevant unit test, though
21
in test mode this is basically disabled.
22
23
Also do this, noting the directory and import of @cocalc/backend/conat.
24
25
~/cocalc/src/packages/backend$ node
26
Welcome to Node.js v18.17.1.
27
Type ".help" for more information.
28
> a = require('@cocalc/conat/time'); require('@cocalc/backend/conat')
29
{
30
getConnection: [Function: debounced],
31
init: [Function: init],
32
getCreds: [AsyncFunction: getCreds]
33
}
34
> await a.default()
35
1741643178722.5
36
37
*/
38
39
import { timeClient } from "@cocalc/conat/service/time";
40
import { reuseInFlight } from "@cocalc/util/reuse-in-flight";
41
import { getClient } from "@cocalc/conat/client";
42
import { delay } from "awaiting";
43
44
// we use exponential backoff starting with a short interval
45
// then making it longer
46
const INTERVAL_START = 5 * 1000;
47
const INTERVAL_GOOD = 1000 * 120;
48
const TOLERANCE = 3000;
49
50
export function init() {
51
syncLoop();
52
}
53
54
let state = "running";
55
export function close() {
56
state = "closed";
57
}
58
59
let syncLoopStarted = false;
60
async function syncLoop() {
61
if (syncLoopStarted) {
62
return;
63
}
64
syncLoopStarted = true;
65
const client = getClient();
66
let d = INTERVAL_START;
67
while (state != "closed" && client.state != "closed") {
68
try {
69
const lastSkew = skew ?? 0;
70
await getSkew();
71
if (state == "closed") return;
72
if (Math.abs((skew ?? 0) - lastSkew) >= TOLERANCE) {
73
// changing a lot so check again soon
74
d = INTERVAL_START;
75
} else {
76
d = Math.min(INTERVAL_GOOD, d * 2);
77
}
78
await delay(d);
79
} catch (err) {
80
// console.log(`WARNING: failed to sync clock -- ${err}`);
81
// reset delay
82
d = INTERVAL_START;
83
await delay(d);
84
}
85
}
86
}
87
88
// skew = amount in ms to subtract from our clock to get sync'd clock
89
export let skew: number | null = null;
90
let rtt: number | null = null;
91
export const getSkew = reuseInFlight(async (): Promise<number> => {
92
if (process.env.COCALC_TEST_MODE || process.env.COCALC_PROJECT_ID) {
93
// projects and test mode assumed to have correct time
94
skew = 0;
95
return skew;
96
}
97
try {
98
const start = Date.now();
99
const client = getClient();
100
const tc = timeClient(client);
101
const serverTime = await tc.time();
102
const end = Date.now();
103
rtt = end - start;
104
skew = start + rtt / 2 - serverTime;
105
return skew;
106
} catch (err) {
107
// console.log("WARNING: temporary issue syncing time", err);
108
skew = 0;
109
return 0;
110
}
111
});
112
113
export async function waitUntilTimeAvailable() {
114
if (skew != null) {
115
return;
116
}
117
await getSkew();
118
}
119
120
// get last measured round trip time
121
export function getLastPingTime(): number | null {
122
return rtt;
123
}
124
export function getLastSkew(): number | null {
125
return skew;
126
}
127
128
export default function getTime({
129
noError,
130
}: { noError?: boolean } = {}): number {
131
if (skew == null) {
132
init();
133
if (noError) {
134
return Date.now();
135
}
136
throw Error("clock skew not known");
137
}
138
return Date.now() - skew;
139
}
140
141