socket-function 0.12.9 → 0.12.10
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.
- package/SocketFunction.ts +13 -2
- package/hot/HotReloadController.ts +8 -3
- package/package.json +1 -1
- package/src/nodeCache.ts +1 -2
- package/time/trueTimeShim.ts +172 -0
package/SocketFunction.ts
CHANGED
|
@@ -15,6 +15,12 @@ import { JSONLACKS } from "./src/JSONLACKS/JSONLACKS";
|
|
|
15
15
|
import "./SetProcessVariables";
|
|
16
16
|
import cborx from "cbor-x";
|
|
17
17
|
import { setFlag } from "./require/compileFlags";
|
|
18
|
+
import { shimDateNow, waitForFirstTimeSync } from "./time/trueTimeShim";
|
|
19
|
+
import { isNode } from "./src/misc";
|
|
20
|
+
|
|
21
|
+
/** Always shim Date.now(), because we usually DO want an accurate time... */
|
|
22
|
+
shimDateNow();
|
|
23
|
+
|
|
18
24
|
setFlag(require, "cbor-x", "allowclient", true);
|
|
19
25
|
let cborxInstance = new cborx.Encoder({ structuredClone: true });
|
|
20
26
|
|
|
@@ -241,6 +247,8 @@ export class SocketFunction {
|
|
|
241
247
|
this.mountedIP = config.ip;
|
|
242
248
|
}
|
|
243
249
|
|
|
250
|
+
await waitForFirstTimeSync();
|
|
251
|
+
|
|
244
252
|
// Wait for any additionals functions to expose themselves
|
|
245
253
|
await delay("immediate");
|
|
246
254
|
|
|
@@ -282,8 +290,11 @@ export class SocketFunction {
|
|
|
282
290
|
return getNodeId(location.address, location.port);
|
|
283
291
|
}
|
|
284
292
|
|
|
285
|
-
public static
|
|
286
|
-
|
|
293
|
+
public static browserNodeId() {
|
|
294
|
+
if (!isNode()) {
|
|
295
|
+
throw new Error("Cannot get browser nodeId on server");
|
|
296
|
+
}
|
|
297
|
+
return SocketFunction.connect({ address: location.hostname, port: +location.port || 443 });
|
|
287
298
|
}
|
|
288
299
|
|
|
289
300
|
public static addGlobalHook(hook: SocketFunctionHook<SocketExposedInterface>) {
|
|
@@ -14,11 +14,10 @@ import { magenta, red } from "../src/formatting/logColors";
|
|
|
14
14
|
* - Triggers a reload server, for modules marked with `module.hotreload`
|
|
15
15
|
*/
|
|
16
16
|
export function watchFilesAndTriggerHotReloading(noAutomaticBrowserWatch = false) {
|
|
17
|
-
|
|
18
17
|
SocketFunction.expose(HotReloadController);
|
|
19
18
|
if (!isNode()) {
|
|
20
19
|
if (!noAutomaticBrowserWatch) {
|
|
21
|
-
HotReloadController.nodes[SocketFunction.
|
|
20
|
+
HotReloadController.nodes[SocketFunction.browserNodeId()]
|
|
22
21
|
.watchFiles()
|
|
23
22
|
.catch(e => console.error("watchFiles error", e))
|
|
24
23
|
;
|
|
@@ -165,5 +164,11 @@ export const HotReloadController = SocketFunction.register(
|
|
|
165
164
|
() => ({
|
|
166
165
|
watchFiles: {},
|
|
167
166
|
fileUpdated: {}
|
|
168
|
-
})
|
|
167
|
+
}),
|
|
168
|
+
() => ({
|
|
169
|
+
|
|
170
|
+
}),
|
|
171
|
+
{
|
|
172
|
+
noAutoExpose: true,
|
|
173
|
+
}
|
|
169
174
|
);
|
package/package.json
CHANGED
package/src/nodeCache.ts
CHANGED
|
@@ -19,8 +19,7 @@ export function getNodeId(domain: string, port: number): string {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function getNodeIdFromLocation() {
|
|
22
|
-
|
|
23
|
-
return getNodeId(location.hostname, location.port ? parseInt(location.port) : 443);
|
|
22
|
+
return SocketFunction.browserNodeId();
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
/** A nodeId not available for reconnecting. */
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { SocketFunction } from "../SocketFunction";
|
|
2
|
+
import { isNode } from "../src/misc";
|
|
3
|
+
|
|
4
|
+
module.allowclient = true;
|
|
5
|
+
|
|
6
|
+
const UPDATE_INTERVAL = 1000 * 60 * 10;
|
|
7
|
+
// More frequent, to ensure we don't run into major issues with sleep (coming back from sleep,
|
|
8
|
+
// having the interval not be fired immediately, and having the time be off for a few minutes).
|
|
9
|
+
const UPDATE_SUB_INTERVAL = 1000 * 10;
|
|
10
|
+
// Smearing is important, otherwise some performance timing (especially on load) can easily be off
|
|
11
|
+
// by a few hundred milliseconds. The current smear parameters will mean even with 1s of offset
|
|
12
|
+
// we only add 10ms every 100ms, so worst case scenario some timing that takes 0ms will take 10ms.
|
|
13
|
+
const UPDATE_SMEAR_TICK_DURATION = 100;
|
|
14
|
+
const UPDATE_SMEAR_TICK_COUNT = 100;
|
|
15
|
+
const UPDATE_VERIFY_COUNT = 3;
|
|
16
|
+
|
|
17
|
+
let trueTimeOffset = 0;
|
|
18
|
+
let didFirstTimeSync = false;
|
|
19
|
+
let onFirstTimeSync!: () => void;
|
|
20
|
+
let firstTimeSyncPromise = new Promise<void>((resolve) => {
|
|
21
|
+
onFirstTimeSync = resolve;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const baseGetTime = Date.now;
|
|
25
|
+
export function getTrueTime() {
|
|
26
|
+
return baseGetTime() + trueTimeOffset;
|
|
27
|
+
}
|
|
28
|
+
export function getTrueTimeOffset() {
|
|
29
|
+
return trueTimeOffset;
|
|
30
|
+
}
|
|
31
|
+
export function waitForFirstTimeSync() {
|
|
32
|
+
return firstTimeSyncPromise;
|
|
33
|
+
}
|
|
34
|
+
export function shimDateNow() {
|
|
35
|
+
Date.now = getTrueTime;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function setGetTimeOffsetBase(base: () => Promise<number>) {
|
|
39
|
+
getTimeOffsetBase = base;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async function defaultGetTimeOffset(): Promise<number> {
|
|
44
|
+
if (!isNode()) {
|
|
45
|
+
let sendTime = baseGetTime();
|
|
46
|
+
let serverTrueTime = await TimeController.nodes[SocketFunction.browserNodeId()].getTrueTime();
|
|
47
|
+
let systemTime = baseGetTime();
|
|
48
|
+
let predictedServerToClientLatency = (systemTime - sendTime) / 2;
|
|
49
|
+
let trueTimeRightNow = serverTrueTime + predictedServerToClientLatency;
|
|
50
|
+
return trueTimeRightNow - systemTime;
|
|
51
|
+
}
|
|
52
|
+
const dgram = await import("dgram");
|
|
53
|
+
const NTP_SERVER = "time.google.com";
|
|
54
|
+
const NTP_PORT = 123;
|
|
55
|
+
const NTP_PACKET_SIZE = 48;
|
|
56
|
+
const NTP_EPOCH_OFFSET = 2208988800000; // Number of milliseconds between 1900-01-01 and 1970-01-01
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const client = dgram.createSocket("udp4");
|
|
59
|
+
const message = Buffer.alloc(NTP_PACKET_SIZE);
|
|
60
|
+
|
|
61
|
+
// Set the first byte to represent NTP client request (LI = 0, VN = 3, Mode = 3)
|
|
62
|
+
message[0] = 0x1B;
|
|
63
|
+
|
|
64
|
+
const sendTime = baseGetTime();
|
|
65
|
+
|
|
66
|
+
client.send(message, 0, message.length, NTP_PORT, NTP_SERVER);
|
|
67
|
+
client.on("error", (err) => {
|
|
68
|
+
client.close();
|
|
69
|
+
reject(err);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
client.on("message", (msg) => {
|
|
73
|
+
const receiveTime = baseGetTime();
|
|
74
|
+
|
|
75
|
+
// Extract the transmit timestamp from the server response
|
|
76
|
+
const transmitTimestampSeconds = msg.readUInt32BE(40);
|
|
77
|
+
const transmitTimestampFraction = msg.readUInt32BE(44);
|
|
78
|
+
const transmitTimestamp = (transmitTimestampSeconds * 1000) + (transmitTimestampFraction * 1000 / 0x100000000) - NTP_EPOCH_OFFSET;
|
|
79
|
+
|
|
80
|
+
const predictedServerToClientLatency = (receiveTime - sendTime) / 2;
|
|
81
|
+
|
|
82
|
+
// Calculate the offset
|
|
83
|
+
const systemTime = baseGetTime();
|
|
84
|
+
const actualTime = transmitTimestamp + predictedServerToClientLatency;
|
|
85
|
+
const offset = actualTime - systemTime;
|
|
86
|
+
|
|
87
|
+
client.close();
|
|
88
|
+
resolve(offset);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let getTimeOffsetBase: () => Promise<number> = defaultGetTimeOffset;
|
|
94
|
+
let updatingOffset = false;
|
|
95
|
+
async function updateTimeOffset() {
|
|
96
|
+
if (updatingOffset) return;
|
|
97
|
+
updatingOffset = true;
|
|
98
|
+
try {
|
|
99
|
+
let offsets: number[] = [];
|
|
100
|
+
for (let i = 0; i < UPDATE_VERIFY_COUNT; i++) {
|
|
101
|
+
try {
|
|
102
|
+
offsets.push(await getTimeOffsetBase());
|
|
103
|
+
} catch (e) {
|
|
104
|
+
console.error("Error getting time offset:", e);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// If we have no offsets, it likely means every call errored out (probably because the network is down).
|
|
108
|
+
// This is fine, just don't update (DO register the first sync as being done, otherwise calling code
|
|
109
|
+
// might be waiting forever).
|
|
110
|
+
if (offsets.length > 0) {
|
|
111
|
+
// Pick the middle offset
|
|
112
|
+
offsets.sort((a, b) => a - b);
|
|
113
|
+
let offset = offsets[Math.floor(offsets.length / 2)];
|
|
114
|
+
|
|
115
|
+
// Smear it slowly
|
|
116
|
+
let currentSmearCount = UPDATE_SMEAR_TICK_COUNT;
|
|
117
|
+
// Update the initial time all at once, otherwise initial requests to other servers might
|
|
118
|
+
// be rejected (because they could use the system time, which could be off by a few seconds).
|
|
119
|
+
if (!didFirstTimeSync) {
|
|
120
|
+
currentSmearCount = 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let prevOffset = trueTimeOffset;
|
|
124
|
+
for (let i = 0; i < currentSmearCount; i++) {
|
|
125
|
+
let fraction = (i + 1) / currentSmearCount;
|
|
126
|
+
trueTimeOffset = prevOffset * (1 - fraction) + offset * fraction;
|
|
127
|
+
if (i < currentSmearCount - 1) {
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, UPDATE_SMEAR_TICK_DURATION));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!didFirstTimeSync) {
|
|
134
|
+
didFirstTimeSync = true;
|
|
135
|
+
onFirstTimeSync();
|
|
136
|
+
}
|
|
137
|
+
} finally {
|
|
138
|
+
updatingOffset = false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let nextUpdateTime = 0;
|
|
143
|
+
setInterval(() => {
|
|
144
|
+
if (baseGetTime() < nextUpdateTime) return;
|
|
145
|
+
nextUpdateTime = baseGetTime() + UPDATE_INTERVAL;
|
|
146
|
+
updateTimeOffset().catch((e) => {
|
|
147
|
+
console.error("Error updating time offset:", e);
|
|
148
|
+
});
|
|
149
|
+
}, UPDATE_SUB_INTERVAL);
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TimeControllerBase {
|
|
153
|
+
public async getTrueTime() {
|
|
154
|
+
await waitForFirstTimeSync();
|
|
155
|
+
return getTrueTime();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const TimeController = SocketFunction.register(
|
|
160
|
+
"TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976",
|
|
161
|
+
new TimeControllerBase(),
|
|
162
|
+
() => ({
|
|
163
|
+
getTrueTime: {},
|
|
164
|
+
}),
|
|
165
|
+
() => ({
|
|
166
|
+
}),
|
|
167
|
+
{
|
|
168
|
+
// NOTE: Autoexpose, because our exposed endpoints are incredibly lightweight
|
|
169
|
+
// (just a ping), and don't expose really expose any data.
|
|
170
|
+
// noAutoExpose: true
|
|
171
|
+
}
|
|
172
|
+
);
|