socket-function 0.10.3 → 0.10.5
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 +11 -3
- package/SocketFunctionTypes.ts +1 -0
- package/package.json +4 -2
- package/require/RequireController.ts +94 -5
- package/require/require.html +1 -0
- package/require/require.js +8 -0
- package/src/CallFactory.ts +82 -13
- package/src/callHTTPHandler.ts +38 -6
- package/src/callManager.ts +4 -0
- 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
42
|
public static httpETagCache = 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
|
|
@@ -65,6 +71,7 @@ export class SocketFunction {
|
|
|
65
71
|
Shape extends SocketExposedShape<{
|
|
66
72
|
[key in keyof ClassInstance]: (...args: any[]) => Promise<unknown>;
|
|
67
73
|
}>,
|
|
74
|
+
Statics
|
|
68
75
|
>(
|
|
69
76
|
classGuid: string,
|
|
70
77
|
instance: ClassInstance,
|
|
@@ -75,8 +82,9 @@ export class SocketFunction {
|
|
|
75
82
|
config?: {
|
|
76
83
|
/** @noAutoExpose If true SocketFunction.expose(Controller) must be called explicitly. */
|
|
77
84
|
noAutoExpose?: boolean;
|
|
85
|
+
statics?: Statics;
|
|
78
86
|
}
|
|
79
|
-
): SocketRegistered<ExtractShape<ClassInstance, Shape>> {
|
|
87
|
+
): SocketRegistered<ExtractShape<ClassInstance, Shape>> & Statics {
|
|
80
88
|
let getDefaultHooks = defaultHooksFnc && lazy(defaultHooksFnc);
|
|
81
89
|
const getShape = lazy(() => {
|
|
82
90
|
let shape = shapeFnc() as SocketExposedShape;
|
|
@@ -146,7 +154,7 @@ export class SocketFunction {
|
|
|
146
154
|
if (!config?.noAutoExpose) {
|
|
147
155
|
this.expose(result);
|
|
148
156
|
}
|
|
149
|
-
return result;
|
|
157
|
+
return Object.assign(result, config?.statics);
|
|
150
158
|
}
|
|
151
159
|
|
|
152
160
|
public static onNextDisconnect(nodeId: string, callback: () => void) {
|
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.5",
|
|
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;
|
|
@@ -64,6 +72,8 @@ const requireSeqNumProcessId = "requireSeqNumProcessId_" + Date.now() + "_" + Ma
|
|
|
64
72
|
const htmlFile = isNodeTrue() && fs.readFileSync(__dirname + "/require.html").toString();
|
|
65
73
|
const jsFile = isNodeTrue() && fs.readFileSync(__dirname + "/require.js").toString();
|
|
66
74
|
const bufferShim = isNodeTrue() && fs.readFileSync(__dirname + "/buffer.js").toString();
|
|
75
|
+
const BEFORE_ENTRY_TEMPLATE = "<!-- BEFORE_ENTRY_TEMPLATE -->";
|
|
76
|
+
const ENTRY_TEMPLATE = "<!-- ENTRY_TEMPLATE -->";
|
|
67
77
|
|
|
68
78
|
const resolvedHTMLFile = isNodeTrue() && (
|
|
69
79
|
htmlFile
|
|
@@ -71,13 +81,30 @@ const resolvedHTMLFile = isNodeTrue() && (
|
|
|
71
81
|
.replace(`<script src="./require.js"></script>`, `<script>${jsFile}</script>`)
|
|
72
82
|
);
|
|
73
83
|
|
|
84
|
+
let beforeEntryText: string[] = [];
|
|
85
|
+
function injectHTMLBeforeStartup(text: string) {
|
|
86
|
+
beforeEntryText.push(text);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type GetModulesResult = ReturnType<RequireControllerBase["getModules"]> extends Promise<infer T> ? T : never;
|
|
90
|
+
type GetModulesArgs = Parameters<RequireControllerBase["getModules"]>;
|
|
91
|
+
let mapGetModules: {
|
|
92
|
+
remap(result: GetModulesResult, args: GetModulesArgs): Promise<GetModulesResult>
|
|
93
|
+
}[] = [];
|
|
94
|
+
function addMapGetModules(remap: typeof mapGetModules[number]["remap"]) {
|
|
95
|
+
mapGetModules.push({ remap });
|
|
96
|
+
}
|
|
97
|
+
|
|
74
98
|
class RequireControllerBase {
|
|
75
99
|
public rootResolvePath = "";
|
|
76
100
|
|
|
77
101
|
public async requireHTML(bootRequirePath?: string) {
|
|
78
102
|
let result = resolvedHTMLFile;
|
|
103
|
+
if (beforeEntryText.length > 0) {
|
|
104
|
+
result = result.replace(BEFORE_ENTRY_TEMPLATE, beforeEntryText.join("\n"));
|
|
105
|
+
}
|
|
79
106
|
if (bootRequirePath) {
|
|
80
|
-
result = result.replace(
|
|
107
|
+
result = result.replace(ENTRY_TEMPLATE, `<script>require(${JSON.stringify(bootRequirePath)});</script>`);
|
|
81
108
|
}
|
|
82
109
|
return setHTTPResultHeaders(Buffer.from(result), { "Content-Type": "text/html" });
|
|
83
110
|
}
|
|
@@ -101,6 +128,7 @@ class RequireControllerBase {
|
|
|
101
128
|
e?: number;
|
|
102
129
|
}[];
|
|
103
130
|
},
|
|
131
|
+
config?: {}
|
|
104
132
|
): Promise<{
|
|
105
133
|
requestsResolvedPaths: string[];
|
|
106
134
|
modules: {
|
|
@@ -108,6 +136,8 @@ class RequireControllerBase {
|
|
|
108
136
|
};
|
|
109
137
|
requireSeqNumProcessId: string;
|
|
110
138
|
}> {
|
|
139
|
+
let httpRequest = getCurrentHTTPRequest();
|
|
140
|
+
|
|
111
141
|
let seqNums: { [seqNum: number]: 1 } = {};
|
|
112
142
|
if (alreadyHave?.requireSeqNumProcessId === requireSeqNumProcessId) {
|
|
113
143
|
for (let { s, e } of alreadyHave.seqNumRanges) {
|
|
@@ -148,6 +178,8 @@ class RequireControllerBase {
|
|
|
148
178
|
serveronly: module.serveronly,
|
|
149
179
|
requests: Object.create(null),
|
|
150
180
|
seqNum: module.requireControllerSeqNum,
|
|
181
|
+
size: module.size,
|
|
182
|
+
version: module.version,
|
|
151
183
|
};
|
|
152
184
|
let moduleObj = modules[module.filename];
|
|
153
185
|
if (moduleObj.allowclient) {
|
|
@@ -232,10 +264,63 @@ class RequireControllerBase {
|
|
|
232
264
|
addModule(clientModule, true);
|
|
233
265
|
}
|
|
234
266
|
|
|
235
|
-
|
|
267
|
+
let result: GetModulesResult = { requestsResolvedPaths, modules, requireSeqNumProcessId };
|
|
268
|
+
for (let remap of mapGetModules) {
|
|
269
|
+
result = await remap.remap(result, [pathRequests, alreadyHave, config]);
|
|
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
|
+
}]),
|
|
281
|
+
};
|
|
282
|
+
let key = sha256Hash(JSON.stringify(simplifiedResult));
|
|
283
|
+
let buffer = await compressCached(key, () => Buffer.from(JSON.stringify(result)));
|
|
284
|
+
setHTTPResultHeaders(buffer, { "Content-Type": "application/json", "Content-Encoding": "gzip" });
|
|
285
|
+
return buffer as any;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return result;
|
|
236
289
|
}
|
|
237
290
|
}
|
|
238
291
|
|
|
292
|
+
let compressCacheSize = 0;
|
|
293
|
+
let compressCache = new Map<string, Buffer>();
|
|
294
|
+
async function compressCached(bufferKey: string, buffer: () => Buffer): Promise<Buffer> {
|
|
295
|
+
let cached = compressCache.get(bufferKey);
|
|
296
|
+
if (!cached) {
|
|
297
|
+
cached = await new Promise<Buffer>((resolve, reject) => {
|
|
298
|
+
zlib.gzip(buffer(), {}, (err, result) => {
|
|
299
|
+
if (err) {
|
|
300
|
+
reject(err);
|
|
301
|
+
} else {
|
|
302
|
+
resolve(result);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
compressCacheSize += cached.length;
|
|
307
|
+
// TODO: Make the cache LRU eviction, instead of just resetting it
|
|
308
|
+
if (compressCacheSize > COMPRESS_CACHE_SIZE) {
|
|
309
|
+
compressCache.clear();
|
|
310
|
+
compressCacheSize = cached.length;
|
|
311
|
+
}
|
|
312
|
+
compressCache.set(bufferKey, cached);
|
|
313
|
+
}
|
|
314
|
+
return cached;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
type ClientRemapCallback = (args: GetModulesArgs) => Promise<GetModulesArgs>;
|
|
318
|
+
declare global {
|
|
319
|
+
/** Must be set clientside BEFORE requests are made (so you likely want to use RequireController.addMapGetModules
|
|
320
|
+
* to inject code that will use this) */
|
|
321
|
+
var remapImportRequestsClientside: undefined | ClientRemapCallback[];
|
|
322
|
+
}
|
|
323
|
+
|
|
239
324
|
let baseController = new RequireControllerBase();
|
|
240
325
|
export function setRequireBootRequire(path: string) {
|
|
241
326
|
baseController.rootResolvePath = path;
|
|
@@ -252,6 +337,10 @@ export const RequireController = SocketFunction.register(
|
|
|
252
337
|
}),
|
|
253
338
|
undefined,
|
|
254
339
|
{
|
|
255
|
-
noAutoExpose: true
|
|
340
|
+
noAutoExpose: true,
|
|
341
|
+
statics: {
|
|
342
|
+
injectHTMLBeforeStartup,
|
|
343
|
+
addMapGetModules,
|
|
344
|
+
}
|
|
256
345
|
}
|
|
257
346
|
);
|
package/require/require.html
CHANGED
package/require/require.js
CHANGED
|
@@ -166,6 +166,12 @@
|
|
|
166
166
|
args.push(true);
|
|
167
167
|
}
|
|
168
168
|
let requestUrl = location.origin + location.pathname + `?classGuid=RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d&functionName=getModules`;
|
|
169
|
+
let remapImportRequestsClientside = globalThis.remapImportRequestsClientside;
|
|
170
|
+
if (remapImportRequestsClientside) {
|
|
171
|
+
for (let fnc of remapImportRequestsClientside) {
|
|
172
|
+
args = await fnc(args);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
169
175
|
let rawText = await requestText(requestUrl, { args });
|
|
170
176
|
let resultObj;
|
|
171
177
|
try {
|
|
@@ -410,6 +416,7 @@
|
|
|
410
416
|
let time = Date.now();
|
|
411
417
|
currentModuleEvaluationStack.push(module.filename);
|
|
412
418
|
try {
|
|
419
|
+
module.isPreloading = true;
|
|
413
420
|
moduleFnc.call(
|
|
414
421
|
{
|
|
415
422
|
// NOTE: Adding __importStar to the module causes typescript to use our implementation,
|
|
@@ -434,6 +441,7 @@
|
|
|
434
441
|
// As in, adding about 500ms to our load time, which is annoying when debugging.
|
|
435
442
|
//console.debug(`Evaluated module ${module.filename} ${Math.ceil(source.length / 1024)}KB`);
|
|
436
443
|
} finally {
|
|
444
|
+
module.isPreloading = false;
|
|
437
445
|
currentModuleEvaluationStack.pop();
|
|
438
446
|
}
|
|
439
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) {
|
|
@@ -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;
|
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) {
|