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 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 locationNode() {
286
- return SocketFunction.connect({ address: location.hostname, port: +location.port });
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.locationNode()]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socket-function",
3
- "version": "0.12.9",
3
+ "version": "0.12.10",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
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
- if (isNode()) throw new Error(`Cannot get nodeId from location, as we are running in NodeJS`);
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
+ );