socket-function 0.10.4 → 0.10.6
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 +8 -2
- package/SocketFunctionTypes.ts +1 -0
- package/package.json +4 -2
- package/require/RequireController.ts +57 -2
- package/require/require.js +2 -0
- package/src/CallFactory.ts +82 -13
- package/src/callHTTPHandler.ts +39 -7
- package/src/callManager.ts +4 -0
- package/src/formatting/logColors.ts +7 -5
- package/src/misc.ts +2 -26
package/SocketFunction.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference path="./require/RequireController.ts" />
|
|
2
2
|
|
|
3
|
-
import { SocketExposedInterface, SocketFunctionHook, SocketFunctionClientHook, SocketExposedShape, SocketRegistered, CallerContext, FullCallType } from "./SocketFunctionTypes";
|
|
3
|
+
import { SocketExposedInterface, SocketFunctionHook, SocketFunctionClientHook, SocketExposedShape, SocketRegistered, CallerContext, FullCallType, CallType } from "./SocketFunctionTypes";
|
|
4
4
|
import { exposeClass, registerClass, registerGlobalClientHook, registerGlobalHook, runClientHooks } from "./src/callManager";
|
|
5
5
|
import { SocketServerConfig, startSocketServer } from "./src/webSocketServer";
|
|
6
6
|
import { getCallFactory, getCreateCallFactory, getNodeId, getNodeIdLocation } from "./src/nodeCache";
|
|
@@ -32,12 +32,18 @@ type ExtractShape<ClassType, Shape> = {
|
|
|
32
32
|
|
|
33
33
|
export class SocketFunction {
|
|
34
34
|
public static logMessages = false;
|
|
35
|
+
public static trackMessageSizes = {
|
|
36
|
+
upload: [] as ((size: number) => void)[],
|
|
37
|
+
download: [] as ((size: number) => void)[],
|
|
38
|
+
};
|
|
35
39
|
|
|
36
40
|
public static MAX_MESSAGE_SIZE = 1024 * 1024 * 32;
|
|
37
41
|
|
|
38
|
-
public static
|
|
42
|
+
public static HTTP_ETAG_CACHE = false;
|
|
39
43
|
public static silent = true;
|
|
40
44
|
|
|
45
|
+
public static HTTP_COMPRESS = false;
|
|
46
|
+
|
|
41
47
|
// In retrospect... dynamically changing the wire serializer is a BAD idea. If any calls happen
|
|
42
48
|
// before it is changed, things just break. Also, it needs to be changed on both sides,
|
|
43
49
|
// or else things break. Also, it is very hard to detect when the issue is different serializers
|
package/SocketFunctionTypes.ts
CHANGED
|
@@ -24,6 +24,7 @@ export type SocketExposedInterfaceClass = {
|
|
|
24
24
|
};
|
|
25
25
|
export type SocketExposedShape<ExposedType extends SocketExposedInterface = SocketExposedInterface> = {
|
|
26
26
|
[functionName in keyof ExposedType]?: {
|
|
27
|
+
compress?: boolean;
|
|
27
28
|
/** Indicates with the same input, we give the same output, forever,
|
|
28
29
|
* independent of code changes. This only works for data storage.
|
|
29
30
|
*/
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "socket-function",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.6",
|
|
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",
|
|
7
7
|
"dependencies": {
|
|
8
|
+
"@types/pako": "^2.0.3",
|
|
8
9
|
"@types/ws": "^8.5.3",
|
|
9
10
|
"cbor-x": "^1.5.6",
|
|
10
11
|
"cookie": "^0.5.0",
|
|
11
12
|
"mobx": "^6.6.2",
|
|
12
13
|
"node-forge": "https://github.com/sliftist/forge#name",
|
|
14
|
+
"pako": "^2.1.0",
|
|
13
15
|
"preact": "^10.10.6",
|
|
14
|
-
"typenode": "^5.3.
|
|
16
|
+
"typenode": "^5.3.10",
|
|
15
17
|
"ws": "^8.8.0"
|
|
16
18
|
},
|
|
17
19
|
"optionalDependencies": {
|
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
import debugbreak from "debugbreak";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import { SocketFunction } from "../SocketFunction";
|
|
5
|
-
import { setHTTPResultHeaders } from "../src/callHTTPHandler";
|
|
6
|
-
import { isNodeTrue } from "../src/misc";
|
|
5
|
+
import { getCurrentHTTPRequest, setHTTPResultHeaders } from "../src/callHTTPHandler";
|
|
6
|
+
import { formatNumberSuffixed, isNodeTrue, sha256Hash, sha256HashPromise } from "../src/misc";
|
|
7
|
+
import zlib from "zlib";
|
|
8
|
+
import { cacheLimited } from "../src/caching";
|
|
9
|
+
import { formatNumber } from "../src/formatting/format";
|
|
10
|
+
|
|
11
|
+
const COMPRESS_CACHE_SIZE = 1024 * 1024 * 128;
|
|
7
12
|
|
|
8
13
|
module.allowclient = true;
|
|
9
14
|
|
|
@@ -55,6 +60,9 @@ export interface SerializedModule {
|
|
|
55
60
|
source?: string;
|
|
56
61
|
|
|
57
62
|
seqNum: number;
|
|
63
|
+
|
|
64
|
+
size?: number;
|
|
65
|
+
version?: number;
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
let nextModuleSeqNum = 1;
|
|
@@ -128,6 +136,8 @@ class RequireControllerBase {
|
|
|
128
136
|
};
|
|
129
137
|
requireSeqNumProcessId: string;
|
|
130
138
|
}> {
|
|
139
|
+
let httpRequest = getCurrentHTTPRequest();
|
|
140
|
+
|
|
131
141
|
let seqNums: { [seqNum: number]: 1 } = {};
|
|
132
142
|
if (alreadyHave?.requireSeqNumProcessId === requireSeqNumProcessId) {
|
|
133
143
|
for (let { s, e } of alreadyHave.seqNumRanges) {
|
|
@@ -168,6 +178,8 @@ class RequireControllerBase {
|
|
|
168
178
|
serveronly: module.serveronly,
|
|
169
179
|
requests: Object.create(null),
|
|
170
180
|
seqNum: module.requireControllerSeqNum,
|
|
181
|
+
size: module.size,
|
|
182
|
+
version: module.version,
|
|
171
183
|
};
|
|
172
184
|
let moduleObj = modules[module.filename];
|
|
173
185
|
if (moduleObj.allowclient) {
|
|
@@ -256,10 +268,53 @@ class RequireControllerBase {
|
|
|
256
268
|
for (let remap of mapGetModules) {
|
|
257
269
|
result = await remap.remap(result, [pathRequests, alreadyHave, config]);
|
|
258
270
|
}
|
|
271
|
+
|
|
272
|
+
// NOTE: Handling compression ourself allows us to efficiently cache (otherwise caching would require
|
|
273
|
+
// hashing the output, which takes almost as long as compression!)
|
|
274
|
+
if (httpRequest && SocketFunction.HTTP_COMPRESS && httpRequest.headers["accept-encoding"]?.includes("gzip")) {
|
|
275
|
+
let simplifiedResult = {
|
|
276
|
+
...result,
|
|
277
|
+
modules: Object.entries(result.modules).map(x => [x[0], {
|
|
278
|
+
filename: x[1].filename,
|
|
279
|
+
version: x[1].version,
|
|
280
|
+
sourceLength: x[1].source?.length,
|
|
281
|
+
}]),
|
|
282
|
+
};
|
|
283
|
+
let key = sha256Hash(JSON.stringify(simplifiedResult));
|
|
284
|
+
let buffer = await compressCached(key, () => Buffer.from(JSON.stringify(result)));
|
|
285
|
+
setHTTPResultHeaders(buffer, { "Content-Type": "application/json", "Content-Encoding": "gzip" });
|
|
286
|
+
return buffer as any;
|
|
287
|
+
}
|
|
288
|
+
|
|
259
289
|
return result;
|
|
260
290
|
}
|
|
261
291
|
}
|
|
262
292
|
|
|
293
|
+
let compressCacheSize = 0;
|
|
294
|
+
let compressCache = new Map<string, Buffer>();
|
|
295
|
+
async function compressCached(bufferKey: string, buffer: () => Buffer): Promise<Buffer> {
|
|
296
|
+
let cached = compressCache.get(bufferKey);
|
|
297
|
+
if (!cached) {
|
|
298
|
+
cached = await new Promise<Buffer>((resolve, reject) => {
|
|
299
|
+
zlib.gzip(buffer(), {}, (err, result) => {
|
|
300
|
+
if (err) {
|
|
301
|
+
reject(err);
|
|
302
|
+
} else {
|
|
303
|
+
resolve(result);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
compressCacheSize += cached.length;
|
|
308
|
+
// TODO: Make the cache LRU eviction, instead of just resetting it
|
|
309
|
+
if (compressCacheSize > COMPRESS_CACHE_SIZE) {
|
|
310
|
+
compressCache.clear();
|
|
311
|
+
compressCacheSize = cached.length;
|
|
312
|
+
}
|
|
313
|
+
compressCache.set(bufferKey, cached);
|
|
314
|
+
}
|
|
315
|
+
return cached;
|
|
316
|
+
}
|
|
317
|
+
|
|
263
318
|
type ClientRemapCallback = (args: GetModulesArgs) => Promise<GetModulesArgs>;
|
|
264
319
|
declare global {
|
|
265
320
|
/** Must be set clientside BEFORE requests are made (so you likely want to use RequireController.addMapGetModules
|
package/require/require.js
CHANGED
|
@@ -416,6 +416,7 @@
|
|
|
416
416
|
let time = Date.now();
|
|
417
417
|
currentModuleEvaluationStack.push(module.filename);
|
|
418
418
|
try {
|
|
419
|
+
module.isPreloading = true;
|
|
419
420
|
moduleFnc.call(
|
|
420
421
|
{
|
|
421
422
|
// NOTE: Adding __importStar to the module causes typescript to use our implementation,
|
|
@@ -440,6 +441,7 @@
|
|
|
440
441
|
// As in, adding about 500ms to our load time, which is annoying when debugging.
|
|
441
442
|
//console.debug(`Evaluated module ${module.filename} ${Math.ceil(source.length / 1024)}KB`);
|
|
442
443
|
} finally {
|
|
444
|
+
module.isPreloading = false;
|
|
443
445
|
currentModuleEvaluationStack.pop();
|
|
444
446
|
}
|
|
445
447
|
|
package/src/CallFactory.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { CallerContext, CallerContextBase, CallType, FullCallType } from "../SocketFunctionTypes";
|
|
2
2
|
import * as ws from "ws";
|
|
3
|
-
import { performLocalCall } from "./callManager";
|
|
3
|
+
import { performLocalCall, shouldCompressCall } from "./callManager";
|
|
4
4
|
import { convertErrorStackToError, formatNumberSuffixed, isNode, list } from "./misc";
|
|
5
5
|
import { createWebsocketFactory, getTLSSocket } from "./websocketFactory";
|
|
6
6
|
import { SocketFunction } from "../SocketFunction";
|
|
7
|
-
import { gzip } from "zlib";
|
|
8
7
|
import * as tls from "tls";
|
|
9
8
|
import { getClientNodeId, getNodeIdLocation, registerNodeClient } from "./nodeCache";
|
|
10
9
|
import debugbreak from "debugbreak";
|
|
@@ -13,12 +12,16 @@ import { red, yellow } from "./formatting/logColors";
|
|
|
13
12
|
import { isSplitableArray, markArrayAsSplitable } from "./fixLargeNetworkCalls";
|
|
14
13
|
import { delay, runInSerial } from "./batching";
|
|
15
14
|
import { formatNumber, formatTime } from "./formatting/format";
|
|
15
|
+
import pako from "pako";
|
|
16
|
+
import { setFlag } from "../require/compileFlags";
|
|
17
|
+
setFlag(require, "pako", "allowclient", true);
|
|
16
18
|
|
|
17
19
|
const MIN_RETRY_DELAY = 1000;
|
|
18
20
|
|
|
19
21
|
type InternalCallType = FullCallType & {
|
|
20
22
|
seqNum: number;
|
|
21
23
|
isReturn: false;
|
|
24
|
+
isArgsCompressed?: boolean;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
type InternalReturnType = {
|
|
@@ -27,7 +30,7 @@ type InternalReturnType = {
|
|
|
27
30
|
error?: string;
|
|
28
31
|
seqNum: number;
|
|
29
32
|
resultSize: number;
|
|
30
|
-
|
|
33
|
+
isResultCompressed?: boolean;
|
|
31
34
|
};
|
|
32
35
|
|
|
33
36
|
|
|
@@ -123,6 +126,11 @@ export async function createCallFactory(
|
|
|
123
126
|
functionName: call.functionName,
|
|
124
127
|
seqNum,
|
|
125
128
|
};
|
|
129
|
+
let originalArgs = call.args;
|
|
130
|
+
if (shouldCompressCall(fullCall)) {
|
|
131
|
+
fullCall.args = await compressObj(fullCall.args) as any;
|
|
132
|
+
fullCall.isArgsCompressed = true;
|
|
133
|
+
}
|
|
126
134
|
let time = Date.now();
|
|
127
135
|
let data: Buffer[];
|
|
128
136
|
let dataMaybePromise = SocketFunction.WIRE_SERIALIZER.serialize(fullCall);
|
|
@@ -138,11 +146,11 @@ export async function createCallFactory(
|
|
|
138
146
|
}
|
|
139
147
|
|
|
140
148
|
if (size > SocketFunction.MAX_MESSAGE_SIZE * 1.5) {
|
|
141
|
-
let splitArgIndex =
|
|
149
|
+
let splitArgIndex = originalArgs.findIndex(isSplitableArray);
|
|
142
150
|
if (splitArgIndex >= 0) {
|
|
143
151
|
console.log(yellow(`Splitting large call due to large args: ${call.classGuid}.${call.functionName}`));
|
|
144
152
|
let SPLIT_GROUPS = 10;
|
|
145
|
-
let splitArg =
|
|
153
|
+
let splitArg = originalArgs[splitArgIndex] as unknown[];
|
|
146
154
|
let subCalls = list(SPLIT_GROUPS).map(index => {
|
|
147
155
|
let start = Math.floor(index / SPLIT_GROUPS * splitArg.length);
|
|
148
156
|
let end = Math.floor((index + 1) / SPLIT_GROUPS * splitArg.length);
|
|
@@ -167,9 +175,6 @@ export async function createCallFactory(
|
|
|
167
175
|
|
|
168
176
|
let resultPromise = new Promise((resolve, reject) => {
|
|
169
177
|
let callback = (result: InternalReturnType) => {
|
|
170
|
-
if (SocketFunction.logMessages) {
|
|
171
|
-
console.log(`SIZE\t${(formatNumberSuffixed(result.resultSize) + "B").padEnd(4, " ")}\t${call.classGuid}.${call.functionName} at ${Date.now()}`);
|
|
172
|
-
}
|
|
173
178
|
pendingCalls.delete(seqNum);
|
|
174
179
|
if (result.error) {
|
|
175
180
|
reject(convertErrorStackToError(result.error));
|
|
@@ -180,6 +185,20 @@ export async function createCallFactory(
|
|
|
180
185
|
pendingCalls.set(seqNum, { callback, data, call: fullCall });
|
|
181
186
|
});
|
|
182
187
|
|
|
188
|
+
{
|
|
189
|
+
let resultSize = data.map(x => x.length).reduce((a, b) => a + b, 0);
|
|
190
|
+
for (let callback of SocketFunction.trackMessageSizes.upload) {
|
|
191
|
+
callback(resultSize);
|
|
192
|
+
}
|
|
193
|
+
if (SocketFunction.logMessages) {
|
|
194
|
+
let fncHack = "";
|
|
195
|
+
if (call.functionName === "addCall") {
|
|
196
|
+
let arg = originalArgs[0] as any;
|
|
197
|
+
fncHack = `.${arg.DomainName}.${arg.ModuleId}.${arg.FunctionId}`;
|
|
198
|
+
}
|
|
199
|
+
console.log(`SIZE\t${(formatNumberSuffixed(resultSize) + "B").padEnd(4, " ")}\t${call.classGuid}.${call.functionName}${fncHack} at ${Date.now()}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
183
202
|
await send(data);
|
|
184
203
|
|
|
185
204
|
return await resultPromise;
|
|
@@ -210,7 +229,6 @@ export async function createCallFactory(
|
|
|
210
229
|
error: error,
|
|
211
230
|
seqNum: call.call.seqNum,
|
|
212
231
|
resultSize: 0,
|
|
213
|
-
compressed: false,
|
|
214
232
|
});
|
|
215
233
|
}
|
|
216
234
|
|
|
@@ -337,6 +355,9 @@ export async function createCallFactory(
|
|
|
337
355
|
let time = Date.now();
|
|
338
356
|
let call = await SocketFunction.WIRE_SERIALIZER.deserialize(currentBuffers) as InternalCallType | InternalReturnType;
|
|
339
357
|
time = Date.now() - time;
|
|
358
|
+
for (let callback of SocketFunction.trackMessageSizes.download) {
|
|
359
|
+
callback(resultSize);
|
|
360
|
+
}
|
|
340
361
|
|
|
341
362
|
if (call.isReturn) {
|
|
342
363
|
let callbackObj = pendingCalls.get(call.seqNum);
|
|
@@ -347,9 +368,24 @@ export async function createCallFactory(
|
|
|
347
368
|
console.log(`Got return for unknown call ${call.seqNum}`);
|
|
348
369
|
return;
|
|
349
370
|
}
|
|
371
|
+
if (SocketFunction.logMessages) {
|
|
372
|
+
let call = callbackObj.call;
|
|
373
|
+
console.log(`SIZE\t${(formatNumberSuffixed(resultSize) + "B").padEnd(4, " ")}\t${call.classGuid}.${call.functionName} at ${Date.now()}`);
|
|
374
|
+
}
|
|
375
|
+
if (call.isResultCompressed) {
|
|
376
|
+
call.result = await decompressObj(call.result as Buffer);
|
|
377
|
+
call.isResultCompressed = false;
|
|
378
|
+
}
|
|
350
379
|
call.resultSize = resultSize;
|
|
351
380
|
callbackObj.callback(call);
|
|
352
381
|
} else {
|
|
382
|
+
if (call.isArgsCompressed) {
|
|
383
|
+
call.args = await decompressObj(call.args as any as Buffer) as any;
|
|
384
|
+
call.isArgsCompressed = false;
|
|
385
|
+
}
|
|
386
|
+
if (SocketFunction.logMessages) {
|
|
387
|
+
console.log(`SIZE\t${(formatNumberSuffixed(resultSize) + "B").padEnd(4, " ")}\t${call.classGuid}.${call.functionName} at ${Date.now()}`);
|
|
388
|
+
}
|
|
353
389
|
if (time > SocketFunction.WIRE_WARN_TIME) {
|
|
354
390
|
console.log(red(`Slow parse, took ${time}ms to parse ${resultSize} bytes, for call to ${call.classGuid}.${call.functionName}`));
|
|
355
391
|
}
|
|
@@ -362,8 +398,11 @@ export async function createCallFactory(
|
|
|
362
398
|
result,
|
|
363
399
|
seqNum: call.seqNum,
|
|
364
400
|
resultSize: resultSize,
|
|
365
|
-
compressed: false,
|
|
366
401
|
};
|
|
402
|
+
if (shouldCompressCall(call)) {
|
|
403
|
+
response.result = await compressObj(response.result) as any;
|
|
404
|
+
response.isResultCompressed = true;
|
|
405
|
+
}
|
|
367
406
|
} catch (e: any) {
|
|
368
407
|
response = {
|
|
369
408
|
isReturn: true,
|
|
@@ -371,7 +410,6 @@ export async function createCallFactory(
|
|
|
371
410
|
seqNum: call.seqNum,
|
|
372
411
|
error: e.stack,
|
|
373
412
|
resultSize: resultSize,
|
|
374
|
-
compressed: false,
|
|
375
413
|
};
|
|
376
414
|
}
|
|
377
415
|
|
|
@@ -384,7 +422,6 @@ export async function createCallFactory(
|
|
|
384
422
|
seqNum: call.seqNum,
|
|
385
423
|
error: new Error(`Response too large to send (${call.classGuid}.${call.functionName}, size: ${formatNumber(totalResultSize)} > ${formatNumber(SocketFunction.MAX_MESSAGE_SIZE)}). If you need to handle very large static data use some external service, such as Backblaze B2 or AWS S3. Or consider fragmenting data at an application level, because sending large data will cause large lag spikes for other clients using this server. Or, if absolutely required, set SocketFunction.MAX_MESSAGE_SIZE to a higher value.`).stack,
|
|
386
424
|
resultSize: resultSize,
|
|
387
|
-
compressed: false,
|
|
388
425
|
};
|
|
389
426
|
result = await SocketFunction.WIRE_SERIALIZER.serialize(response);
|
|
390
427
|
}
|
|
@@ -394,15 +431,47 @@ export async function createCallFactory(
|
|
|
394
431
|
}
|
|
395
432
|
throw new Error(`Unhandled data type ${typeof message}`);
|
|
396
433
|
} catch (e: any) {
|
|
434
|
+
let message = e.stack || e.message || e;
|
|
397
435
|
// NOTE: I'm looking for all types of errors here (specifically, .send errors), in case
|
|
398
436
|
// there are errors I should be handling.
|
|
399
|
-
if (
|
|
437
|
+
if (message.startsWith("Error: Cannot send data to") && message.includes("as the connection has closed")) {
|
|
400
438
|
// This is fine, just ignore it
|
|
401
439
|
} else {
|
|
440
|
+
debugbreak(2);
|
|
441
|
+
debugger;
|
|
402
442
|
console.error(e.stack);
|
|
403
443
|
}
|
|
404
444
|
}
|
|
405
445
|
}
|
|
406
446
|
|
|
407
447
|
return callFactory;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
async function compressObj(obj: unknown): Promise<Buffer> {
|
|
452
|
+
let buffers = await SocketFunction.WIRE_SERIALIZER.serialize(obj);
|
|
453
|
+
let lengthBuffer = Buffer.from((new Float64Array(buffers.map(x => x.length))).buffer);
|
|
454
|
+
let buffer = Buffer.concat([lengthBuffer, ...buffers]);
|
|
455
|
+
return Buffer.from(pako.gzip(buffer));
|
|
456
|
+
}
|
|
457
|
+
async function decompressObj(obj: Buffer): Promise<unknown> {
|
|
458
|
+
try {
|
|
459
|
+
let buffer = Buffer.from(pako.ungzip(obj));
|
|
460
|
+
let lengthBuffer = buffer.slice(0, 8);
|
|
461
|
+
let lengths = new Float64Array(lengthBuffer.buffer, lengthBuffer.byteOffset, lengthBuffer.byteLength / 8);
|
|
462
|
+
let buffers: Buffer[] = [];
|
|
463
|
+
let offset = 8;
|
|
464
|
+
for (let length of lengths) {
|
|
465
|
+
buffers.push(buffer.slice(offset, offset + length));
|
|
466
|
+
offset += length;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return await SocketFunction.WIRE_SERIALIZER.deserialize(buffers);
|
|
470
|
+
} catch (e) {
|
|
471
|
+
// We were encountering issues with the checksum failing when unzipping. Presumably if the data
|
|
472
|
+
// is bad deserialize will also fail. I can't repro it anymore though...
|
|
473
|
+
debugbreak(2);
|
|
474
|
+
debugger;
|
|
475
|
+
throw e;
|
|
476
|
+
}
|
|
408
477
|
}
|
package/src/callHTTPHandler.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { CallerContext, CallType, FullCallType } from "../SocketFunctionTypes";
|
|
|
4
4
|
import { isDataImmutable, performLocalCall } from "./callManager";
|
|
5
5
|
import { SocketFunction } from "../SocketFunction";
|
|
6
6
|
import { gzip } from "zlib";
|
|
7
|
+
import zlib from "zlib";
|
|
7
8
|
import { formatNumberSuffixed, sha256Hash } from "./misc";
|
|
8
9
|
import { getClientNodeId, getNodeId } from "./nodeCache";
|
|
9
10
|
|
|
@@ -48,6 +49,11 @@ export function getNodeIdsFromRequest(request: http.IncomingMessage) {
|
|
|
48
49
|
return { nodeId, localNodeId };
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
let requests = new Map<CallerContext, http.IncomingMessage>();
|
|
53
|
+
export function getCurrentHTTPRequest(): http.IncomingMessage | undefined {
|
|
54
|
+
return requests.get(SocketFunction.getCaller());
|
|
55
|
+
}
|
|
56
|
+
|
|
51
57
|
export async function httpCallHandler(request: http.IncomingMessage, response: http.ServerResponse) {
|
|
52
58
|
try {
|
|
53
59
|
// Always set x-frame-options, to prevent iframe embedding click hijacking
|
|
@@ -65,7 +71,9 @@ export async function httpCallHandler(request: http.IncomingMessage, response: h
|
|
|
65
71
|
let protocol = "https";
|
|
66
72
|
let url = protocol + "://" + request.headers.host + request.url;
|
|
67
73
|
|
|
68
|
-
|
|
74
|
+
if (SocketFunction.logMessages) {
|
|
75
|
+
console.log(`HTTP request (${request.method}) ${url}`);
|
|
76
|
+
}
|
|
69
77
|
let urlObj = new URL(url);
|
|
70
78
|
|
|
71
79
|
let payload = await new Promise<Buffer>((resolve, reject) => {
|
|
@@ -136,10 +144,16 @@ export async function httpCallHandler(request: http.IncomingMessage, response: h
|
|
|
136
144
|
}
|
|
137
145
|
}
|
|
138
146
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
147
|
+
requests.set(caller, request);
|
|
148
|
+
let result: unknown;
|
|
149
|
+
try {
|
|
150
|
+
result = await performLocalCall({
|
|
151
|
+
caller,
|
|
152
|
+
call
|
|
153
|
+
});
|
|
154
|
+
} finally {
|
|
155
|
+
requests.delete(caller);
|
|
156
|
+
}
|
|
143
157
|
|
|
144
158
|
let resultBuffer: Buffer;
|
|
145
159
|
if (typeof result === "object" && result && result instanceof Buffer) {
|
|
@@ -152,7 +166,7 @@ export async function httpCallHandler(request: http.IncomingMessage, response: h
|
|
|
152
166
|
|
|
153
167
|
// NOTE: Our ETag caching is only to reduce data sent on the wire, we evaluate the calls
|
|
154
168
|
// every time (so it is strictly a wire cache for HTTP, not a computation cache)
|
|
155
|
-
if (SocketFunction.
|
|
169
|
+
if (SocketFunction.HTTP_ETAG_CACHE) {
|
|
156
170
|
response.setHeader("cache-control", "private, max-age=0, must-revalidate");
|
|
157
171
|
let hash = sha256Hash(resultBuffer);
|
|
158
172
|
response.setHeader("ETag", hash);
|
|
@@ -173,8 +187,26 @@ export async function httpCallHandler(request: http.IncomingMessage, response: h
|
|
|
173
187
|
return;
|
|
174
188
|
}
|
|
175
189
|
}
|
|
190
|
+
if (SocketFunction.HTTP_COMPRESS && request.headers["accept-encoding"]?.includes("gzip") && !headers?.["Content-Encoding"]) {
|
|
191
|
+
// NOTE: This is a BIT slow. To speed it up, functions can use an internal cache, according to their function,
|
|
192
|
+
// and return a Buffer (which they can as any cast to make the returned type allowed, as returned Buffers will
|
|
193
|
+
// just be treated like a buffer of JSON data).
|
|
194
|
+
// - The caller should use getCurrentHTTPRequest first though, to check if gzip is allowed
|
|
195
|
+
response.setHeader("Content-Encoding", "gzip");
|
|
196
|
+
resultBuffer = await new Promise<Buffer>((resolve, reject) => {
|
|
197
|
+
zlib.gzip(resultBuffer, {}, (err, result) => {
|
|
198
|
+
if (err) {
|
|
199
|
+
reject(err);
|
|
200
|
+
} else {
|
|
201
|
+
resolve(result);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
176
206
|
response.write(resultBuffer);
|
|
177
|
-
|
|
207
|
+
if (SocketFunction.logMessages) {
|
|
208
|
+
console.log(`HTTP response ${formatNumberSuffixed(resultBuffer.length)}B (${request.method}) ${url}`);
|
|
209
|
+
}
|
|
178
210
|
|
|
179
211
|
} catch (e: any) {
|
|
180
212
|
console.log(`HTTP error (${request.method}) ${e.stack}`);
|
package/src/callManager.ts
CHANGED
|
@@ -15,6 +15,10 @@ let exposedClasses = new Set<string>();
|
|
|
15
15
|
let globalHooks: SocketFunctionHook[] = [];
|
|
16
16
|
let globalClientHooks: SocketFunctionClientHook[] = [];
|
|
17
17
|
|
|
18
|
+
export function shouldCompressCall(call: CallType) {
|
|
19
|
+
return !!classes[call.classGuid]?.shape[call.functionName]?.compress;
|
|
20
|
+
}
|
|
21
|
+
|
|
18
22
|
export async function performLocalCall(
|
|
19
23
|
config: {
|
|
20
24
|
call: FullCallType;
|
|
@@ -5,14 +5,16 @@ function ansiHSL(h: number, s: number, l: number, text: string): string {
|
|
|
5
5
|
return ansiRGB(r, g, b, text);
|
|
6
6
|
}
|
|
7
7
|
function ansiRGB(r: number, g: number, b: number, text: string): string {
|
|
8
|
-
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[
|
|
8
|
+
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
const lightness = 68;
|
|
12
|
-
export const blue =
|
|
12
|
+
export const blue = (text: string) => `\x1b[34m${text}\x1b[0m`;
|
|
13
13
|
export const red = ansiHSL.bind(null, 0, 100, lightness);
|
|
14
|
-
export const green = (text: string) => `\x1b[32m${text}\x1b[
|
|
15
|
-
export const yellow = (text: string) => `\x1b[33m${text}\x1b[
|
|
14
|
+
export const green = (text: string) => `\x1b[32m${text}\x1b[0m`;
|
|
15
|
+
export const yellow = (text: string) => `\x1b[33m${text}\x1b[0m`;
|
|
16
16
|
export const white = ansiHSL.bind(null, 0, 0, 80);
|
|
17
17
|
|
|
18
|
-
export const magenta = (text: string) => `\x1b[35m${text}\x1b[
|
|
18
|
+
export const magenta = (text: string) => `\x1b[35m${text}\x1b[0m`;
|
|
19
|
+
|
|
20
|
+
console.log(blue('blue'));
|
package/src/misc.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as crypto from "crypto";
|
|
2
2
|
import { canHaveChildren, MaybePromise } from "./types";
|
|
3
|
+
import { formatNumber } from "./formatting/format";
|
|
3
4
|
|
|
4
5
|
export const timeInSecond = 1000;
|
|
5
6
|
export const timeInMinute = timeInSecond * 60;
|
|
@@ -58,32 +59,7 @@ export function isNodeTrue() {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
export function formatNumberSuffixed(count: number): string {
|
|
61
|
-
|
|
62
|
-
if (count < 0) {
|
|
63
|
-
return "-" + formatNumberSuffixed(-count);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
let absValue = Math.abs(count);
|
|
67
|
-
|
|
68
|
-
const extraFactor = 10;
|
|
69
|
-
let divisor = 1;
|
|
70
|
-
let suffix = "";
|
|
71
|
-
if (absValue < 1000 * extraFactor) {
|
|
72
|
-
|
|
73
|
-
} else if (absValue < 1000 * 1000 * extraFactor) {
|
|
74
|
-
suffix = "K";
|
|
75
|
-
divisor = 1000;
|
|
76
|
-
} else if (absValue < 1000 * 1000 * 1000 * extraFactor) {
|
|
77
|
-
suffix = "M";
|
|
78
|
-
divisor = 1000 * 1000;
|
|
79
|
-
} else {
|
|
80
|
-
suffix = "B";
|
|
81
|
-
divisor = 1000 * 1000 * 1000;
|
|
82
|
-
}
|
|
83
|
-
count /= divisor;
|
|
84
|
-
absValue /= divisor;
|
|
85
|
-
|
|
86
|
-
return Math.round(count).toString() + suffix;
|
|
62
|
+
return formatNumber(count);
|
|
87
63
|
}
|
|
88
64
|
|
|
89
65
|
export function list(count: number) {
|