socket-function 0.12.15 → 0.13.0
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 +15 -2
- package/SocketFunctionTypes.ts +13 -6
- package/package.json +2 -2
- package/require/RequireController.ts +3 -0
- package/require/require.js +9 -2
- package/src/CallFactory.ts +119 -31
- package/src/JSONLACKS/JSONLACKS.generated.js +302 -72
- package/src/JSONLACKS/JSONLACKS.pegjs +31 -6
- package/src/JSONLACKS/JSONLACKS.ts +17 -2
- package/src/batching.ts +50 -10
- package/src/caching.ts +6 -4
- package/src/callManager.ts +21 -3
- package/src/formatting/format.ts +8 -0
- package/src/misc.ts +8 -0
- package/src/nodeCache.ts +1 -0
- package/src/profiling/measure.ts +28 -8
- package/src/tlsParsing.ts +5 -1
- package/src/webSocketServer.ts +26 -8
- package/src/websocketFactory.ts +57 -51
- package/time/trueTimeShim.ts +8 -0
package/SocketFunction.ts
CHANGED
|
@@ -91,6 +91,13 @@ export class SocketFunction {
|
|
|
91
91
|
/** @noAutoExpose If true SocketFunction.expose(Controller) must be called explicitly. */
|
|
92
92
|
noAutoExpose?: boolean;
|
|
93
93
|
statics?: Statics;
|
|
94
|
+
/** Skip timing functions calls. Useful if a lot of functions have wait time that
|
|
95
|
+
is unrelated to processing, and therefore their timings won't be useful.
|
|
96
|
+
- Also useful if our auto function wrapping code is breaking functionality,
|
|
97
|
+
such as if you have a singleton function which you compare with ===,
|
|
98
|
+
which will breaks because we replaced it with a wrapped measure function.
|
|
99
|
+
*/
|
|
100
|
+
noFunctionMeasure?: boolean;
|
|
94
101
|
}
|
|
95
102
|
): SocketRegistered<ExtractShape<ClassInstance, Shape>> & Statics {
|
|
96
103
|
let getDefaultHooks = defaultHooksFnc && lazy(defaultHooksFnc);
|
|
@@ -101,14 +108,20 @@ export class SocketFunction {
|
|
|
101
108
|
for (let value of Object.values(shape)) {
|
|
102
109
|
if (!value) continue;
|
|
103
110
|
value.clientHooks = [...(defaultHooks?.clientHooks || []), ...(value.clientHooks || [])];
|
|
104
|
-
|
|
111
|
+
if (value.noDefaultHooks) {
|
|
112
|
+
value.hooks = [...(value.hooks || [])];
|
|
113
|
+
} else {
|
|
114
|
+
value.hooks = [...(defaultHooks?.hooks || []), ...(value.hooks || [])];
|
|
115
|
+
}
|
|
105
116
|
value.dataImmutable = defaultHooks?.dataImmutable ?? value.dataImmutable;
|
|
106
117
|
}
|
|
107
118
|
return shape as any as SocketExposedShape;
|
|
108
119
|
});
|
|
109
120
|
|
|
110
121
|
void Promise.resolve().then(() => {
|
|
111
|
-
registerClass(classGuid, instance as SocketExposedInterface, getShape()
|
|
122
|
+
registerClass(classGuid, instance as SocketExposedInterface, getShape(), {
|
|
123
|
+
noFunctionMeasure: config?.noFunctionMeasure,
|
|
124
|
+
});
|
|
112
125
|
});
|
|
113
126
|
|
|
114
127
|
let nodeProxy = getCallProxy(classGuid, async (call) => {
|
package/SocketFunctionTypes.ts
CHANGED
|
@@ -22,15 +22,22 @@ export type SocketExposedInterfaceClass = {
|
|
|
22
22
|
new(): unknown;
|
|
23
23
|
prototype: unknown;
|
|
24
24
|
};
|
|
25
|
+
export type FunctionFlags = {
|
|
26
|
+
compress?: boolean;
|
|
27
|
+
|
|
28
|
+
/** Indicates with the same input, we give the same output, forever,
|
|
29
|
+
* independent of code changes. This only works for data storage.
|
|
30
|
+
*/
|
|
31
|
+
dataImmutable?: boolean;
|
|
32
|
+
|
|
33
|
+
/** Allows overriding SocketFunction.MAX_MESSAGE_SIZE for responses from this function. */
|
|
34
|
+
responseLimit?: number;
|
|
35
|
+
};
|
|
25
36
|
export type SocketExposedShape<ExposedType extends SocketExposedInterface = SocketExposedInterface> = {
|
|
26
|
-
[functionName in keyof ExposedType]?: {
|
|
27
|
-
compress?: boolean;
|
|
28
|
-
/** Indicates with the same input, we give the same output, forever,
|
|
29
|
-
* independent of code changes. This only works for data storage.
|
|
30
|
-
*/
|
|
31
|
-
dataImmutable?: boolean;
|
|
37
|
+
[functionName in keyof ExposedType]?: FunctionFlags & {
|
|
32
38
|
hooks?: SocketFunctionHook<ExposedType>[];
|
|
33
39
|
clientHooks?: SocketFunctionClientHook<ExposedType>[];
|
|
40
|
+
noDefaultHooks?: boolean;
|
|
34
41
|
};
|
|
35
42
|
};
|
|
36
43
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "socket-function",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
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",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"pako": "^2.1.0",
|
|
15
15
|
"preact": "^10.10.6",
|
|
16
16
|
"typenode": "^5.4.1",
|
|
17
|
-
"ws": "^8.
|
|
17
|
+
"ws": "^8.17.1"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
20
|
"rdtsc-now": "^0.4.2"
|
|
@@ -53,6 +53,7 @@ export interface SerializedModule {
|
|
|
53
53
|
// request => resolvedPath
|
|
54
54
|
[request: string]: string;
|
|
55
55
|
};
|
|
56
|
+
asyncRequests: { [request: string]: true };
|
|
56
57
|
// NOTE: IF !allowclient && !serveronly, it might just mean we didn't add allowclient
|
|
57
58
|
// to the module yet. BUT, if serveronly, then we know for sure we don't want it client.
|
|
58
59
|
// So the messages and behavior will be different.
|
|
@@ -201,6 +202,7 @@ class RequireControllerBase {
|
|
|
201
202
|
seqNum: module.requireControllerSeqNum,
|
|
202
203
|
size: module.size,
|
|
203
204
|
version: module.version,
|
|
205
|
+
asyncRequests: module.asyncRequires,
|
|
204
206
|
};
|
|
205
207
|
let moduleObj = modules[module.filename];
|
|
206
208
|
if (moduleObj.allowclient) {
|
|
@@ -271,6 +273,7 @@ class RequireControllerBase {
|
|
|
271
273
|
path: "",
|
|
272
274
|
paths: [],
|
|
273
275
|
requires: {},
|
|
276
|
+
asyncRequires: {},
|
|
274
277
|
allowclient: true,
|
|
275
278
|
moduleContents: `console.warn(${JSON.stringify(error)})`,
|
|
276
279
|
};
|
package/require/require.js
CHANGED
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
// request => resolvedPath
|
|
75
75
|
[request: string]: string;
|
|
76
76
|
};
|
|
77
|
+
asyncRequests: { [request: string]: true };
|
|
77
78
|
// NOTE: IF !allowclient && !serveronly, it might just mean we didn't add allowclient
|
|
78
79
|
// to the module yet. BUT, if serveronly, then we know for sure we don't want it client.
|
|
79
80
|
// So the messages and behavior will be different.
|
|
@@ -255,6 +256,9 @@
|
|
|
255
256
|
if (typeof asyncIsFine !== "boolean") {
|
|
256
257
|
asyncIsFine = false;
|
|
257
258
|
}
|
|
259
|
+
if (request in serializedModule.asyncRequests) {
|
|
260
|
+
asyncIsFine = true;
|
|
261
|
+
}
|
|
258
262
|
if (request in builtInModuleExports) {
|
|
259
263
|
return builtInModuleExports[request];
|
|
260
264
|
}
|
|
@@ -292,8 +296,8 @@
|
|
|
292
296
|
"color: red", "color: unset",
|
|
293
297
|
"color: red", "color: unset",
|
|
294
298
|
);
|
|
299
|
+
debugger;
|
|
295
300
|
}
|
|
296
|
-
debugger;
|
|
297
301
|
return rootRequire(resolvedPath);
|
|
298
302
|
}
|
|
299
303
|
|
|
@@ -427,7 +431,10 @@
|
|
|
427
431
|
|
|
428
432
|
// Import children, as the children may be allowed clientside, and may have side-effects!
|
|
429
433
|
if (!source) {
|
|
430
|
-
let requests = Object.keys(serializedModule.requests)
|
|
434
|
+
let requests = Object.keys(serializedModule.requests)
|
|
435
|
+
.filter(x => x !== "NOTALLOWEDCLIENTSIDE")
|
|
436
|
+
.filter(x => !(x in serializedModule.asyncRequests))
|
|
437
|
+
;
|
|
431
438
|
source = requests.map(id => `require(${JSON.stringify(id)});\n`).join("");
|
|
432
439
|
}
|
|
433
440
|
|
package/src/CallFactory.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CallerContext, CallerContextBase, CallType, FullCallType } from "../SocketFunctionTypes";
|
|
2
2
|
import * as ws from "ws";
|
|
3
|
-
import { performLocalCall, shouldCompressCall } from "./callManager";
|
|
4
|
-
import { convertErrorStackToError, formatNumberSuffixed, isNode, list } from "./misc";
|
|
3
|
+
import { getCallFlags, performLocalCall, shouldCompressCall } from "./callManager";
|
|
4
|
+
import { convertErrorStackToError, formatNumberSuffixed, isBufferType, isNode, list } from "./misc";
|
|
5
5
|
import { createWebsocketFactory, getTLSSocket } from "./websocketFactory";
|
|
6
6
|
import { SocketFunction } from "../SocketFunction";
|
|
7
7
|
import * as tls from "tls";
|
|
@@ -12,8 +12,11 @@ import { red, yellow } from "./formatting/logColors";
|
|
|
12
12
|
import { isSplitableArray, markArrayAsSplitable } from "./fixLargeNetworkCalls";
|
|
13
13
|
import { delay, runInSerial } from "./batching";
|
|
14
14
|
import { formatNumber, formatTime } from "./formatting/format";
|
|
15
|
+
import zlib from "zlib";
|
|
15
16
|
import pako from "pako";
|
|
16
17
|
import { setFlag } from "../require/compileFlags";
|
|
18
|
+
import { measureFnc, measureWrap } from "./profiling/measure";
|
|
19
|
+
import { MaybePromise } from "./types";
|
|
17
20
|
setFlag(require, "pako", "allowclient", true);
|
|
18
21
|
|
|
19
22
|
// NOTE: If it is too low, and too many servers disconnect, we can easily spend 100% of our time
|
|
@@ -51,7 +54,7 @@ export interface CallFactory {
|
|
|
51
54
|
export interface SenderInterface {
|
|
52
55
|
nodeId?: string;
|
|
53
56
|
// Only set AFTER "open" (if set at all, as in the browser we don't have access to the socket).
|
|
54
|
-
|
|
57
|
+
_socket?: tls.TLSSocket;
|
|
55
58
|
|
|
56
59
|
send(data: string | Buffer): void;
|
|
57
60
|
|
|
@@ -201,8 +204,11 @@ export async function createCallFactory(
|
|
|
201
204
|
console.log(`SIZE\t${(formatNumberSuffixed(resultSize) + "B").padEnd(4, " ")}\tREMOTE CALL\t${call.classGuid}.${call.functionName}${fncHack} at ${Date.now()}`);
|
|
202
205
|
}
|
|
203
206
|
}
|
|
204
|
-
|
|
205
|
-
|
|
207
|
+
// If sending OR resultPromise throws, we want to error out. This solves some issues with resultPromise
|
|
208
|
+
// erroring out first, which is before we await it, which makes NodeJS angry (unhandled promise rejection).
|
|
209
|
+
// Also, technically, we could receive the result before we finish sending, in which case, we might
|
|
210
|
+
// as well return it immediately.
|
|
211
|
+
await Promise.race([send(data), resultPromise]);
|
|
206
212
|
return await resultPromise;
|
|
207
213
|
}
|
|
208
214
|
};
|
|
@@ -246,12 +252,12 @@ export async function createCallFactory(
|
|
|
246
252
|
// NOTE: No more logging, as we throw, so the caller should be logging the
|
|
247
253
|
// error (or swallowing it, if that is what it wants to do).
|
|
248
254
|
//console.log(`Websocket error for ${niceConnectionName}`, e.message);
|
|
249
|
-
onClose(`Connection error for ${niceConnectionName}: ${e.message}`);
|
|
255
|
+
onClose(new Error(`Connection error for ${niceConnectionName}: ${e.message}`).stack!);
|
|
250
256
|
});
|
|
251
257
|
|
|
252
258
|
newWebSocket.addEventListener("close", async () => {
|
|
253
259
|
//console.log(`Websocket closed ${niceConnectionName}`);
|
|
254
|
-
onClose(`Connection closed to ${niceConnectionName}`);
|
|
260
|
+
onClose(new Error(`Connection closed to ${niceConnectionName}`).stack!);
|
|
255
261
|
});
|
|
256
262
|
|
|
257
263
|
newWebSocket.addEventListener("message", onMessage);
|
|
@@ -269,9 +275,10 @@ export async function createCallFactory(
|
|
|
269
275
|
newWebSocket.addEventListener("close", () => resolve());
|
|
270
276
|
newWebSocket.addEventListener("error", () => resolve());
|
|
271
277
|
});
|
|
272
|
-
} else if (newWebSocket.readyState
|
|
273
|
-
onClose(`Websocket received in closed state`);
|
|
278
|
+
} else if (newWebSocket.readyState === 1 /* OPEN */) {
|
|
274
279
|
callFactory.isConnected = true;
|
|
280
|
+
} else {
|
|
281
|
+
onClose(new Error(`Websocket received in closed state`).stack!);
|
|
275
282
|
}
|
|
276
283
|
}
|
|
277
284
|
|
|
@@ -285,6 +292,7 @@ export async function createCallFactory(
|
|
|
285
292
|
bufferLengths?: number[];
|
|
286
293
|
metadata: Omit<InternalReturnType, "result">;
|
|
287
294
|
};
|
|
295
|
+
let sendInSerial = runInSerial(async (val: () => Promise<void>) => val());
|
|
288
296
|
async function sendRaw(data: (string | Buffer)[]) {
|
|
289
297
|
if (!webSocketPromise) {
|
|
290
298
|
if (canReconnect) {
|
|
@@ -294,9 +302,30 @@ export async function createCallFactory(
|
|
|
294
302
|
}
|
|
295
303
|
}
|
|
296
304
|
let webSocket = await webSocketPromise;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
305
|
+
await sendInSerial(async () => {
|
|
306
|
+
for (let d of data) {
|
|
307
|
+
if (d.length > 1000 * 1000 * 10) {
|
|
308
|
+
console.log(`Sending large packet ${formatNumber(d.length)}B to ${nodeId} at ${Date.now()}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// NOTE: If our latency is 500ms, with 10MB/s, then we need a high water
|
|
312
|
+
// mark of at least 5MB, otherwise our connection is slowed down.
|
|
313
|
+
// - Using the actual high water mark is too difficult, as we receive incoming connections.
|
|
314
|
+
// This is also easier to configure, and we can dynamically change it if we have to.
|
|
315
|
+
// NOTE: In practice we only hit this when sending large Buffers (~30MB), so low values
|
|
316
|
+
// are equivalent to waiting for drain. We want to avoid waiting for drain, so we use a high value.
|
|
317
|
+
const maxWriteBuffer = 128 * 1024 * 1024;
|
|
318
|
+
webSocket.send(d);
|
|
319
|
+
|
|
320
|
+
let socket = webSocket._socket;
|
|
321
|
+
if (socket) {
|
|
322
|
+
while (socket.writableLength > maxWriteBuffer) {
|
|
323
|
+
// NOTE: Waiting 1ms probably waits more like 16ms.
|
|
324
|
+
await new Promise(r => setTimeout(r, 1));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
});
|
|
300
329
|
}
|
|
301
330
|
async function send(data: Buffer[]) {
|
|
302
331
|
await sendRaw([
|
|
@@ -321,6 +350,7 @@ export async function createCallFactory(
|
|
|
321
350
|
}
|
|
322
351
|
}
|
|
323
352
|
data = fitBuffers;
|
|
353
|
+
header.bufferCount = fitBuffers.length;
|
|
324
354
|
} else {
|
|
325
355
|
throw new Error(`Cannot send large amounts of data unless we are returning Buffer or Buffer[]`);
|
|
326
356
|
}
|
|
@@ -366,7 +396,7 @@ export async function createCallFactory(
|
|
|
366
396
|
let lenLeft = len;
|
|
367
397
|
let buffers: Buffer[] = [];
|
|
368
398
|
while (lenLeft > 0) {
|
|
369
|
-
let buf = currentBuffers.
|
|
399
|
+
let buf = currentBuffers.shift();
|
|
370
400
|
if (!buf) {
|
|
371
401
|
throw new Error(`Not enough buffers received.`);
|
|
372
402
|
}
|
|
@@ -413,7 +443,7 @@ export async function createCallFactory(
|
|
|
413
443
|
if (call.isReturn) {
|
|
414
444
|
let callbackObj = pendingCalls.get(call.seqNum);
|
|
415
445
|
if (time > SocketFunction.WIRE_WARN_TIME) {
|
|
416
|
-
console.log(red(`Slow parse, took ${time}ms to parse ${resultSize} bytes, for
|
|
446
|
+
console.log(red(`Slow parse, took ${time}ms to parse ${resultSize} bytes, for receiving result of call to ${callbackObj?.call.classGuid}.${callbackObj?.call.functionName}`));
|
|
417
447
|
}
|
|
418
448
|
if (!callbackObj) {
|
|
419
449
|
console.log(`Got return for unknown call ${call.seqNum} (created at time ${new Date(call.seqNum)})`);
|
|
@@ -468,14 +498,15 @@ export async function createCallFactory(
|
|
|
468
498
|
let { result, ...remaining } = response;
|
|
469
499
|
await sendWithHeader(result, { type: "Buffer[]", bufferCount: result.length, metadata: remaining });
|
|
470
500
|
} else {
|
|
501
|
+
const LIMIT = getCallFlags(call)?.responseLimit || SocketFunction.MAX_MESSAGE_SIZE * 1.5;
|
|
471
502
|
let result: Buffer[] = await SocketFunction.WIRE_SERIALIZER.serialize(response);
|
|
472
503
|
let totalResultSize = result.map(x => x.length).reduce((a, b) => a + b, 0);
|
|
473
|
-
if (totalResultSize >
|
|
504
|
+
if (totalResultSize > LIMIT) {
|
|
474
505
|
response = {
|
|
475
506
|
isReturn: true,
|
|
476
507
|
result: undefined,
|
|
477
508
|
seqNum: call.seqNum,
|
|
478
|
-
error: new Error(`Response too large to send
|
|
509
|
+
error: new Error(`Response too large to send. Return Buffer[] to exceed the limits, or set responseLimit when registering the collection. ${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, if absolutely required, set SocketFunction.MAX_MESSAGE_SIZE to a higher value.`).stack,
|
|
479
510
|
};
|
|
480
511
|
result = await SocketFunction.WIRE_SERIALIZER.serialize(response);
|
|
481
512
|
}
|
|
@@ -490,14 +521,18 @@ export async function createCallFactory(
|
|
|
490
521
|
if (typeof message === "object" && "data" in message) {
|
|
491
522
|
message = message.data;
|
|
492
523
|
}
|
|
524
|
+
// Extra clienside parsing is required
|
|
493
525
|
if (!isNode()) {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
message
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
526
|
+
// Immediately start the arrayBuffer conversion. This should be fast, but...
|
|
527
|
+
// maybe we will add more here, and so doing it in parallel might be useful.
|
|
528
|
+
let fixMessageBlob = (async () => {
|
|
529
|
+
if (message instanceof Blob) {
|
|
530
|
+
message = Buffer.from(await message.arrayBuffer());
|
|
531
|
+
}
|
|
532
|
+
})();
|
|
533
|
+
// We need to force the results to be in serial, otherwise strings leapfrog
|
|
534
|
+
// ahead of buffers, which breaks things.
|
|
535
|
+
await clientsideSerial(fixMessageBlob);
|
|
501
536
|
}
|
|
502
537
|
if (typeof message === "string") {
|
|
503
538
|
if (message.startsWith("{")) {
|
|
@@ -530,6 +565,9 @@ export async function createCallFactory(
|
|
|
530
565
|
return;
|
|
531
566
|
}
|
|
532
567
|
if (message instanceof Buffer) {
|
|
568
|
+
if (message.byteLength > 1000 * 1000 * 10) {
|
|
569
|
+
console.log(`Received large packet ${formatNumber(message.byteLength)}B at ${Date.now()}`);
|
|
570
|
+
}
|
|
533
571
|
if (!pendingCall) {
|
|
534
572
|
throw new Error(`Received data without size`);
|
|
535
573
|
}
|
|
@@ -543,11 +581,13 @@ export async function createCallFactory(
|
|
|
543
581
|
}
|
|
544
582
|
throw new Error(`Unhandled data type ${typeof message}`);
|
|
545
583
|
} catch (e: any) {
|
|
546
|
-
let
|
|
584
|
+
let err = e.stack || e.message || e;
|
|
547
585
|
// NOTE: I'm looking for all types of errors here (specifically, .send errors), in case
|
|
548
586
|
// there are errors I should be handling.
|
|
549
|
-
if (
|
|
587
|
+
if (err.startsWith("Error: Cannot send data to") && err.includes("as the connection has closed")) {
|
|
550
588
|
// This is fine, just ignore it
|
|
589
|
+
} else if (err.includes("The requested file could not be read, typically due to permission problems that have occurred after a reference to a file was acquired.")) {
|
|
590
|
+
console.error(`WebSocket data was dropped by the browser due to exceeding the Blob limit. Either you are about to run out of memory, or you hit the much lower Incognito Blob limit. This will likely break the application. To reset the memory you must close all tabs of this site. This is a bug/feature in chrome.`);
|
|
551
591
|
} else {
|
|
552
592
|
debugbreak(2);
|
|
553
593
|
debugger;
|
|
@@ -559,16 +599,64 @@ export async function createCallFactory(
|
|
|
559
599
|
return callFactory;
|
|
560
600
|
}
|
|
561
601
|
|
|
602
|
+
async function doStream(stream: GenericTransformStream, buffer: Buffer): Promise<Buffer> {
|
|
603
|
+
let reader = stream.readable.getReader();
|
|
604
|
+
let writer = stream.writable.getWriter();
|
|
605
|
+
let writePromise = writer.write(buffer);
|
|
606
|
+
let closePromise = writer.close();
|
|
607
|
+
|
|
608
|
+
let outputBuffers: Buffer[] = [];
|
|
609
|
+
while (true) {
|
|
610
|
+
let { value, done } = await reader.read();
|
|
611
|
+
if (done) {
|
|
612
|
+
await writePromise;
|
|
613
|
+
await closePromise;
|
|
614
|
+
return Buffer.concat(outputBuffers);
|
|
615
|
+
}
|
|
616
|
+
outputBuffers.push(Buffer.from(value));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
async function unzipBase(buffer: Buffer): Promise<Buffer> {
|
|
620
|
+
if (isNode()) {
|
|
621
|
+
return new Promise((resolve, reject) => {
|
|
622
|
+
zlib.gunzip(buffer, (err: any, result: Buffer) => {
|
|
623
|
+
if (err) reject(err);
|
|
624
|
+
else resolve(result);
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
} else {
|
|
628
|
+
// NOTE: pako seems to be faster, at least clientside.
|
|
629
|
+
// TIMING: 700ms vs 1200ms
|
|
630
|
+
// - This might just be faster for small files.
|
|
631
|
+
return Buffer.from(pako.inflate(buffer));
|
|
632
|
+
// @ts-ignore
|
|
633
|
+
// return await doStream(new DecompressionStream("gzip"), buffer);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async function zipBase(buffer: Buffer, level?: number): Promise<Buffer> {
|
|
637
|
+
if (isNode()) {
|
|
638
|
+
return new Promise((resolve, reject) => {
|
|
639
|
+
zlib.gzip(buffer, { level }, (err: any, result: Buffer) => {
|
|
640
|
+
if (err) reject(err);
|
|
641
|
+
else resolve(result);
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
} else {
|
|
645
|
+
// @ts-ignore
|
|
646
|
+
return await doStream(new CompressionStream("gzip"), buffer);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
562
649
|
|
|
563
|
-
async function
|
|
650
|
+
const compressObj = measureWrap(async function wireCallCompress(obj: unknown): Promise<Buffer> {
|
|
564
651
|
let buffers = await SocketFunction.WIRE_SERIALIZER.serialize(obj);
|
|
565
652
|
let lengthBuffer = Buffer.from((new Float64Array(buffers.map(x => x.length))).buffer);
|
|
566
653
|
let buffer = Buffer.concat([lengthBuffer, ...buffers]);
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
654
|
+
let result = await zipBase(buffer);
|
|
655
|
+
return result;
|
|
656
|
+
});
|
|
657
|
+
const decompressObj = measureWrap(async function wireCallDecompress(obj: Buffer): Promise<unknown> {
|
|
570
658
|
try {
|
|
571
|
-
let buffer =
|
|
659
|
+
let buffer = await unzipBase(obj);
|
|
572
660
|
let lengthBuffer = buffer.slice(0, 8);
|
|
573
661
|
let lengths = new Float64Array(lengthBuffer.buffer, lengthBuffer.byteOffset, lengthBuffer.byteLength / 8);
|
|
574
662
|
let buffers: Buffer[] = [];
|
|
@@ -586,4 +674,4 @@ async function decompressObj(obj: Buffer): Promise<unknown> {
|
|
|
586
674
|
debugger;
|
|
587
675
|
throw e;
|
|
588
676
|
}
|
|
589
|
-
}
|
|
677
|
+
});
|