socket-function 0.39.0 → 0.41.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 +52 -4
- package/SocketFunctionTypes.ts +26 -10
- package/package.json +1 -1
- package/src/CallFactory.ts +33 -2
- package/src/callManager.ts +16 -6
package/SocketFunction.ts
CHANGED
|
@@ -16,6 +16,7 @@ import "./SetProcessVariables";
|
|
|
16
16
|
import cborx from "cbor-x";
|
|
17
17
|
import { setFlag } from "./require/compileFlags";
|
|
18
18
|
import { isNode } from "./src/misc";
|
|
19
|
+
import { getPendingCallCount, harvestCallTimes, harvestFailedCallCount } from "./src/CallFactory";
|
|
19
20
|
|
|
20
21
|
/** Always shim Date.now(), because we usually DO want an accurate time... */
|
|
21
22
|
setImmediate(async () => {
|
|
@@ -84,8 +85,43 @@ export class SocketFunction {
|
|
|
84
85
|
return caller;
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
public static harvestFailedCallCount = () => harvestFailedCallCount();
|
|
89
|
+
public static getPendingCallCount = () => getPendingCallCount();
|
|
90
|
+
public static harvestCallTimes = () => harvestCallTimes();
|
|
91
|
+
|
|
87
92
|
// NOTE: We use callbacks we don't run into issues with cyclic dependencies
|
|
88
93
|
// (ex, using a hook in a controller where the hook also calls the controller).
|
|
94
|
+
/*
|
|
95
|
+
export const DiskLoggerController = SocketFunction.register(
|
|
96
|
+
// Can be anything, but should be unique amongst other controllers on your server.
|
|
97
|
+
"DiskLoggerController-f76a6fdf-3bd5-4bd4-a183-55a8be0a5a32",
|
|
98
|
+
// Contains the functions that can be exposed, which must all be async.
|
|
99
|
+
// Only those listed below will be exposed.
|
|
100
|
+
new DiskLoggerControllerBase(),
|
|
101
|
+
() => ({
|
|
102
|
+
// Only functions listed here will be exposed
|
|
103
|
+
getRemoteLogFiles: {},
|
|
104
|
+
getRemoteLogBuffer: {
|
|
105
|
+
compress: true,
|
|
106
|
+
// SocketFunctionClientHook[]
|
|
107
|
+
clientHooks: [
|
|
108
|
+
(x) => {
|
|
109
|
+
// If overrideResult is set, it skips the call and returns overrideResult
|
|
110
|
+
x.overrideResult = Buffer.from(...);
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
() => ({
|
|
116
|
+
// Default hooks for all functions
|
|
117
|
+
// SocketFunctionHook[]
|
|
118
|
+
hooks: [assertIsManagementUser],
|
|
119
|
+
}),
|
|
120
|
+
{
|
|
121
|
+
// Additionaly flags
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
*/
|
|
89
125
|
public static register<
|
|
90
126
|
ClassInstance extends object,
|
|
91
127
|
Shape extends SocketExposedShape<{
|
|
@@ -213,10 +249,22 @@ export class SocketFunction {
|
|
|
213
249
|
let hookResult = await runClientHooks(call, shapeObj as Exclude<SocketExposedShape[""], undefined>, callFactory.connectionId);
|
|
214
250
|
|
|
215
251
|
if ("overrideResult" in hookResult) {
|
|
216
|
-
|
|
252
|
+
for (let callback of hookResult.onResult) {
|
|
253
|
+
await callback(hookResult.overrideResult);
|
|
254
|
+
}
|
|
255
|
+
if ("overrideResult" in hookResult) {
|
|
256
|
+
return hookResult.overrideResult;
|
|
257
|
+
}
|
|
217
258
|
}
|
|
218
259
|
|
|
219
|
-
|
|
260
|
+
let result = await callFactory.performCall(call);
|
|
261
|
+
for (let callback of hookResult.onResult) {
|
|
262
|
+
await callback(result);
|
|
263
|
+
}
|
|
264
|
+
if ("overrideResult" in hookResult) {
|
|
265
|
+
return hookResult.overrideResult;
|
|
266
|
+
}
|
|
267
|
+
return result;
|
|
220
268
|
} finally {
|
|
221
269
|
time = Date.now() - time;
|
|
222
270
|
if (SocketFunction.logMessages) {
|
|
@@ -374,10 +422,10 @@ export class SocketFunction {
|
|
|
374
422
|
return SocketFunction.connect({ address: location.hostname, port: +location.port || 443 });
|
|
375
423
|
}
|
|
376
424
|
|
|
377
|
-
public static addGlobalHook(hook: SocketFunctionHook
|
|
425
|
+
public static addGlobalHook(hook: SocketFunctionHook) {
|
|
378
426
|
registerGlobalHook(hook as SocketFunctionHook);
|
|
379
427
|
}
|
|
380
|
-
public static addGlobalClientHook(hook: SocketFunctionClientHook
|
|
428
|
+
public static addGlobalClientHook(hook: SocketFunctionClientHook) {
|
|
381
429
|
registerGlobalClientHook(hook as SocketFunctionClientHook);
|
|
382
430
|
}
|
|
383
431
|
}
|
package/SocketFunctionTypes.ts
CHANGED
|
@@ -35,8 +35,13 @@ export type FunctionFlags = {
|
|
|
35
35
|
};
|
|
36
36
|
export type SocketExposedShape<ExposedType extends SocketExposedInterface = SocketExposedInterface> = {
|
|
37
37
|
[functionName in keyof ExposedType]?: FunctionFlags & {
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// NOTE: Due tohow register is called we can't use ExposedType[functionName] here,
|
|
39
|
+
// because we didn'tt use the double call pattern. Maybe we will later,
|
|
40
|
+
// but the type benefits are marginal. Args and overrideResult can be typed,
|
|
41
|
+
// but 99% of the time those are used by generic helper functions anyways,
|
|
42
|
+
// which only want unknowns anyways.
|
|
43
|
+
hooks?: SocketFunctionHook[];
|
|
44
|
+
clientHooks?: SocketFunctionClientHook[];
|
|
40
45
|
noDefaultHooks?: boolean;
|
|
41
46
|
/** BUG: I think this is broken if it is on the default hooks function? */
|
|
42
47
|
noClientHooks?: boolean;
|
|
@@ -53,25 +58,36 @@ export interface FullCallType<FncT extends FncType = FncType, FncName extends st
|
|
|
53
58
|
nodeId: string;
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
export interface SocketFunctionHook
|
|
57
|
-
(config: HookContext
|
|
61
|
+
export interface SocketFunctionHook {
|
|
62
|
+
(config: HookContext): MaybePromise<void>;
|
|
58
63
|
/** NOTE: This is useful when we need a clientside hook to set up state specifically for our serverside hook. */
|
|
59
|
-
clientHook?: SocketFunctionClientHook
|
|
64
|
+
clientHook?: SocketFunctionClientHook;
|
|
60
65
|
}
|
|
61
|
-
export type HookContext
|
|
66
|
+
export type HookContext = {
|
|
62
67
|
call: FullCallType;
|
|
63
68
|
// If the result is overriden, we continue evaluating hooks BUT DO NOT perform the final call
|
|
69
|
+
// - It is important we continue evaluating hooks, in case some later hooks check permissions
|
|
70
|
+
// and throw. We wouldn't want a caching layer to accidentally avoid a permissions check.
|
|
64
71
|
overrideResult?: unknown;
|
|
72
|
+
// Is called on a result, even if it is from overrideResult
|
|
73
|
+
// Maybe further mutate overrideResult, or even add it
|
|
74
|
+
onResult: ((result: unknown) => MaybePromise<void>)[];
|
|
65
75
|
};
|
|
66
76
|
|
|
67
|
-
export type ClientHookContext
|
|
77
|
+
export type ClientHookContext = {
|
|
68
78
|
call: FullCallType;
|
|
69
|
-
// If the result is overriden, we
|
|
79
|
+
// If the result is overriden, we STOP evaluating hooks and do not perform the final call
|
|
80
|
+
// - We stop evaluating hooks, because other hooks might end up making unnecessary calls,
|
|
81
|
+
// which won't be needed, because we aren't calling the server. There is no security issue,
|
|
82
|
+
// because the clientside checks are never security checks (how could they be, the client
|
|
83
|
+
// can't authorize itself...)
|
|
70
84
|
overrideResult?: unknown;
|
|
85
|
+
// Is called on a result, even if it is from overrideResult
|
|
86
|
+
onResult: ((result: unknown) => MaybePromise<void>)[];
|
|
71
87
|
connectionId: { nodeId: string };
|
|
72
88
|
};
|
|
73
|
-
export interface SocketFunctionClientHook
|
|
74
|
-
(config: ClientHookContext
|
|
89
|
+
export interface SocketFunctionClientHook {
|
|
90
|
+
(config: ClientHookContext): MaybePromise<void>;
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
export interface SocketRegisterType<ExposedType = any> {
|
package/package.json
CHANGED
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
3
|
import { getCallFlags, performLocalCall, shouldCompressCall } from "./callManager";
|
|
4
|
-
import { convertErrorStackToError, formatNumberSuffixed, isBufferType, isNode, list } from "./misc";
|
|
4
|
+
import { convertErrorStackToError, formatNumberSuffixed, isBufferType, isNode, list, timeInHour, timeInMinute } from "./misc";
|
|
5
5
|
import { createWebsocketFactory, getTLSSocket } from "./websocketFactory";
|
|
6
6
|
import { SocketFunction } from "../SocketFunction";
|
|
7
7
|
import * as tls from "tls";
|
|
@@ -10,7 +10,7 @@ import debugbreak from "debugbreak";
|
|
|
10
10
|
import { lazy } from "./caching";
|
|
11
11
|
import { red, yellow } from "./formatting/logColors";
|
|
12
12
|
import { isSplitableArray, markArrayAsSplitable } from "./fixLargeNetworkCalls";
|
|
13
|
-
import { delay, runInSerial } from "./batching";
|
|
13
|
+
import { delay, runInfinitePoll, runInSerial } from "./batching";
|
|
14
14
|
import { formatNumber, formatTime } from "./formatting/format";
|
|
15
15
|
import zlib from "zlib";
|
|
16
16
|
import pako from "pako";
|
|
@@ -68,6 +68,30 @@ export interface SenderInterface {
|
|
|
68
68
|
ping?(): void;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
let pendingCallCount = 0;
|
|
72
|
+
let harvestableFailedCalls = 0;
|
|
73
|
+
const CALL_TIMES_LIMIT = 1000 * 1000 * 10;
|
|
74
|
+
let harvestableCallTimes: { start: number; end: number; }[] = [];
|
|
75
|
+
export function harvestFailedCallCount() {
|
|
76
|
+
let count = harvestableFailedCalls;
|
|
77
|
+
harvestableFailedCalls = 0;
|
|
78
|
+
return count;
|
|
79
|
+
}
|
|
80
|
+
export function getPendingCallCount() {
|
|
81
|
+
return pendingCallCount;
|
|
82
|
+
}
|
|
83
|
+
export function harvestCallTimes() {
|
|
84
|
+
let times = harvestableCallTimes;
|
|
85
|
+
harvestableCallTimes = [];
|
|
86
|
+
return times;
|
|
87
|
+
}
|
|
88
|
+
runInfinitePoll(timeInMinute * 15, () => {
|
|
89
|
+
if (harvestableCallTimes.length > CALL_TIMES_LIMIT) {
|
|
90
|
+
harvestableCallTimes = harvestableCallTimes.slice(-CALL_TIMES_LIMIT);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
|
|
71
95
|
export async function createCallFactory(
|
|
72
96
|
webSocketBase: SenderInterface | undefined,
|
|
73
97
|
// The node id we are connecting to (or that connected to us)
|
|
@@ -179,8 +203,13 @@ export async function createCallFactory(
|
|
|
179
203
|
}
|
|
180
204
|
|
|
181
205
|
let resultPromise = new Promise((resolve, reject) => {
|
|
206
|
+
let startTime = Date.now();
|
|
207
|
+
pendingCallCount++;
|
|
182
208
|
let callback = (result: InternalReturnType) => {
|
|
209
|
+
pendingCallCount--;
|
|
183
210
|
pendingCalls.delete(seqNum);
|
|
211
|
+
harvestableCallTimes.push({ start: startTime, end: Date.now(), });
|
|
212
|
+
|
|
184
213
|
if (result.error) {
|
|
185
214
|
reject(convertErrorStackToError(result.error));
|
|
186
215
|
} else {
|
|
@@ -225,11 +254,13 @@ export async function createCallFactory(
|
|
|
225
254
|
function onClose(error: string) {
|
|
226
255
|
callFactory.connectionId = { nodeId };
|
|
227
256
|
callFactory.lastClosed = Date.now();
|
|
257
|
+
callFactory.isConnected = false;
|
|
228
258
|
webSocketPromise = undefined;
|
|
229
259
|
if (!canReconnect) {
|
|
230
260
|
callFactory.closedForever = true;
|
|
231
261
|
}
|
|
232
262
|
for (let [key, call] of pendingCalls) {
|
|
263
|
+
harvestableFailedCalls++;
|
|
233
264
|
pendingCalls.delete(key);
|
|
234
265
|
call.callback({
|
|
235
266
|
isReturn: true,
|
package/src/callManager.ts
CHANGED
|
@@ -51,14 +51,25 @@ export async function performLocalCall(
|
|
|
51
51
|
|
|
52
52
|
let serverContext = await runServerHooks(call, caller, functionShape);
|
|
53
53
|
if ("overrideResult" in serverContext) {
|
|
54
|
-
|
|
54
|
+
for (let callback of serverContext.onResult) {
|
|
55
|
+
await callback(serverContext.overrideResult);
|
|
56
|
+
}
|
|
57
|
+
if ("overrideResult" in serverContext) {
|
|
58
|
+
return serverContext.overrideResult;
|
|
59
|
+
}
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
// NOTE: We purposely don't await inside _setSocketContext, so the context is reset synchronously
|
|
58
63
|
let result = _setSocketContext(caller, () => {
|
|
59
64
|
return controller[call.functionName](...call.args);
|
|
60
65
|
});
|
|
66
|
+
for (let callback of serverContext.onResult) {
|
|
67
|
+
await callback(result);
|
|
68
|
+
}
|
|
61
69
|
|
|
70
|
+
if ("overrideResult" in serverContext) {
|
|
71
|
+
return serverContext.overrideResult;
|
|
72
|
+
}
|
|
62
73
|
return await result;
|
|
63
74
|
}
|
|
64
75
|
|
|
@@ -120,7 +131,7 @@ export const runClientHooks = measureWrap(async function runClientHooks(
|
|
|
120
131
|
hooks: Exclude<SocketExposedShape[""], undefined>,
|
|
121
132
|
connectionId: { nodeId: string },
|
|
122
133
|
): Promise<ClientHookContext> {
|
|
123
|
-
let context: ClientHookContext = { call: callType, connectionId };
|
|
134
|
+
let context: ClientHookContext = { call: callType, connectionId, onResult: [] };
|
|
124
135
|
|
|
125
136
|
let clientHooks = hooks.clientHooks?.slice() || [];
|
|
126
137
|
if (!hooks.noClientHooks) {
|
|
@@ -133,6 +144,7 @@ export const runClientHooks = measureWrap(async function runClientHooks(
|
|
|
133
144
|
}
|
|
134
145
|
for (let hook of clientHooks) {
|
|
135
146
|
await hook(context);
|
|
147
|
+
// NOTE: See ClientHookContext.overrideResult for why we break here
|
|
136
148
|
if ("overrideResult" in context) {
|
|
137
149
|
break;
|
|
138
150
|
}
|
|
@@ -146,12 +158,10 @@ export const runServerHooks = measureWrap(async function runServerHooks(
|
|
|
146
158
|
caller: CallerContext,
|
|
147
159
|
hooks: Exclude<SocketExposedShape[""], undefined>,
|
|
148
160
|
): Promise<HookContext> {
|
|
149
|
-
let hookContext: HookContext = { call: callType };
|
|
161
|
+
let hookContext: HookContext = { call: callType, onResult: [] };
|
|
150
162
|
for (let hook of globalHooks.concat(hooks.hooks || [])) {
|
|
151
163
|
await _setSocketContext(caller, () => hook(hookContext));
|
|
152
|
-
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
164
|
+
// NOTE: See HookContext.overrideResult for why we don't break here
|
|
155
165
|
}
|
|
156
166
|
return hookContext;
|
|
157
167
|
});
|