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 +38 -21
- package/package.json +1 -1
- package/require/require.js +1 -2
- package/spec.txt +4 -0
- package/src/CallFactory.ts +56 -7
- package/src/callHTTPHandler.ts +45 -14
- package/src/misc.ts +28 -0
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
|
|
50
|
-
if (
|
|
51
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
package/require/require.js
CHANGED
|
@@ -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
|
|
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
|
package/src/CallFactory.ts
CHANGED
|
@@ -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:
|
|
119
|
+
data: Buffer;
|
|
115
120
|
call: InternalCallType;
|
|
116
121
|
reconnectTimeout: number | undefined;
|
|
117
|
-
callback: (
|
|
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:
|
|
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
|
-
|
|
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));
|
package/src/callHTTPHandler.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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...
|