socket-function 0.7.10 → 0.7.11

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
@@ -25,6 +25,12 @@ type PickByType<T, Value> = {
25
25
  };
26
26
 
27
27
  export class SocketFunction {
28
+ public static logMessages = false;
29
+ public static compression: undefined | {
30
+ type: "gzip";
31
+ };
32
+ public static httpETagCache = false;
33
+
28
34
  public static register<
29
35
  ClassInstance extends object,
30
36
  Shape extends SocketExposedShape<SocketExposedInterface, CallContext>,
@@ -46,29 +52,40 @@ export class SocketFunction {
46
52
  registerClass(classGuid, instance as SocketExposedInterface, shape as any as SocketExposedShape);
47
53
 
48
54
  let nodeProxy = getCallProxy(classGuid, async (nodeId, functionName, args) => {
49
- let callFactory = await getCallFactoryNodeId(nodeId);
50
- if (!callFactory) {
51
- throw new Error(`Cannot reach node ${nodeId}. It might have been incorrect provided to us via another node, which should have provided us a NetworkLocation instead.`);
52
- }
53
-
54
- let shapeObj = shape[functionName];
55
- if (!shapeObj) {
56
- throw new Error(`Function ${functionName} is not in shape`);
55
+ let time = Date.now();
56
+ if (SocketFunction.logMessages) {
57
+ console.log(`START\t\t\t${classGuid}.${functionName}`);
57
58
  }
58
-
59
- let call: CallType = {
60
- classGuid,
61
- args,
62
- functionName,
63
- };
64
-
65
- let hookResult = await runClientHooks(call, shapeObj as SocketExposedShape[""]);
66
-
67
- if ("overrideResult" in hookResult) {
68
- return hookResult.overrideResult;
59
+ try {
60
+ let callFactory = await getCallFactoryNodeId(nodeId);
61
+ if (!callFactory) {
62
+ throw new Error(`Cannot reach node ${nodeId}. It might have been incorrect provided to us via another node, which should have provided us a NetworkLocation instead.`);
63
+ }
64
+
65
+ let shapeObj = shape[functionName];
66
+ if (!shapeObj) {
67
+ throw new Error(`Function ${functionName} is not in shape`);
68
+ }
69
+
70
+ let call: CallType = {
71
+ classGuid,
72
+ args,
73
+ functionName,
74
+ };
75
+
76
+ let hookResult = await runClientHooks(call, shapeObj as SocketExposedShape[""]);
77
+
78
+ if ("overrideResult" in hookResult) {
79
+ return hookResult.overrideResult;
80
+ }
81
+
82
+ return await callFactory.performCall(call);
83
+ } finally {
84
+ time = Date.now() - time;
85
+ if (SocketFunction.logMessages) {
86
+ console.log(`TIME\t${time}ms\t${classGuid}.${functionName}`);
87
+ }
69
88
  }
70
-
71
- return await callFactory.performCall(call);
72
89
  });
73
90
 
74
91
  let output: SocketRegistered = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socket-function",
3
- "version": "0.7.10",
3
+ "version": "0.7.11",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "dependencies": {
@@ -440,10 +440,9 @@
440
440
  let url = new URL(endpoint);
441
441
 
442
442
  let json = JSON.stringify(values);
443
- if (json.length < 6000 && false) {
443
+ if (json.length < 6000) {
444
444
  // NOTE: Try to use a GET, as GETs can be cached! However, if the data is too large,
445
445
  // we have to use a post, or else the request url will be too large
446
- let parameters = [];
447
446
  for (let key in values) {
448
447
  url.searchParams.set(key, JSON.stringify(values[key]));
449
448
  }
package/spec.txt CHANGED
@@ -3,6 +3,10 @@ spec.txt
3
3
  - Add a flag for rejectUnauthorized, that is off by default
4
4
  - Add the ability to specify the certs yourself (so you can specify your identity with a real cert)
5
5
  - Then use real certificates on the server
6
+ - Fix multiple clients on the same machines
7
+ - Maybe we need to exchange link information
8
+ - The login emails SHOULD redirect, if the close doesn't works
9
+ (due to copying the link instead of clicking it)
6
10
 
7
11
  - Other stuff
8
12
  - JSON buffer serialize, which generates an object, that allows for rehydration of buffers
@@ -2,16 +2,19 @@ import { CallerContext, CallType, NetworkLocation } from "../SocketFunctionTypes
2
2
  import * as ws from "ws";
3
3
  import type * as net from "net";
4
4
  import { performLocalCall } from "./callManager";
5
- import { convertErrorStackToError, isNode } from "./misc";
5
+ import { convertErrorStackToError, formatNumberSuffixed, isNode } from "./misc";
6
6
  import { createWebsocket, getNodeId, getTLSSocket } from "./nodeAuthentication";
7
7
  import debugbreak from "debugbreak";
8
8
  import http from "http";
9
+ import { SocketFunction } from "../SocketFunction";
10
+ import { gzip } from "zlib";
9
11
 
10
12
  const retryInterval = 2000;
11
13
 
12
14
  type InternalCallType = CallType & {
13
15
  seqNum: number;
14
16
  isReturn: false;
17
+ compress: boolean;
15
18
  }
16
19
 
17
20
  type InternalReturnType = {
@@ -19,6 +22,8 @@ type InternalReturnType = {
19
22
  result: unknown;
20
23
  error?: string;
21
24
  seqNum: number;
25
+ resultSize: number;
26
+ compressed: boolean;
22
27
  };
23
28
 
24
29
 
@@ -76,7 +81,7 @@ export async function callFactoryFromWS(
76
81
  export interface SenderInterface {
77
82
  nodeId?: string;
78
83
 
79
- send(data: string): void;
84
+ send(data: string | Buffer): void;
80
85
 
81
86
  on(event: "open", listener: () => void): this;
82
87
  on(event: "close", listener: (code: number, reason: Buffer) => void): this;
@@ -111,10 +116,10 @@ async function createCallFactory(
111
116
 
112
117
 
113
118
  let pendingCalls: Map<number, {
114
- data: string;
119
+ data: Buffer;
115
120
  call: InternalCallType;
116
121
  reconnectTimeout: number | undefined;
117
- callback: (result: InternalReturnType) => void;
122
+ callback: (resultJSON: InternalReturnType) => void;
118
123
  }> = new Map();
119
124
  // NOTE: It is important to make this as random as possible, to prevent
120
125
  // reconnections dues to a process being reset causing seqNum collisions
@@ -138,7 +143,7 @@ async function createCallFactory(
138
143
 
139
144
  niceConnectionName = `${niceConnectionName} (${callerContext.nodeId})`;
140
145
 
141
- async function sendWithRetry(reconnectTimeout: number | undefined, data: string) {
146
+ async function sendWithRetry(reconnectTimeout: number | undefined, data: Buffer) {
142
147
  if (!retriesEnabled) {
143
148
  webSocket.send(data);
144
149
  return;
@@ -190,6 +195,8 @@ async function createCallFactory(
190
195
  result: undefined,
191
196
  error: `Connection lost to ${niceConnectionName}`,
192
197
  seqNum: call.call.seqNum,
198
+ resultSize: 0,
199
+ compressed: false,
193
200
  });
194
201
  }
195
202
  return;
@@ -243,6 +250,8 @@ async function createCallFactory(
243
250
  result: undefined,
244
251
  error: String(e),
245
252
  seqNum: call.call.seqNum,
253
+ resultSize: 0,
254
+ compressed: false,
246
255
  });
247
256
  });
248
257
  }
@@ -278,8 +287,28 @@ async function createCallFactory(
278
287
  if (typeof message === "object" && "data" in message) {
279
288
  message = message.data;
280
289
  }
290
+ if (message instanceof Blob) {
291
+ message = Buffer.from(await message.arrayBuffer());
292
+ }
281
293
  }
282
294
  if (message instanceof Buffer || typeof message === "string") {
295
+
296
+ let resultSize = message.length;
297
+
298
+ if (message instanceof Buffer && message[0] === 0) {
299
+ // First byte of 0 means it is decompressed (as JSON can't have a first byte of 0).
300
+ (message as any) = message.slice(1);
301
+
302
+ // TODO: Add typings for DecompressionStream
303
+ let DecompressionStream = (window as any).DecompressionStream;
304
+ // https://stackoverflow.com/a/68829631/1117119
305
+ let stream = new DecompressionStream("gzip");
306
+ let blob = new Blob([message]);
307
+ let decompressedStream = (await (blob.stream() as any).pipeThrough(stream));
308
+ let arrayBuffer = await new Response(decompressedStream).arrayBuffer();
309
+ (message as any) = Buffer.from(arrayBuffer);
310
+ }
311
+
283
312
  let call = JSON.parse(message.toString()) as InternalCallType | InternalReturnType;
284
313
  if (call.isReturn) {
285
314
  let callbackObj = pendingCalls.get(call.seqNum);
@@ -287,6 +316,7 @@ async function createCallFactory(
287
316
  console.log(`Got return for unknown call ${call.seqNum}`);
288
317
  return;
289
318
  }
319
+ call.resultSize = resultSize;
290
320
  callbackObj.callback(call);
291
321
  } else {
292
322
  if (call.seqNum <= lastReceivedSeqNum) {
@@ -302,6 +332,8 @@ async function createCallFactory(
302
332
  isReturn: true,
303
333
  result,
304
334
  seqNum: call.seqNum,
335
+ resultSize: resultSize,
336
+ compressed: false,
305
337
  };
306
338
  } catch (e: any) {
307
339
  response = {
@@ -309,10 +341,23 @@ async function createCallFactory(
309
341
  result: undefined,
310
342
  seqNum: call.seqNum,
311
343
  error: e.stack,
344
+ resultSize: resultSize,
345
+ compressed: false,
312
346
  };
313
347
  }
314
348
 
315
- await sendWithRetry(call.reconnectTimeout, JSON.stringify(response));
349
+ let result: Buffer;
350
+ if (isNode() && call.compress && SocketFunction.compression?.type === "gzip") {
351
+ response.compressed = true;
352
+ result = Buffer.from(JSON.stringify(response));
353
+ result = await new Promise<Buffer>((resolve, reject) =>
354
+ gzip(result, (err, result) => err ? reject(err) : resolve(result))
355
+ );
356
+ result = Buffer.concat([new Uint8Array([0]), result]);
357
+ } else {
358
+ result = Buffer.from(JSON.stringify(response));
359
+ }
360
+ await sendWithRetry(call.reconnectTimeout, result);
316
361
  }
317
362
  return;
318
363
  }
@@ -339,10 +384,14 @@ async function createCallFactory(
339
384
  classGuid: call.classGuid,
340
385
  functionName: call.functionName,
341
386
  seqNum,
387
+ compress: !!SocketFunction.compression,
342
388
  };
343
- let data = JSON.stringify(fullCall);
389
+ let data = Buffer.from(JSON.stringify(fullCall));
344
390
  let resultPromise = new Promise((resolve, reject) => {
345
391
  let callback = (result: InternalReturnType) => {
392
+ if (SocketFunction.logMessages) {
393
+ console.log(`SIZE\t${(formatNumberSuffixed(result.resultSize) + "B").padEnd(4, " ")}\t${call.classGuid}.${call.functionName}`);
394
+ }
346
395
  pendingCalls.delete(seqNum);
347
396
  if (result.error) {
348
397
  reject(convertErrorStackToError(result.error));
@@ -7,6 +7,9 @@ import { performLocalCall } from "./callManager";
7
7
  import { getNodeIdRaw } from "./nodeAuthentication";
8
8
  import debugbreak from "debugbreak";
9
9
  import * as cookie from "cookie";
10
+ import { SocketFunction } from "../SocketFunction";
11
+ import { gzip } from "zlib";
12
+ import { formatNumberSuffixed, sha256Hash } from "./misc";
10
13
 
11
14
  const nodeIdCookie = "node-id4";
12
15
 
@@ -150,24 +153,52 @@ export async function httpCallHandler(request: http.IncomingMessage, response: h
150
153
  call
151
154
  });
152
155
 
156
+ let resultBuffer: Buffer;
153
157
  if (typeof result === "object" && result && result instanceof Buffer) {
154
- let headers = (result as HTTPResultType)[resultHeaders];
155
- if (headers) {
156
- for (let headerName in headers) {
157
- response.setHeader(headerName, headers[headerName]);
158
- }
159
- let status = headers["status"];
160
- if (status) {
161
- response.writeHead(+status);
162
- return;
163
- }
164
- }
165
- response.write(result);
158
+ resultBuffer = result;
166
159
  } else {
167
- response.write(JSON.stringify(result));
160
+ resultBuffer = Buffer.from(JSON.stringify(result));
161
+ }
162
+
163
+ let headers = (resultBuffer as HTTPResultType)[resultHeaders];
164
+ if (SocketFunction.compression?.type === "gzip" && !headers?.["Content-Encoding"]) {
165
+ if (request.headers["accept-encoding"]?.includes("gzip")) {
166
+ resultBuffer = await new Promise<Buffer>((resolve, reject) =>
167
+ gzip(resultBuffer, (err, result) => err ? reject(err) : resolve(result))
168
+ );
169
+ response.setHeader("Content-Encoding", "gzip");
170
+ }
168
171
  }
172
+
173
+
174
+ // NOTE: Our ETag caching is only to reduce data sent on the wire, we evaluate the calls
175
+ // every time (so it is strictly a wire cache, not computation cache)
176
+ response.setHeader("cache-control", "private, s-maxage=0, max-age=0, must-revalidate");
177
+ if (SocketFunction.httpETagCache) {
178
+ let hash = sha256Hash(resultBuffer);
179
+ response.setHeader("ETag", hash);
180
+ if (request.headers["if-none-match"] === hash) {
181
+ response.writeHead(304);
182
+ console.log(`CACHED HTTP response ${formatNumberSuffixed(resultBuffer.length)}B (${request.method}) ${url}`);
183
+ return;
184
+ }
185
+ }
186
+
187
+ if (headers) {
188
+ for (let headerName in headers) {
189
+ response.setHeader(headerName, headers[headerName]);
190
+ }
191
+ let status = headers["status"];
192
+ if (status) {
193
+ response.writeHead(+status);
194
+ return;
195
+ }
196
+ }
197
+ response.write(resultBuffer);
198
+ console.log(`HTTP response ${formatNumberSuffixed(resultBuffer.length)}B (${request.method}) ${url}`);
199
+
169
200
  } catch (e: any) {
170
- console.error(`Request error`, e.stack);
201
+ console.log(`HTTP error (${request.method}) ${e.stack}`);
171
202
  response.writeHead(500, String(e.message));
172
203
  } finally {
173
204
  response.end();
package/src/misc.ts CHANGED
@@ -27,6 +27,34 @@ export function isNodeTrue() {
27
27
  return isNode() as true;
28
28
  }
29
29
 
30
+ export function formatNumberSuffixed(count: number): string {
31
+ if (typeof count !== "number") return "0";
32
+ if (count < 0) {
33
+ return "-" + formatNumberSuffixed(-count);
34
+ }
35
+
36
+ let absValue = Math.abs(count);
37
+
38
+ const extraFactor = 10;
39
+ let divisor = 1;
40
+ let suffix = "";
41
+ if (absValue < 1000 * extraFactor) {
42
+
43
+ } else if (absValue < 1000 * 1000 * extraFactor) {
44
+ suffix = "K";
45
+ divisor = 1000;
46
+ } else if (absValue < 1000 * 1000 * 1000 * extraFactor) {
47
+ suffix = "M";
48
+ divisor = 1000 * 1000;
49
+ } else {
50
+ suffix = "B";
51
+ divisor = 1000 * 1000 * 1000;
52
+ }
53
+ count /= divisor;
54
+ absValue /= divisor;
55
+
56
+ return Math.round(count).toString() + suffix;
57
+ }
30
58
 
31
59
  if (isNode()) {
32
60
  // TODO: Find a better place for this...