socket-function 1.1.5 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,6 +29,10 @@ export declare class SocketFunction {
29
29
  serialize: (obj: unknown) => MaybePromise<Buffer[]>;
30
30
  deserialize: (buffers: Buffer[]) => MaybePromise<unknown>;
31
31
  };
32
+ /** We will try the alternate node IDs first, however, if they fail, we will go through all of them and then eventually try the original node ID.
33
+ * VERY useful, allowing us to change global ips to local ones, which short-circuits the router, massively increasing bandwidth and decreasing latency.
34
+ */
35
+ static GET_ALTERNATE_NODE_IDS: (nodeId: string) => MaybePromise<string[] | undefined>;
32
36
  static WIRE_WARN_TIME: number;
33
37
  private static onMountCallbacks;
34
38
  static exposedClasses: Set<string>;
package/SocketFunction.ts CHANGED
@@ -72,11 +72,17 @@ export class SocketFunction {
72
72
  // In retrospect... dynamically changing the wire serializer is a BAD idea. If any calls happen
73
73
  // before it is changed, things just break. Also, it needs to be changed on both sides,
74
74
  // or else things break. Also, it is very hard to detect when the issue is different serializers
75
+ // NOTE: The only reason this is still exposed is in case in the future we want to intercept our traffic, and we want convenient functions to know how to decode it (although there are a still few other layers under this, for compression and Buffer[] sending efficiency).
75
76
  public static readonly WIRE_SERIALIZER = {
76
77
  serialize: measureWrap((obj: unknown): MaybePromise<Buffer[]> => [cborxInstance.encode(obj)], "WIRE_SERIALIZER|serialize"),
77
78
  deserialize: measureWrap((buffers: Buffer[]): MaybePromise<unknown> => cborxInstance.decode(buffers[0]), "WIRE_SERIALIZER|deserialize"),
78
79
  };
79
80
 
81
+ /** We will try the alternate node IDs first, however, if they fail, we will go through all of them and then eventually try the original node ID.
82
+ * VERY useful, allowing us to change global ips to local ones, which short-circuits the router, massively increasing bandwidth and decreasing latency.
83
+ */
84
+ public static GET_ALTERNATE_NODE_IDS = (nodeId: string): MaybePromise<string[] | undefined> => undefined;
85
+
80
86
  public static WIRE_WARN_TIME = 100;
81
87
 
82
88
  private static onMountCallbacks = new Map<string, (() => MaybePromise<void>)[]>();
@@ -130,5 +130,6 @@ export type CallerContextBase = {
130
130
  // requests can be redirected to us and would accept them, even though they are being
131
131
  // blatantly MITMed).
132
132
  // IF they are the server, calling us back, then this will just be ""
133
+ // If they're connecting on the same machine, this might be a special domain (127-0-0-1.querysub.com). This is controlled by the GET_ALTERNATE_NODE_IDS override.
133
134
  localNodeId: string;
134
135
  };
package/index.d.ts CHANGED
@@ -38,6 +38,10 @@ declare module "socket-function/SocketFunction" {
38
38
  serialize: (obj: unknown) => MaybePromise<Buffer[]>;
39
39
  deserialize: (buffers: Buffer[]) => MaybePromise<unknown>;
40
40
  };
41
+ /** We will try the alternate node IDs first, however, if they fail, we will go through all of them and then eventually try the original node ID.
42
+ * VERY useful, allowing us to change global ips to local ones, which short-circuits the router, massively increasing bandwidth and decreasing latency.
43
+ */
44
+ static GET_ALTERNATE_NODE_IDS: (nodeId: string) => MaybePromise<string[] | undefined>;
41
45
  static WIRE_WARN_TIME: number;
42
46
  private static onMountCallbacks;
43
47
  static exposedClasses: Set<string>;
@@ -1408,6 +1412,17 @@ declare module "socket-function/test" {
1408
1412
  }
1409
1413
 
1410
1414
  declare module "socket-function/time/trueTimeShim" {
1415
+ export declare function getTimeComponentsDetailed(): {
1416
+ systemTime: number;
1417
+ fromOffset: number;
1418
+ toOffset: number;
1419
+ fromTime: number;
1420
+ toTime: number;
1421
+ };
1422
+ export declare function getTimeComponents(): {
1423
+ systemTime: number;
1424
+ offset: number;
1425
+ };
1411
1426
  export declare function getTrueTime(): number;
1412
1427
  export declare function getTrueTimeOffset(): number;
1413
1428
  export declare function waitForFirstTimeSync(): Promise<void> | undefined;
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "socket-function",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "dependencies": {
7
7
  "@types/pako": "^2.0.3",
8
8
  "@types/ws": "^8.5.3",
9
9
  "cbor-x": "^1.6.0",
10
+ "lmdb": "^3.5.1",
10
11
  "mobx": "^6.6.2",
11
12
  "pako": "^2.1.0",
12
13
  "preact": "10.24.3",
@@ -16,11 +17,11 @@
16
17
  },
17
18
  "types": "index.d.ts",
18
19
  "scripts": {
19
- "test": "yarn typenode ./test/server.ts",
20
+ "test": "yarn typenode ./test.ts",
21
+ "test-all": "node run-test.js",
20
22
  "type": "yarn tsc --noEmit",
21
23
  "emit-dts": "yarn tsc --project tsconfig.declarations.json || exit 0",
22
24
  "generate-index-dts": "node ./generateIndexDts.js",
23
- "update-typings": "yarn generate-index-dts && yarn emit-dts",
24
25
  "update-types": "yarn emit-dts && yarn generate-index-dts",
25
26
  "prepublishOnly": "yarn update-types",
26
27
  "testsni": "yarn typenode ./src/sniTest.ts"
package/run-test.js ADDED
@@ -0,0 +1,59 @@
1
+ const { spawn } = require('child_process');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ // Clean up old test files
6
+ console.log('Cleaning up old test files...');
7
+ const distDir = path.join(__dirname, 'dist');
8
+ if (fs.existsSync(distDir)) {
9
+ const files = fs.readdirSync(distDir)
10
+ .filter(f => f.startsWith('time-samples-') && f.endsWith('.json'));
11
+ for (const file of files) {
12
+ fs.unlinkSync(path.join(distDir, file));
13
+ }
14
+ }
15
+
16
+ // Launch 4 concurrent processes
17
+ console.log('Launching 4 concurrent test processes...\n');
18
+
19
+ function runTest() {
20
+ return new Promise((resolve, reject) => {
21
+ const proc = spawn('yarn', ['test'], {
22
+ stdio: 'inherit',
23
+ shell: true
24
+ });
25
+
26
+ proc.on('close', (code) => {
27
+ if (code === 0) {
28
+ resolve();
29
+ } else {
30
+ reject(new Error(`Process exited with code ${code}`));
31
+ }
32
+ });
33
+
34
+ proc.on('error', reject);
35
+ });
36
+ }
37
+
38
+ Promise.all([
39
+ runTest(),
40
+ runTest(),
41
+ runTest(),
42
+ runTest()
43
+ ])
44
+ .then(() => {
45
+ console.log('\n\nAll sampling complete. Running verification...\n');
46
+
47
+ const verify = spawn('yarn', ['test', 'verify'], {
48
+ stdio: 'inherit',
49
+ shell: true
50
+ });
51
+
52
+ verify.on('close', (code) => {
53
+ process.exit(code);
54
+ });
55
+ })
56
+ .catch((err) => {
57
+ console.error('Error running tests:', err);
58
+ process.exit(1);
59
+ });
@@ -19,8 +19,6 @@ import { measureFnc, measureWrap, registerMeasureInfo } from "./profiling/measur
19
19
  import { MaybePromise } from "./types";
20
20
  import { Zip } from "./Zip";
21
21
  import { LZ4 } from "./lz4/LZ4";
22
- //LZ4.compress;
23
- //LZ4.decompress;
24
22
 
25
23
  setFlag(require, "pako", "allowclient", true);
26
24
 
@@ -282,16 +280,22 @@ export async function createCallFactory(
282
280
  };
283
281
 
284
282
  let webSocketPromise: Promise<SenderInterface> | undefined;
283
+ let hasEverConnected = false;
285
284
  if (webSocketBase) {
286
285
  webSocketPromise = Promise.resolve(webSocketBase);
287
286
  await initializeWebsocket(webSocketBase);
288
287
  }
289
288
 
290
- async function initializeWebsocket(newWebSocket: SenderInterface) {
289
+ async function initializeWebsocket(newWebSocket: SenderInterface, skipCloseHandling = false) {
291
290
  registerOnce();
292
291
  callFactory.receivedInitializeState = undefined;
293
292
 
294
293
  function onClose(error: string) {
294
+ // We try various connections, and if they fail, we will just try other node IDs until we finally do connect, and then we stick with that nodeId, and when it disconnects we need to handle disconnections normally.
295
+ if (skipCloseHandling && !hasEverConnected) {
296
+ return;
297
+ }
298
+
295
299
  callFactory.connectionId = { nodeId };
296
300
  callFactory.lastClosed = Date.now();
297
301
  callFactory.isConnected = false;
@@ -341,6 +345,7 @@ export async function createCallFactory(
341
345
  console.log(`Connection established to ${niceConnectionName}`);
342
346
  }
343
347
  callFactory.isConnected = true;
348
+ hasEverConnected = true;
344
349
  resolve();
345
350
  });
346
351
  newWebSocket.addEventListener("close", () => resolve());
@@ -348,6 +353,7 @@ export async function createCallFactory(
348
353
  });
349
354
  } else if (newWebSocket.readyState === 1 /* OPEN */) {
350
355
  callFactory.isConnected = true;
356
+ hasEverConnected = true;
351
357
  } else {
352
358
  onClose(new Error(`Websocket received in closed state`).stack!);
353
359
  }
@@ -441,6 +447,23 @@ export async function createCallFactory(
441
447
  }
442
448
  lastConnectionAttempt = Date.now();
443
449
 
450
+ // Try alternates, and if any work, use them
451
+ try {
452
+ let alternates = await SocketFunction.GET_ALTERNATE_NODE_IDS(nodeId);
453
+ if (alternates) {
454
+ for (let alternateNodeId of alternates) {
455
+ let newWebSocket = createWebsocket(alternateNodeId);
456
+ await initializeWebsocket(newWebSocket, true);
457
+
458
+ if (callFactory.isConnected) {
459
+ return newWebSocket;
460
+ }
461
+ }
462
+ }
463
+ } catch (e) {
464
+ console.error("Error getting alternate node IDs", e);
465
+ }
466
+
444
467
  let newWebSocket = createWebsocket(nodeId);
445
468
  await initializeWebsocket(newWebSocket);
446
469
 
package/test.ts CHANGED
@@ -1,33 +1,167 @@
1
- import { getExternalIP } from "./src/networking";
2
- import http from "http";
3
- import * as dgram from "dgram";
4
- import os from "os";
5
- import debugbreak from "debugbreak";
6
- import { forwardPort } from "./src/forwardPort";
1
+ import { getTimeComponentsDetailed, waitForFirstTimeSync } from "./time/trueTimeShim";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
7
4
 
5
+ const SAMPLE_COUNT = 1000;
6
+ const SAMPLE_INTERVAL_MS = 10;
8
7
 
9
- // Usage example:
10
- async function main() {
11
- const externalPort = 11300;
12
- const internalPort = externalPort;
8
+ // Generate a simple numeric ID for this test run
9
+ const TEST_RUN_ID = Math.floor(Math.random() * 10000);
10
+
11
+ async function sampleMode() {
12
+ await waitForFirstTimeSync();
13
+
14
+ type Sample = {
15
+ id: number;
16
+ systemTime: number;
17
+ offset: number;
18
+ fromOffset: number;
19
+ toOffset: number;
20
+ fromTime: number;
21
+ toTime: number;
22
+ fraction: number;
23
+ };
24
+
25
+ const samples: Sample[] = [];
26
+
27
+ console.log(`[Test ID: ${TEST_RUN_ID}] Sampling ${SAMPLE_COUNT} time components...`);
28
+ for (let i = 0; i < SAMPLE_COUNT; i++) {
29
+ const detailed = getTimeComponentsDetailed();
30
+
31
+ // Calculate smearing using the same systemTime
32
+ const elapsed = detailed.systemTime - detailed.fromTime;
33
+ const duration = detailed.toTime - detailed.fromTime;
34
+ const fraction = duration > 0 ? Math.min(1, elapsed / duration) : 0;
35
+ const offset = detailed.fromOffset + (detailed.toOffset - detailed.fromOffset) * fraction;
36
+
37
+ samples.push({
38
+ id: TEST_RUN_ID,
39
+ systemTime: detailed.systemTime,
40
+ offset,
41
+ fromOffset: detailed.fromOffset,
42
+ toOffset: detailed.toOffset,
43
+ fromTime: detailed.fromTime,
44
+ toTime: detailed.toTime,
45
+ fraction,
46
+ });
47
+
48
+ if (SAMPLE_INTERVAL_MS > 0) {
49
+ await new Promise(resolve => setTimeout(resolve, SAMPLE_INTERVAL_MS));
50
+ }
51
+ }
52
+
53
+ // Write to file with unique name
54
+ const distDir = path.join(__dirname, "dist");
55
+ if (!fs.existsSync(distDir)) {
56
+ fs.mkdirSync(distDir, { recursive: true });
57
+ }
58
+
59
+ const filename = path.join(distDir, `time-samples-${Date.now()}-${process.pid}.json`);
60
+ fs.writeFileSync(filename, JSON.stringify(samples, null, 2));
61
+
62
+ console.log(`[Test ID: ${TEST_RUN_ID}] Wrote ${samples.length} samples to ${filename}`);
63
+ }
64
+
65
+ async function verifyMode() {
66
+ const distDir = path.join(__dirname, "dist");
67
+ if (!fs.existsSync(distDir)) {
68
+ console.error("No dist directory found. Run sampling mode first.");
69
+ process.exit(1);
70
+ }
13
71
 
14
- await forwardPort({ externalPort, internalPort });
72
+ const files = fs.readdirSync(distDir)
73
+ .filter(f => f.startsWith("time-samples-") && f.endsWith(".json"))
74
+ .map(f => path.join(distDir, f));
15
75
 
16
- // Listen on the external port
17
- // const server = http.createServer((req, res) => {
18
- // console.log("Request received");
19
- // res.end("Hello, world!");
20
- // });
21
- // server.listen(externalPort, "0.0.0.0");
76
+ if (files.length === 0) {
77
+ console.error("No sample files found. Run sampling mode first.");
78
+ process.exit(1);
79
+ }
22
80
 
23
- // {
24
- // const externalIP = await getExternalIP();
25
- // let test = await fetch(`http://${externalIP}:${externalPort}`);
26
- // console.log(await test.text());
27
- // }
81
+ console.log(`Found ${files.length} sample files`);
28
82
 
83
+ type Sample = {
84
+ id: number;
85
+ systemTime: number;
86
+ offset: number;
87
+ fromOffset: number;
88
+ toOffset: number;
89
+ fromTime: number;
90
+ toTime: number;
91
+ fraction: number;
92
+ file: string;
93
+ };
94
+
95
+ // Load all samples from all files
96
+ const allSamples: Sample[] = [];
97
+
98
+ for (const file of files) {
99
+ const content = fs.readFileSync(file, "utf-8");
100
+ const samples = JSON.parse(content);
101
+ for (const sample of samples) {
102
+ allSamples.push({ ...sample, file: path.basename(file) });
103
+ }
104
+ }
105
+
106
+ console.log(`Loaded ${allSamples.length} total samples`);
107
+
108
+ // Sort by systemTime
109
+ allSamples.sort((a, b) => a.systemTime - b.systemTime);
110
+
111
+ // Verify that systemTime + offset is monotonically increasing
112
+ let errors = 0;
113
+ let lastTrueTime = -Infinity;
114
+
115
+ for (let i = 0; i < allSamples.length; i++) {
116
+ const sample = allSamples[i];
117
+ const trueTime = sample.systemTime + sample.offset;
118
+
119
+ if (trueTime < lastTrueTime) {
120
+ errors++;
121
+ const prev = allSamples[i - 1];
122
+ const prevTrueTime = prev.systemTime + prev.offset;
123
+
124
+ console.error(`\nERROR at index ${i}: trueTime went backwards!`);
125
+ console.error(` Previous [ID: ${prev.id}, file: ${prev.file}]:`);
126
+ console.error(` systemTime: ${prev.systemTime}`);
127
+ console.error(` offset: ${prev.offset}`);
128
+ console.error(` trueTime: ${prevTrueTime}`);
129
+ console.error(` smearing: ${prev.fromOffset} -> ${prev.toOffset} (${(prev.fraction * 100).toFixed(2)}%)`);
130
+ console.error(` timeWindow: ${prev.fromTime} -> ${prev.toTime}`);
131
+ console.error(` Current [ID: ${sample.id}, file: ${sample.file}]:`);
132
+ console.error(` systemTime: ${sample.systemTime}`);
133
+ console.error(` offset: ${sample.offset}`);
134
+ console.error(` trueTime: ${trueTime}`);
135
+ console.error(` smearing: ${sample.fromOffset} -> ${sample.toOffset} (${(sample.fraction * 100).toFixed(2)}%)`);
136
+ console.error(` timeWindow: ${sample.fromTime} -> ${sample.toTime}`);
137
+ console.error(` Difference: ${trueTime - lastTrueTime}ms`);
138
+ }
139
+
140
+ lastTrueTime = trueTime;
141
+ }
142
+
143
+ if (errors === 0) {
144
+ console.log(`✓ SUCCESS: All ${allSamples.length} samples are correctly ordered!`);
145
+ console.log(` Time range: ${allSamples[0].systemTime} to ${allSamples[allSamples.length - 1].systemTime}`);
146
+ console.log(` Duration: ${allSamples[allSamples.length - 1].systemTime - allSamples[0].systemTime}ms`);
147
+ } else {
148
+ console.error(`✗ FAILED: Found ${errors} ordering violations`);
149
+ process.exit(1);
150
+ }
151
+ }
152
+
153
+ async function main() {
154
+ const mode = process.argv[2];
29
155
 
30
- //await createPortMapping({ externalPort, internalPort, gateWayIP, internalIP, });
156
+ if (mode === "once") {
157
+ await waitForFirstTimeSync();
158
+ console.log(Date.now());
159
+ }
160
+ else if (mode === "verify") {
161
+ await verifyMode();
162
+ } else {
163
+ await sampleMode();
164
+ }
31
165
  }
32
166
 
33
- main().catch(e => console.error(e));
167
+ main().catch(e => console.error(e)).finally(() => process.exit(0));
@@ -1,3 +1,14 @@
1
+ export declare function getTimeComponentsDetailed(): {
2
+ systemTime: number;
3
+ fromOffset: number;
4
+ toOffset: number;
5
+ fromTime: number;
6
+ toTime: number;
7
+ };
8
+ export declare function getTimeComponents(): {
9
+ systemTime: number;
10
+ offset: number;
11
+ };
1
12
  export declare function getTrueTime(): number;
2
13
  export declare function getTrueTimeOffset(): number;
3
14
  export declare function waitForFirstTimeSync(): Promise<void> | undefined;
@@ -1,225 +1,528 @@
1
- import { SocketFunction } from "../SocketFunction";
2
- import { blue, green, red, yellow } from "../src/formatting/logColors";
3
- import { isNode } from "../src/misc";
4
-
5
- // IMPOTRANT! We don't ensure that the times of return are unique. We cannot ensure they are unique because the amount of precision is only about ten thousand date times per millisecond, Which would mean if the calling code called date.now frequently enough, which doesn't even have to be that frequent, it could slowly drift farther and farther ahead of the real time, which would be really bad.
6
-
7
- module.allowclient = true;
8
-
9
- const UPDATE_INTERVAL = 1000 * 60 * 10;
10
- // More frequent, to ensure we don't run into major issues with sleep (coming back from sleep,
11
- // having the interval not be fired immediately, and having the time be off for a few minutes).
12
- const UPDATE_SUB_INTERVAL = 1000 * 10;
13
- // Smearing is important, otherwise some performance timing (especially on load) can easily be off
14
- // by a few hundred milliseconds. The current smear parameters will mean even with 1s of offset
15
- // we only add 10ms every 100ms, so worst case scenario some timing that takes 0ms will take 10ms.
16
- const UPDATE_SMEAR_TICK_DURATION = 100;
17
- const UPDATE_SMEAR_TICK_COUNT = 100;
18
- const UPDATE_VERIFY_COUNT = 3;
19
-
20
- // Time can never go backwards, but we can run at a slower rate until the output time allows
21
- // the real time to catch up with it.
22
- const MINIMUM_TIME_RATE = 0.5;
23
-
24
- let trueTimeOffset = 0;
25
- let didFirstTimeSync = false;
26
- let onFirstTimeSync!: () => void;
27
- let firstTimeSyncPromise = new Promise<void>((resolve) => {
28
- onFirstTimeSync = resolve;
29
- });
30
-
31
- const baseGetTime = Date.now;
32
- let lastTime = 0;
33
- let lastBaseTime = 0;
34
- export function getTrueTime() {
35
- let baseTime = baseGetTime();
36
- let time = baseTime + trueTimeOffset;
37
- // Only adjust time once we have a time offset. Otherwise systems with a really bad clock
38
- // might take days be correct. It is better for the time to jump once at startup, rather
39
- // than be off by days, for days at a time.
40
- if (lastTime && trueTimeOffset) {
41
- if (time < lastTime) {
42
- let diff = baseTime - lastBaseTime;
43
- if (diff >= 0) {
44
- // Some time passed, so we have a baseline for how much to increase the time by.
45
- // This allows the real time to catch up with our time naturally.
46
- time = lastTime + diff * MINIMUM_TIME_RATE;
47
- } else {
48
- // The issue is the system time going backwards. In this case, allow the time to change
49
- }
50
- }
51
- }
52
- lastTime = time;
53
- lastBaseTime = baseTime;
54
- return time;
55
- }
56
- export function getTrueTimeOffset() {
57
- return trueTimeOffset;
58
- }
59
- export function waitForFirstTimeSync(): Promise<void> | undefined {
60
- if (didFirstTimeSync) return undefined;
61
- return firstTimeSyncPromise;
62
- }
63
- let shimmed = false;
64
- export function shimDateNow() {
65
- if (shimmed) return;
66
- shimmed = true;
67
- Date.now = getTrueTime;
68
- }
69
- export function getBrowserTime() {
70
- return baseGetTime();
71
- }
72
-
73
- export function setGetTimeOffsetBase(base: () => Promise<number>) {
74
- getTimeOffsetBase = base;
75
- }
76
-
77
- async function defaultGetTimeOffset(): Promise<number> {
78
- if (!isNode()) {
79
- let sendTime = baseGetTime();
80
- let serverTrueTime = await TimeController.nodes[SocketFunction.browserNodeId()].getTrueTime();
81
- let systemTime = baseGetTime();
82
- let predictedServerToClientLatency = (systemTime - sendTime) / 2;
83
- let trueTimeRightNow = serverTrueTime + predictedServerToClientLatency;
84
- return trueTimeRightNow - systemTime;
85
- }
86
- const dgram = await import("dgram");
87
- const NTP_SERVER = "time.google.com";
88
- const NTP_PORT = 123;
89
- const NTP_PACKET_SIZE = 48;
90
- const NTP_EPOCH_OFFSET = 2208988800000; // Number of milliseconds between 1900-01-01 and 1970-01-01
91
- return new Promise((resolve, reject) => {
92
- const client = dgram.createSocket("udp4");
93
- const message = Buffer.alloc(NTP_PACKET_SIZE);
94
-
95
- // Set the first byte to represent NTP client request (LI = 0, VN = 3, Mode = 3)
96
- message[0] = 0x1B;
97
-
98
- const sendTime = baseGetTime();
99
-
100
- client.send(message, 0, message.length, NTP_PORT, NTP_SERVER);
101
- client.on("error", (err) => {
102
- client.close();
103
- reject(err);
104
- });
105
-
106
- client.on("message", (msg) => {
107
- const receiveTime = baseGetTime();
108
-
109
- // Extract the transmit timestamp from the server response
110
- const transmitTimestampSeconds = msg.readUInt32BE(40);
111
- const transmitTimestampFraction = msg.readUInt32BE(44);
112
- const transmitTimestamp = (transmitTimestampSeconds * 1000) + (transmitTimestampFraction * 1000 / 0x100000000) - NTP_EPOCH_OFFSET;
113
-
114
- const predictedServerToClientLatency = (receiveTime - sendTime) / 2;
115
-
116
- // Calculate the offset
117
- const systemTime = baseGetTime();
118
- const actualTime = transmitTimestamp + predictedServerToClientLatency;
119
- const offset = actualTime - systemTime;
120
-
121
- client.close();
122
- resolve(offset);
123
- });
124
- });
125
- }
126
-
127
- let getTimeOffsetBase: () => Promise<number> = defaultGetTimeOffset;
128
- let updatingOffset = false;
129
- async function updateTimeOffset() {
130
- if (updatingOffset) return;
131
- updatingOffset = true;
132
- try {
133
- let offsets: number[] = [];
134
- for (let i = 0; i < UPDATE_VERIFY_COUNT; i++) {
135
- try {
136
- offsets.push(await getTimeOffsetBase());
137
- } catch (e) {
138
- console.error("Error getting time offset:", e);
139
- }
140
- }
141
- // If we have no offsets, it likely means every call errored out (probably because the network is down).
142
- // This is fine, just don't update (DO register the first sync as being done, otherwise calling code
143
- // might be waiting forever).
144
- if (offsets.length > 0) {
145
- // Pick the middle offset
146
- offsets.sort((a, b) => a - b);
147
- let offset = offsets[Math.floor(offsets.length / 2)];
148
-
149
- // Smear it slowly
150
- let currentSmearCount = UPDATE_SMEAR_TICK_COUNT;
151
- // Update the initial time all at once, otherwise initial requests to other servers might
152
- // be rejected (because they could use the system time, which could be off by a few seconds).
153
- if (!didFirstTimeSync) {
154
- currentSmearCount = 1;
155
- }
156
-
157
- let prevOffset = trueTimeOffset;
158
- let offsetRound = Math.abs(Math.round(offset));
159
- let offsetColored = (
160
- Math.abs(offset) > 600 && red(offsetRound + "ms")
161
- || Math.abs(offset) > 300 && yellow(offsetRound + "ms")
162
- || green(offsetRound + "ms")
163
- );
164
- if (Math.abs(offset) > 500) {
165
- console.log(`${blue("Synchronized time")}, local clock was ${offset > 0 ? "behind" : "ahead"} by ${offsetColored} @ ${blue(Date.now() + "")}`);
166
- }
167
- for (let i = 0; i < currentSmearCount; i++) {
168
- let fraction = (i + 1) / currentSmearCount;
169
- trueTimeOffset = prevOffset * (1 - fraction) + offset * fraction;
170
- if (i < currentSmearCount - 1) {
171
- await new Promise((resolve) => setTimeout(resolve, UPDATE_SMEAR_TICK_DURATION));
172
- }
173
- }
174
- }
175
-
176
- if (!didFirstTimeSync) {
177
- didFirstTimeSync = true;
178
- onFirstTimeSync();
179
- }
180
- } finally {
181
- updatingOffset = false;
182
- }
183
- }
184
-
185
- let nextUpdateTime = 0;
186
- setInterval(() => {
187
- if (baseGetTime() < nextUpdateTime) return;
188
- nextUpdateTime = baseGetTime() + UPDATE_INTERVAL;
189
- updateTimeOffset().catch((e) => {
190
- console.warn("Error updating time offset:", e);
191
- });
192
- }, UPDATE_SUB_INTERVAL);
193
- setImmediate(() => {
194
- updateTimeOffset().catch((e) => {
195
- console.error("Error updating initial offset:", e);
196
- });
197
- });
198
-
199
-
200
- class TimeControllerBase {
201
- public async getTrueTime() {
202
- await waitForFirstTimeSync();
203
- return getTrueTime();
204
- }
205
- }
206
-
207
- const TimeController = SocketFunction.register(
208
- "TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976",
209
- new TimeControllerBase(),
210
- () => ({
211
- getTrueTime: {
212
- // No hooks, as this needs to run very early on. Also, it is basically just a ping,
213
- // so it should be safe for anyone to use (we might even make it just a regular HTTPS endpoint,
214
- // or even just set up a dedicated domain for this).
215
- noDefaultHooks: true,
216
- noClientHooks: true,
217
- },
218
- }),
219
- () => ({}),
220
- {
221
- // NOTE: Autoexpose, because our exposed endpoints are incredibly lightweight
222
- // (just a ping), and don't expose really expose any data.
223
- // noAutoExpose: true
224
- }
1
+ import { SocketFunction } from "../SocketFunction";
2
+ import { blue, green, red, yellow } from "../src/formatting/logColors";
3
+ import { isNode } from "../src/misc";
4
+
5
+ // IMPOTRANT! We don't ensure that the times of return are unique. We cannot ensure they are unique because the amount of precision is only about ten thousand date times per millisecond, Which would mean if the calling code called date.now frequently enough, which doesn't even have to be that frequent, it could slowly drift farther and farther ahead of the real time, which would be really bad.
6
+
7
+ module.allowclient = true;
8
+
9
+
10
+ const UPDATE_VERIFY_COUNT = 3;
11
+
12
+ // Configuration for cross-process synchronization
13
+ const UPDATE_TRANSITION_GAP = 1000 * 60 * 20; // 5 minutes between current and next
14
+ const UPDATE_CHECK_INTERVAL = 1000 * 60 * 5; // Check every 1 minute
15
+ const DEBUG_TIME_SYNC = false; // Enable debug logging for time synchronization
16
+
17
+ // Time can never go backwards, but we can run at a slower rate until the output time allows
18
+ // the real time to catch up with it.
19
+ const MINIMUM_TIME_RATE = 0.5;
20
+
21
+ const THROW_ON_ERROR = false;
22
+
23
+ // Hugely important as if we don't synchronize between processes, it means our logs are going to be confusing and out of order.
24
+ // - Of course, cross-machine, the logs could be out of order. However, due to the latency between machines, that's less likely. The latency will probably be a few milliseconds, and hopefully, our time isn't more than a few milliseconds off of the real time. However, between processes, the latency could easily be microseconds, and our time will absolutely certainly be microseconds off of the real time.
25
+ let USE_LMDB_PROCESS_SYNC = true;
26
+
27
+ function debugLog(...args: any[]) {
28
+ if (DEBUG_TIME_SYNC) {
29
+ console.log("[TimeSync]", ...args);
30
+ }
31
+ }
32
+
33
+ type TimeOffsetData = {
34
+ lastOffset: number;
35
+ lastUpdateTime: number;
36
+ offset: number;
37
+ updateTime: number;
38
+ nextOffset: number;
39
+ nextUpdateTime: number;
40
+ };
41
+
42
+ let cachedTimeOffsetData: TimeOffsetData | undefined = undefined;
43
+ let didFirstTimeSync = false;
44
+ let onFirstTimeSync!: () => void;
45
+ let firstTimeSyncPromise = new Promise<void>((resolve) => {
46
+ onFirstTimeSync = resolve;
47
+ });
48
+
49
+ const baseGetTime = Date.now;
50
+ let lastTime = 0;
51
+ let lastBaseTime = 0;
52
+
53
+ export function getTimeComponentsDetailed(): {
54
+ systemTime: number;
55
+ fromOffset: number;
56
+ toOffset: number;
57
+ fromTime: number;
58
+ toTime: number;
59
+ } {
60
+ const systemTime = baseGetTime();
61
+ const data = cachedTimeOffsetData;
62
+
63
+ if (!data) {
64
+ return {
65
+ systemTime,
66
+ fromOffset: 0,
67
+ toOffset: 0,
68
+ fromTime: systemTime,
69
+ toTime: systemTime,
70
+ };
71
+ }
72
+
73
+ if (systemTime < data.lastUpdateTime) {
74
+ // Before everything (shouldn't happen)
75
+ if (THROW_ON_ERROR) throw new Error(`systemTime ${systemTime} is before lastUpdateTime ${data.lastUpdateTime}`);
76
+ return {
77
+ systemTime,
78
+ fromOffset: data.lastOffset,
79
+ toOffset: data.lastOffset,
80
+ fromTime: data.lastUpdateTime,
81
+ toTime: data.lastUpdateTime,
82
+ };
83
+ } else if (systemTime < data.updateTime) {
84
+ // Smear between lastOffset and offset
85
+ return {
86
+ systemTime,
87
+ fromOffset: data.lastOffset,
88
+ toOffset: data.offset,
89
+ fromTime: data.lastUpdateTime,
90
+ toTime: data.updateTime,
91
+ };
92
+ } else if (systemTime < data.nextUpdateTime) {
93
+ // Smear between offset and nextOffset
94
+ return {
95
+ systemTime,
96
+ fromOffset: data.offset,
97
+ toOffset: data.nextOffset,
98
+ fromTime: data.updateTime,
99
+ toTime: data.nextUpdateTime,
100
+ };
101
+ } else {
102
+ // Past everything (shouldn't happen often)
103
+ if (THROW_ON_ERROR) throw new Error(`systemTime ${systemTime} is past nextUpdateTime ${data.nextUpdateTime}`);
104
+ return {
105
+ systemTime,
106
+ fromOffset: data.nextOffset,
107
+ toOffset: data.nextOffset,
108
+ fromTime: data.nextUpdateTime,
109
+ toTime: data.nextUpdateTime,
110
+ };
111
+ }
112
+ }
113
+
114
+ export function getTimeComponents(): { systemTime: number; offset: number } {
115
+ const detailed = getTimeComponentsDetailed();
116
+ const elapsed = detailed.systemTime - detailed.fromTime;
117
+ const duration = detailed.toTime - detailed.fromTime;
118
+ const fraction = duration > 0 ? Math.min(1, elapsed / duration) : 0;
119
+ const offset = detailed.fromOffset + (detailed.toOffset - detailed.fromOffset) * fraction;
120
+ return { systemTime: detailed.systemTime, offset };
121
+ }
122
+
123
+ export function getTrueTime() {
124
+ const { systemTime, offset } = getTimeComponents();
125
+ let time = systemTime + offset;
126
+
127
+ // Only adjust time once we have a time offset. Otherwise systems with a really bad clock
128
+ // might take days be correct. It is better for the time to jump once at startup, rather
129
+ // than be off by days, for days at a time.
130
+ if (lastTime && offset) {
131
+ if (time < lastTime) {
132
+ let diff = systemTime - lastBaseTime;
133
+ if (diff >= 0) {
134
+ // Some time passed, so we have a baseline for how much to increase the time by.
135
+ // This allows the real time to catch up with our time naturally.
136
+ time = lastTime + diff * MINIMUM_TIME_RATE;
137
+ } else {
138
+ // The issue is the system time going backwards. In this case, allow the time to change
139
+ }
140
+ }
141
+ }
142
+ lastTime = time;
143
+ lastBaseTime = systemTime;
144
+ return time;
145
+ }
146
+ export function getTrueTimeOffset() {
147
+ const { offset } = getTimeComponents();
148
+ return offset;
149
+ }
150
+ export function waitForFirstTimeSync(): Promise<void> | undefined {
151
+ if (didFirstTimeSync) return undefined;
152
+ return firstTimeSyncPromise;
153
+ }
154
+ let shimmed = false;
155
+ export function shimDateNow() {
156
+ if (shimmed) return;
157
+ shimmed = true;
158
+ Date.now = getTrueTime;
159
+ }
160
+ export function getBrowserTime() {
161
+ return baseGetTime();
162
+ }
163
+
164
+ export function setGetTimeOffsetBase(base: () => Promise<number>) {
165
+ getTimeOffsetBase = base;
166
+ }
167
+
168
+ async function defaultGetTimeOffset(): Promise<number> {
169
+ if (!isNode()) {
170
+ let sendTime = baseGetTime();
171
+ let serverTrueTime = await TimeController.nodes[SocketFunction.browserNodeId()].getTrueTime();
172
+ let systemTime = baseGetTime();
173
+ let predictedServerToClientLatency = (systemTime - sendTime) / 2;
174
+ let trueTimeRightNow = serverTrueTime + predictedServerToClientLatency;
175
+ return trueTimeRightNow - systemTime;
176
+ }
177
+
178
+ const dgram = await import("dgram");
179
+ const NTP_SERVER = "time.google.com";
180
+ const NTP_PORT = 123;
181
+ const NTP_PACKET_SIZE = 48;
182
+ const NTP_EPOCH_OFFSET = 2208988800000; // Number of milliseconds between 1900-01-01 and 1970-01-01
183
+ return new Promise((resolve, reject) => {
184
+ const client = dgram.createSocket("udp4");
185
+ const message = Buffer.alloc(NTP_PACKET_SIZE);
186
+
187
+ // Set the first byte to represent NTP client request (LI = 0, VN = 3, Mode = 3)
188
+ message[0] = 0x1B;
189
+
190
+ const sendTime = baseGetTime();
191
+
192
+ client.send(message, 0, message.length, NTP_PORT, NTP_SERVER);
193
+ client.on("error", (err) => {
194
+ client.close();
195
+ reject(err);
196
+ });
197
+
198
+ client.on("message", (msg) => {
199
+ const receiveTime = baseGetTime();
200
+
201
+ // Extract the transmit timestamp from the server response
202
+ const transmitTimestampSeconds = msg.readUInt32BE(40);
203
+ const transmitTimestampFraction = msg.readUInt32BE(44);
204
+ const transmitTimestamp = (transmitTimestampSeconds * 1000) + (transmitTimestampFraction * 1000 / 0x100000000) - NTP_EPOCH_OFFSET;
205
+
206
+ const predictedServerToClientLatency = (receiveTime - sendTime) / 2;
207
+
208
+ // Calculate the offset
209
+ const systemTime = baseGetTime();
210
+ const actualTime = transmitTimestamp + predictedServerToClientLatency;
211
+ const offset = actualTime - systemTime;
212
+
213
+ client.close();
214
+ resolve(offset);
215
+ });
216
+ });
217
+ }
218
+
219
+ let timeOffsetDb: import("lmdb").RootDatabase<TimeOffsetData, string> | undefined = undefined;
220
+ async function getTimeOffsetDb() {
221
+ if (!USE_LMDB_PROCESS_SYNC) return undefined;
222
+ if (timeOffsetDb) return timeOffsetDb;
223
+ if (!isNode()) return undefined;
224
+
225
+ try {
226
+ const lmdb = await import("lmdb");
227
+ const path = await import("path");
228
+ const os = await import("os");
229
+
230
+ const dbPath = path.join(os.tmpdir(), "socket-function-time-offset-2");
231
+ timeOffsetDb = lmdb.open<TimeOffsetData, string>({
232
+ path: dbPath,
233
+ // Enable versioning for conditional writes
234
+ useVersions: true,
235
+ });
236
+ return timeOffsetDb;
237
+ } catch (e) {
238
+ console.error("Error opening LMDB database:", e);
239
+ return undefined;
240
+ }
241
+ }
242
+
243
+ async function getTimeOffsetFromLmdb(): Promise<{
244
+ data: TimeOffsetData;
245
+ version: number;
246
+ } | undefined> {
247
+ if (!isNode() || !USE_LMDB_PROCESS_SYNC) {
248
+ // Skip LMDB for browsers or if disabled
249
+ return undefined;
250
+ }
251
+
252
+ try {
253
+ const db = await getTimeOffsetDb();
254
+ if (!db) return undefined;
255
+
256
+ const entry = await db.getEntry("timeOffset"); // Gets {value, version} atomically
257
+ if (!entry) return undefined;
258
+
259
+ const data = entry.value;
260
+ const version = entry.version;
261
+
262
+ if (data &&
263
+ typeof version === "number" &&
264
+ typeof data.lastOffset === "number" &&
265
+ typeof data.lastUpdateTime === "number" &&
266
+ typeof data.offset === "number" &&
267
+ typeof data.updateTime === "number" &&
268
+ typeof data.nextOffset === "number" &&
269
+ typeof data.nextUpdateTime === "number") {
270
+ return { data, version };
271
+ }
272
+ return undefined;
273
+ } catch (e) {
274
+ console.error("Error reading from LMDB database:", e);
275
+ return undefined;
276
+ }
277
+ }
278
+
279
+ async function setTimeOffsetInLmdb(
280
+ data: TimeOffsetData,
281
+ expectedVersion: number
282
+ ): Promise<boolean> {
283
+ try {
284
+ const db = await getTimeOffsetDb();
285
+ if (!db) return false;
286
+
287
+ // Atomic conditional write - only succeeds if version matches expectedVersion
288
+ // Use random version to minimize collision probability on retries
289
+ const newVersion = Math.random();
290
+
291
+ // Conditional write with version check
292
+ const success = await db.ifVersion("timeOffset", expectedVersion, () => {
293
+ return db.put("timeOffset", data, newVersion);
294
+ });
295
+
296
+ return success !== undefined;
297
+ } catch (e) {
298
+ console.error("Error writing to LMDB database:", e);
299
+ return false;
300
+ }
301
+ }
302
+
303
+ let getTimeOffsetBase: () => Promise<number> = defaultGetTimeOffset;
304
+
305
+ async function fetchNewOffset(): Promise<number> {
306
+ let offsets: number[] = [];
307
+ for (let i = 0; i < UPDATE_VERIFY_COUNT; i++) {
308
+ try {
309
+ offsets.push(await getTimeOffsetBase());
310
+ } catch (e) {
311
+ console.error("Error getting time offset:", e);
312
+ }
313
+ }
314
+
315
+ if (offsets.length === 0) {
316
+ // All calls failed, return 0 as fallback
317
+ return 0;
318
+ }
319
+
320
+ // Pick the middle offset
321
+ offsets.sort((a, b) => a - b);
322
+ let offset = offsets[Math.floor(offsets.length / 2)];
323
+
324
+ // Log if offset is significant
325
+ let offsetRound = Math.abs(Math.round(offset));
326
+ let offsetColored = (
327
+ Math.abs(offset) > 600 && red(offsetRound + "ms")
328
+ || Math.abs(offset) > 300 && yellow(offsetRound + "ms")
329
+ || green(offsetRound + "ms")
330
+ );
331
+ if (Math.abs(offset) > 500) {
332
+ console.log(`${blue("Synchronized time")}, local clock was ${offset > 0 ? "behind" : "ahead"} by ${offsetColored} @ ${blue(Date.now() + "")}`);
333
+ }
334
+
335
+ return offset;
336
+ }
337
+
338
+ let updatingOffset = false;
339
+ async function updateTimeOffset() {
340
+ if (updatingOffset) return;
341
+ updatingOffset = true;
342
+
343
+ try {
344
+ const db = await getTimeOffsetDb();
345
+ if (!db) {
346
+ // IMPORTANT: Always use baseGetTime() for scheduling, never getTrueTime().
347
+ // Our update schedule must be based on the stable system clock, not the
348
+ // offset-adjusted time which changes as we synchronize.
349
+ const currentTime = baseGetTime();
350
+ let cachedData = cachedTimeOffsetData;
351
+ if (cachedData && currentTime >= cachedData.nextUpdateTime) {
352
+ // Past the end - reset and reinitialize
353
+ cachedData = undefined;
354
+ debugLog("Past nextUpdateTime, resetting");
355
+ }
356
+
357
+ if (!cachedData) {
358
+ // First time initialization
359
+ const offset = await fetchNewOffset();
360
+ cachedTimeOffsetData = {
361
+ lastOffset: offset,
362
+ lastUpdateTime: currentTime,
363
+ offset: offset,
364
+ updateTime: currentTime + UPDATE_TRANSITION_GAP,
365
+ nextOffset: offset,
366
+ nextUpdateTime: currentTime + UPDATE_TRANSITION_GAP * 2,
367
+ };
368
+ debugLog("Initialized - time offset:", offset, "ms, next update in", UPDATE_TRANSITION_GAP, "ms");
369
+ } else if (currentTime >= cachedData.updateTime) {
370
+ // Time to rotate
371
+ const newOffset = await fetchNewOffset();
372
+ cachedTimeOffsetData = {
373
+ lastOffset: cachedData.offset,
374
+ lastUpdateTime: cachedData.updateTime,
375
+ offset: cachedData.nextOffset,
376
+ updateTime: cachedData.nextUpdateTime,
377
+ nextOffset: newOffset,
378
+ nextUpdateTime: cachedData.nextUpdateTime + UPDATE_TRANSITION_GAP,
379
+ };
380
+ const timeUntilNext = cachedTimeOffsetData.nextUpdateTime - baseGetTime();
381
+ debugLog("Advancing time offset - current:", cachedTimeOffsetData.offset, "ms, next:", cachedTimeOffsetData.nextOffset, "ms, next update in", timeUntilNext, "ms");
382
+ }
383
+
384
+ if (!didFirstTimeSync) {
385
+ didFirstTimeSync = true;
386
+ onFirstTimeSync();
387
+ }
388
+ return;
389
+ }
390
+
391
+ // At this point: Node.js, LMDB enabled and working
392
+ // Main LMDB path with atomic synchronization
393
+ while (true) {
394
+ const entry = await getTimeOffsetFromLmdb();
395
+ // IMPORTANT: Always use baseGetTime() for scheduling, never getTrueTime().
396
+ // Our update schedule must be based on the stable system clock, not the
397
+ // offset-adjusted time which changes as we synchronize.
398
+ const currentTime = baseGetTime();
399
+
400
+ let cachedData = entry?.data;
401
+ const readVersion = entry?.version;
402
+
403
+ if (cachedData && currentTime >= cachedData.nextUpdateTime) {
404
+ // Past the end - reset and reinitialize
405
+ cachedData = undefined;
406
+ debugLog("Past nextUpdateTime, resetting");
407
+ }
408
+
409
+ if (!cachedData || !readVersion) {
410
+ // First time initialization - use conditional write to handle race
411
+ const offset = await fetchNewOffset();
412
+ const initData: TimeOffsetData = {
413
+ lastOffset: offset,
414
+ lastUpdateTime: currentTime,
415
+ offset: offset,
416
+ updateTime: currentTime + UPDATE_TRANSITION_GAP,
417
+ nextOffset: offset,
418
+ nextUpdateTime: currentTime + UPDATE_TRANSITION_GAP * 2,
419
+ };
420
+
421
+ const newVersion = Math.random();
422
+
423
+ if (readVersion) {
424
+ const success = await setTimeOffsetInLmdb(initData, readVersion);
425
+ if (!success) {
426
+ debugLog("Lost the race, retrying");
427
+ // Lost the race, retry
428
+ continue;
429
+ }
430
+ console.log("Successfully wrote atomic reset");
431
+ break;
432
+ }
433
+
434
+ // Try to write our data
435
+ await db.put("timeOffset", initData, newVersion);
436
+
437
+ // Read back to see what actually got written
438
+ const actualEntry = await db.getEntry("timeOffset");
439
+ if (actualEntry?.version !== newVersion) {
440
+ // Lost the race, another process wrote after us
441
+ // Retry from the top to read their data
442
+ debugLog("Value was changed by another process, retrying");
443
+ continue;
444
+ }
445
+
446
+ // We won the race, use our data
447
+ cachedTimeOffsetData = initData;
448
+ debugLog("Initialized - time offset:", offset, "ms, next update in", UPDATE_TRANSITION_GAP, "ms");
449
+ break;
450
+ }
451
+
452
+ if (currentTime >= cachedData.updateTime) {
453
+ // Time to rotate
454
+ const newOffset = await fetchNewOffset();
455
+ const newData: TimeOffsetData = {
456
+ lastOffset: cachedData.offset,
457
+ lastUpdateTime: cachedData.updateTime,
458
+ offset: cachedData.nextOffset,
459
+ updateTime: cachedData.nextUpdateTime,
460
+ nextOffset: newOffset,
461
+ nextUpdateTime: cachedData.nextUpdateTime + UPDATE_TRANSITION_GAP,
462
+ };
463
+
464
+ const success = await setTimeOffsetInLmdb(newData, readVersion);
465
+ if (!success) {
466
+ // Lost the race, retry
467
+ continue;
468
+ }
469
+
470
+ const timeUntilNext = newData.nextUpdateTime - baseGetTime();
471
+ debugLog("Advancing time offset - current:", newData.offset, "ms, next:", newData.nextOffset, "ms, next update in", timeUntilNext, "ms");
472
+ cachedTimeOffsetData = newData;
473
+ break;
474
+ } else {
475
+ cachedTimeOffsetData = cachedData;
476
+ const timeUntilNext = cachedData.updateTime - baseGetTime();
477
+ debugLog("Loaded from LMDB - current:", cachedData.offset, "ms, next:", cachedData.nextOffset, "ms, next update in", timeUntilNext, "ms");
478
+ break;
479
+ }
480
+ }
481
+
482
+ if (!didFirstTimeSync) {
483
+ didFirstTimeSync = true;
484
+ onFirstTimeSync();
485
+ }
486
+ } finally {
487
+ updatingOffset = false;
488
+ }
489
+ }
490
+
491
+ setInterval(() => {
492
+ updateTimeOffset().catch((e) => {
493
+ console.warn("Error updating time offset:", e);
494
+ });
495
+ }, UPDATE_CHECK_INTERVAL);
496
+ setImmediate(() => {
497
+ updateTimeOffset().catch((e) => {
498
+ console.error("Error updating initial offset:", e);
499
+ });
500
+ });
501
+
502
+
503
+ class TimeControllerBase {
504
+ public async getTrueTime() {
505
+ await waitForFirstTimeSync();
506
+ return getTrueTime();
507
+ }
508
+ }
509
+
510
+ const TimeController = SocketFunction.register(
511
+ "TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976",
512
+ new TimeControllerBase(),
513
+ () => ({
514
+ getTrueTime: {
515
+ // No hooks, as this needs to run very early on. Also, it is basically just a ping,
516
+ // so it should be safe for anyone to use (we might even make it just a regular HTTPS endpoint,
517
+ // or even just set up a dedicated domain for this).
518
+ noDefaultHooks: true,
519
+ noClientHooks: true,
520
+ },
521
+ }),
522
+ () => ({}),
523
+ {
524
+ // NOTE: Autoexpose, because our exposed endpoints are incredibly lightweight
525
+ // (just a ping), and don't expose really expose any data.
526
+ // noAutoExpose: true
527
+ }
225
528
  );