socket-function 0.38.0 → 0.40.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 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
- return hookResult.overrideResult;
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
- return await callFactory.performCall(call);
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<SocketExposedInterface>) {
425
+ public static addGlobalHook(hook: SocketFunctionHook) {
378
426
  registerGlobalHook(hook as SocketFunctionHook);
379
427
  }
380
- public static addGlobalClientHook(hook: SocketFunctionClientHook<SocketExposedInterface>) {
428
+ public static addGlobalClientHook(hook: SocketFunctionClientHook) {
381
429
  registerGlobalClientHook(hook as SocketFunctionClientHook);
382
430
  }
383
431
  }
@@ -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
- hooks?: SocketFunctionHook<ExposedType>[];
39
- clientHooks?: SocketFunctionClientHook<ExposedType>[];
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<ExposedType extends SocketExposedInterface = SocketExposedInterface> {
57
- (config: HookContext<ExposedType>): MaybePromise<void>;
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<ExposedType>;
64
+ clientHook?: SocketFunctionClientHook;
60
65
  }
61
- export type HookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface> = {
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<ExposedType extends SocketExposedInterface = SocketExposedInterface> = {
77
+ export type ClientHookContext = {
68
78
  call: FullCallType;
69
- // If the result is overriden, we continue evaluating hooks BUT DO NOT perform the final call
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<ExposedType extends SocketExposedInterface = SocketExposedInterface> {
74
- (config: ClientHookContext<ExposedType>): MaybePromise<void>;
89
+ export interface SocketFunctionClientHook {
90
+ (config: ClientHookContext): MaybePromise<void>;
75
91
  }
76
92
 
77
93
  export interface SocketRegisterType<ExposedType = any> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socket-function",
3
- "version": "0.38.0",
3
+ "version": "0.40.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",
@@ -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: 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(Date.now() - startTime);
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,
@@ -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
- return serverContext.overrideResult;
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
- if ("overrideResult" in hookContext) {
153
- break;
154
- }
164
+ // NOTE: See HookContext.overrideResult for why we don't break here
155
165
  }
156
166
  return hookContext;
157
167
  });
@@ -30,7 +30,8 @@ const measureOverhead = 5 / 1000;
30
30
 
31
31
  const AsyncFunction = (async () => { }).constructor;
32
32
 
33
- const noDiskLogPrefix = "\u200C";
33
+ // NOTE: Also hardcoded in diskLogger.ts (in querysub)
34
+ const noDiskLogPrefix = "█ ";
34
35
 
35
36
  // TIMING: 1-5us. I have seen timing values greatly vary, but it does seem to be quite high, despite
36
37
  // microbenchmarks saying it is slow. Perhaps it is because getOwnTime breaks the cpu pipeline,
@@ -78,6 +79,9 @@ export function measureBlock<T extends (...args: any[]) => any>(fnc: T, name?: s
78
79
  }
79
80
 
80
81
  let extraInfoGetters: (() => string | undefined)[] = [];
82
+ /** NOTE: You should often call registerNodeMetadata for this as well. registerMeasureInfo
83
+ * is for logs, while registerNodeMetadata is for the overview page.
84
+ */
81
85
  export function registerMeasureInfo(getInfo: () => string | undefined) {
82
86
  extraInfoGetters.push(getInfo);
83
87
  }