socket-function 0.12.8 → 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
  ;
@@ -42,6 +41,7 @@ declare global {
42
41
  hotreload?: boolean;
43
42
  /** Only hotreloads the file in the browser. */
44
43
  hotreloadBrowser?: boolean;
44
+ noserverhotreload?: boolean;
45
45
  }
46
46
  }
47
47
  }
@@ -70,7 +70,7 @@ const hotReloadModule = cache((module: NodeJS.Module) => {
70
70
  if (curr.mtime.getTime() === prev.mtime.getTime()) return;
71
71
  console.log(`Hot reloading due to change: ${module.filename}`);
72
72
  module.updateContents?.();
73
- if (isNode()) {
73
+ if (isNode() && !module.noserverhotreload) {
74
74
  if (
75
75
  module.hotreload
76
76
  // A fairly big hack (as this could just be in a string, or something similar), but... it also VERY useful
@@ -164,5 +164,11 @@ export const HotReloadController = SocketFunction.register(
164
164
  () => ({
165
165
  watchFiles: {},
166
166
  fileUpdated: {}
167
- })
167
+ }),
168
+ () => ({
169
+
170
+ }),
171
+ {
172
+ noAutoExpose: true,
173
+ }
168
174
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socket-function",
3
- "version": "0.12.8",
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",
@@ -40,7 +40,7 @@ declare global {
40
40
  interface Window {
41
41
  clientsideBootTime: number;
42
42
  }
43
- var suppressUnexpectedModuleWarning: boolean;
43
+ var suppressUnexpectedModuleWarning: number | undefined;
44
44
  }
45
45
 
46
46
  export interface SerializedModule {
@@ -1,6 +1,8 @@
1
1
  (function () {
2
2
  //# sourceURL=require.js
3
3
 
4
+ let startTime = Date.now();
5
+
4
6
  // Globals
5
7
  Object.assign(window, {
6
8
  process: {
@@ -144,7 +146,7 @@
144
146
  }
145
147
  }
146
148
  async function rootRequireMultiple(requests) {
147
- console.log(`%cimport(${requests.join(", ")})`, "color: orange");
149
+ console.log(`%cimport(${requests.join(", ")}) at ${Date.now() - startTime}ms`, "color: orange");
148
150
 
149
151
  let time = Date.now();
150
152
 
@@ -216,7 +218,7 @@
216
218
  let moduleCount = Object.values(modules).filter(x => x.source).length;
217
219
  let requireModuleCount = Object.values(modules).filter(x => !x.source).length;
218
220
  let dependenciesOnlyText = requireModuleCount ? ` (+${requireModuleCount} dependencies only)` : "";
219
- console.log(`%cimport(${requests.join(", ")}) download ${time}ms, ${Math.ceil(rawText.length / 1024)}KB, ${moduleCount} modules${dependenciesOnlyText}`, "color: green");
221
+ console.log(`%cimport(${requests.join(", ")}) finished download ${time}ms, ${Math.ceil(rawText.length / 1024)}KB, ${moduleCount} modules${dependenciesOnlyText} at ${Date.now() - startTime}ms`, "color: green");
220
222
 
221
223
  time = Date.now();
222
224
 
@@ -234,7 +236,7 @@
234
236
  return requestsResolvedPaths.map(x => getModule(x));
235
237
  } finally {
236
238
  time = Date.now() - time;
237
- console.log(`%cimport(${requests.join(", ")}) evaluate ${time}ms (${moduleCount} modules)`, "color: blue");
239
+ console.log(`%cimport(${requests.join(", ")}) finished evaluate ${time}ms (${moduleCount} modules) at ${Date.now() - startTime}ms`, "color: blue");
238
240
  }
239
241
  }
240
242
 
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
+ );