socket-function 1.1.6 → 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.
@@ -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
@@ -1412,6 +1412,17 @@ declare module "socket-function/test" {
1412
1412
  }
1413
1413
 
1414
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
+ };
1415
1426
  export declare function getTrueTime(): number;
1416
1427
  export declare function getTrueTimeOffset(): number;
1417
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.6",
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,7 +17,8 @@
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",
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
+ });
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
  );