socket-function 0.9.3 → 0.9.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/.eslintrc.js +50 -50
- package/SocketFunction.ts +280 -280
- package/SocketFunctionTypes.ts +90 -90
- package/hot/HotReloadController.ts +105 -105
- package/mobx/UrlParam.ts +39 -39
- package/mobx/observer.tsx +49 -49
- package/mobx/promiseToObservable.tsx +41 -41
- package/package.json +1 -1
- package/require/CSSShim.ts +19 -19
- package/require/RequireController.ts +252 -252
- package/require/buffer.js +2368 -2368
- package/require/compileFlags.ts +44 -44
- package/require/require.html +13 -13
- package/require/require.js +464 -462
- package/spec.txt +115 -115
- package/src/CallFactory.ts +389 -389
- package/src/JSONLACKS/JSONLACKS.generated.js +17 -17
- package/src/JSONLACKS/JSONLACKS.pegjs +247 -247
- package/src/JSONLACKS/JSONLACKS.ts +441 -429
- package/src/args.ts +21 -21
- package/src/batching.ts +177 -170
- package/src/caching.ts +359 -318
- package/src/callHTTPHandler.ts +203 -203
- package/src/callManager.ts +134 -134
- package/src/certStore.ts +29 -29
- package/src/fixLargeNetworkCalls.ts +8 -8
- package/src/formatting/colors.ts +78 -78
- package/src/formatting/format.ts +160 -160
- package/src/formatting/logColors.ts +17 -17
- package/src/misc.ts +315 -302
- package/src/nodeCache.ts +92 -92
- package/src/nodeProxy.ts +54 -54
- package/src/profiling/getOwnTime.ts +107 -142
- package/src/profiling/measure.ts +289 -273
- package/src/profiling/stats.ts +212 -212
- package/src/profiling/tcpLagProxy.ts +63 -63
- package/src/storagePath.ts +10 -10
- package/src/tlsParsing.ts +96 -96
- package/src/types.ts +8 -8
- package/src/webSocketServer.ts +254 -250
- package/test/client.css +2 -2
- package/test/client.ts +46 -46
- package/test/server.ts +43 -43
- package/test/shared.ts +52 -52
- package/tsconfig.json +26 -26
package/.eslintrc.js
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
"root": true,
|
|
3
|
-
"env": {
|
|
4
|
-
"browser": true,
|
|
5
|
-
"es6": true,
|
|
6
|
-
"node": true
|
|
7
|
-
},
|
|
8
|
-
"extends": [],
|
|
9
|
-
"globals": {
|
|
10
|
-
"Atomics": "readonly",
|
|
11
|
-
"SharedArrayBuffer": "readonly"
|
|
12
|
-
},
|
|
13
|
-
"overrides": [
|
|
14
|
-
{
|
|
15
|
-
"files": [
|
|
16
|
-
"**/*.ts",
|
|
17
|
-
"**/*.tsx"
|
|
18
|
-
]
|
|
19
|
-
}
|
|
20
|
-
],
|
|
21
|
-
"parser": "@typescript-eslint/parser",
|
|
22
|
-
"parserOptions": {
|
|
23
|
-
"ecmaVersion": 2018,
|
|
24
|
-
"sourceType": "module",
|
|
25
|
-
"project": "./tsconfig.json",
|
|
26
|
-
"tsconfigRootDir": __dirname,
|
|
27
|
-
},
|
|
28
|
-
"plugins": [
|
|
29
|
-
"@typescript-eslint"
|
|
30
|
-
],
|
|
31
|
-
"rules": {
|
|
32
|
-
"@typescript-eslint/unbound-method": [
|
|
33
|
-
"error",
|
|
34
|
-
{
|
|
35
|
-
"ignoreStatic": true
|
|
36
|
-
}
|
|
37
|
-
],
|
|
38
|
-
"@typescript-eslint/no-floating-promises": [
|
|
39
|
-
"error"
|
|
40
|
-
],
|
|
41
|
-
"quotes": [
|
|
42
|
-
"error",
|
|
43
|
-
"double",
|
|
44
|
-
{
|
|
45
|
-
"allowTemplateLiterals": true
|
|
46
|
-
}
|
|
47
|
-
],
|
|
48
|
-
"eqeqeq": "error",
|
|
49
|
-
"semi": "error"
|
|
50
|
-
}
|
|
1
|
+
module.exports = {
|
|
2
|
+
"root": true,
|
|
3
|
+
"env": {
|
|
4
|
+
"browser": true,
|
|
5
|
+
"es6": true,
|
|
6
|
+
"node": true
|
|
7
|
+
},
|
|
8
|
+
"extends": [],
|
|
9
|
+
"globals": {
|
|
10
|
+
"Atomics": "readonly",
|
|
11
|
+
"SharedArrayBuffer": "readonly"
|
|
12
|
+
},
|
|
13
|
+
"overrides": [
|
|
14
|
+
{
|
|
15
|
+
"files": [
|
|
16
|
+
"**/*.ts",
|
|
17
|
+
"**/*.tsx"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"parser": "@typescript-eslint/parser",
|
|
22
|
+
"parserOptions": {
|
|
23
|
+
"ecmaVersion": 2018,
|
|
24
|
+
"sourceType": "module",
|
|
25
|
+
"project": "./tsconfig.json",
|
|
26
|
+
"tsconfigRootDir": __dirname,
|
|
27
|
+
},
|
|
28
|
+
"plugins": [
|
|
29
|
+
"@typescript-eslint"
|
|
30
|
+
],
|
|
31
|
+
"rules": {
|
|
32
|
+
"@typescript-eslint/unbound-method": [
|
|
33
|
+
"error",
|
|
34
|
+
{
|
|
35
|
+
"ignoreStatic": true
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"@typescript-eslint/no-floating-promises": [
|
|
39
|
+
"error"
|
|
40
|
+
],
|
|
41
|
+
"quotes": [
|
|
42
|
+
"error",
|
|
43
|
+
"double",
|
|
44
|
+
{
|
|
45
|
+
"allowTemplateLiterals": true
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"eqeqeq": "error",
|
|
49
|
+
"semi": "error"
|
|
50
|
+
}
|
|
51
51
|
};
|
package/SocketFunction.ts
CHANGED
|
@@ -1,281 +1,281 @@
|
|
|
1
|
-
/// <reference path="./require/RequireController.ts" />
|
|
2
|
-
|
|
3
|
-
import { SocketExposedInterface, SocketFunctionHook, SocketFunctionClientHook, SocketExposedShape, SocketRegistered, CallerContext, FullCallType } from "./SocketFunctionTypes";
|
|
4
|
-
import { exposeClass, registerClass, registerGlobalClientHook, registerGlobalHook, runClientHooks } from "./src/callManager";
|
|
5
|
-
import { SocketServerConfig, startSocketServer } from "./src/webSocketServer";
|
|
6
|
-
import { getCallFactory, getCreateCallFactory, getNodeId, getNodeIdLocation } from "./src/nodeCache";
|
|
7
|
-
import { getCallProxy } from "./src/nodeProxy";
|
|
8
|
-
import { Args, MaybePromise } from "./src/types";
|
|
9
|
-
import { setDefaultHTTPCall } from "./src/callHTTPHandler";
|
|
10
|
-
import debugbreak from "debugbreak";
|
|
11
|
-
import { lazy } from "./src/caching";
|
|
12
|
-
import { delay } from "./src/batching";
|
|
13
|
-
|
|
14
|
-
module.allowclient = true;
|
|
15
|
-
|
|
16
|
-
type ExtractShape<ClassType, Shape> = {
|
|
17
|
-
[key in keyof Shape]: (
|
|
18
|
-
key extends keyof ClassType
|
|
19
|
-
? ClassType[key] extends SocketExposedInterface[""]
|
|
20
|
-
? ClassType[key]
|
|
21
|
-
: ClassType[key] extends Function ? "All exposed function must be async (or return a Promise)" : never
|
|
22
|
-
: "Function is in shape, but not in class"
|
|
23
|
-
);
|
|
24
|
-
};
|
|
25
|
-
// https://stackoverflow.com/a/69756175/1117119
|
|
26
|
-
type PickByType<T, Value> = {
|
|
27
|
-
[P in keyof T as T[P] extends Value | undefined ? P : never]: T[P]
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export class SocketFunction {
|
|
31
|
-
public static logMessages = false;
|
|
32
|
-
|
|
33
|
-
public static MAX_MESSAGE_SIZE = 1024 * 1024 * 32;
|
|
34
|
-
public static compression: undefined | {
|
|
35
|
-
type: "gzip";
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
public static httpETagCache = false;
|
|
39
|
-
public static silent = true;
|
|
40
|
-
|
|
41
|
-
public static WIRE_WARN_TIME = 100;
|
|
42
|
-
|
|
43
|
-
private static onMountCallbacks = new Map<string, (() => MaybePromise<void>)[]>();
|
|
44
|
-
public static exposedClasses = new Set<string>();
|
|
45
|
-
|
|
46
|
-
public static callerContext: CallerContext | undefined;
|
|
47
|
-
public static getCaller(): CallerContext {
|
|
48
|
-
const caller = SocketFunction.callerContext;
|
|
49
|
-
if (!caller) throw new Error(`Tried to access caller when not in the synchronous phase of a function call`);
|
|
50
|
-
return caller;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// NOTE: We use callbacks we don't run into issues with cyclic dependencies
|
|
54
|
-
// (ex, using a hook in a controller where the hook also calls the controller).
|
|
55
|
-
public static register<
|
|
56
|
-
ClassInstance extends object,
|
|
57
|
-
Shape extends SocketExposedShape<SocketExposedInterface>,
|
|
58
|
-
>(
|
|
59
|
-
classGuid: string,
|
|
60
|
-
instance: ClassInstance,
|
|
61
|
-
shapeFnc: () => Shape,
|
|
62
|
-
defaultHooksFnc?: () => SocketExposedShape[""] & {
|
|
63
|
-
onMount?: () => MaybePromise<void>;
|
|
64
|
-
}
|
|
65
|
-
): SocketRegistered<ExtractShape<ClassInstance, Shape>> {
|
|
66
|
-
let getDefaultHooks = defaultHooksFnc && lazy(defaultHooksFnc);
|
|
67
|
-
const getShape = lazy(() => {
|
|
68
|
-
let shape = shapeFnc();
|
|
69
|
-
let defaultHooks = getDefaultHooks?.();
|
|
70
|
-
|
|
71
|
-
for (let value of Object.values(shape)) {
|
|
72
|
-
if (!value) continue;
|
|
73
|
-
value.clientHooks = [...(defaultHooks?.clientHooks || []), ...(value.clientHooks || [])];
|
|
74
|
-
value.hooks = [...(defaultHooks?.hooks || []), ...(value.hooks || [])];
|
|
75
|
-
value.dataImmutable = defaultHooks?.dataImmutable ?? value.dataImmutable;
|
|
76
|
-
}
|
|
77
|
-
return shape as any as SocketExposedShape;
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
void Promise.resolve().then(() => {
|
|
81
|
-
registerClass(classGuid, instance as SocketExposedInterface, getShape());
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
let nodeProxy = getCallProxy(classGuid, async (call) => {
|
|
85
|
-
let nodeId = call.nodeId;
|
|
86
|
-
let functionName = call.functionName;
|
|
87
|
-
let time = Date.now();
|
|
88
|
-
if (SocketFunction.logMessages) {
|
|
89
|
-
console.log(`START\t\t\t${classGuid}.${functionName} at ${Date.now()}`);
|
|
90
|
-
}
|
|
91
|
-
try {
|
|
92
|
-
let callFactory = await getCreateCallFactory(nodeId);
|
|
93
|
-
|
|
94
|
-
let shapeObj = getShape()[functionName];
|
|
95
|
-
if (!shapeObj) {
|
|
96
|
-
throw new Error(`Function ${functionName} is not in shape`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
let hookResult = await runClientHooks(call, shapeObj as SocketExposedShape[""], callFactory.connectionId);
|
|
100
|
-
|
|
101
|
-
if ("overrideResult" in hookResult) {
|
|
102
|
-
return hookResult.overrideResult;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return await callFactory.performCall(call);
|
|
106
|
-
} finally {
|
|
107
|
-
time = Date.now() - time;
|
|
108
|
-
if (SocketFunction.logMessages) {
|
|
109
|
-
console.log(`FINISHED\t${time}ms\t${classGuid}.${functionName} at ${Date.now()}`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
let output: SocketRegistered = {
|
|
115
|
-
nodes: nodeProxy,
|
|
116
|
-
_classGuid: classGuid,
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
void Promise.resolve().then(() => {
|
|
120
|
-
let onMount = getDefaultHooks?.().onMount;
|
|
121
|
-
if (onMount) {
|
|
122
|
-
let callbacks = SocketFunction.onMountCallbacks.get(classGuid);
|
|
123
|
-
if (!callbacks) {
|
|
124
|
-
callbacks = [];
|
|
125
|
-
SocketFunction.onMountCallbacks.set(classGuid, callbacks);
|
|
126
|
-
}
|
|
127
|
-
callbacks.push(onMount);
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
return output as any;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
public static onNextDisconnect(nodeId: string, callback: () => void) {
|
|
135
|
-
(async () => {
|
|
136
|
-
let factory = await getCallFactory(nodeId);
|
|
137
|
-
if (!factory) {
|
|
138
|
-
callback();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
factory.onNextDisconnect(callback);
|
|
143
|
-
})().catch(() => {
|
|
144
|
-
callback();
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
public static getLastDisconnectTime(nodeId: string): number | undefined {
|
|
148
|
-
let factory = getCallFactory(nodeId);
|
|
149
|
-
if (!factory) {
|
|
150
|
-
return undefined;
|
|
151
|
-
}
|
|
152
|
-
if (factory instanceof Promise) {
|
|
153
|
-
return undefined;
|
|
154
|
-
}
|
|
155
|
-
return factory.lastClosed;
|
|
156
|
-
}
|
|
157
|
-
public static isNodeConnected(nodeId: string): boolean {
|
|
158
|
-
let factory = getCallFactory(nodeId);
|
|
159
|
-
if (!factory) {
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
if (factory instanceof Promise) {
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
165
|
-
return !!factory.isConnected;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** NOTE: Only works if the nodeIs used is from SocketFunction.connect (we can't convert arbitrary nodeIds into urls,
|
|
169
|
-
* as we have no way of knowing how to contain a nodeId).
|
|
170
|
-
* */
|
|
171
|
-
public static getHTTPCallLink(call: FullCallType): string {
|
|
172
|
-
let location = getNodeIdLocation(call.nodeId);
|
|
173
|
-
if (!location) {
|
|
174
|
-
throw new Error(`Cannot find call location for nodeId, and so do not know where call location is. NodeId ${call.nodeId}`);
|
|
175
|
-
}
|
|
176
|
-
let url = new URL(`https://${location.address}:${location.port}`);
|
|
177
|
-
url.searchParams.set("classGuid", call.classGuid);
|
|
178
|
-
url.searchParams.set("functionName", call.functionName);
|
|
179
|
-
url.searchParams.set("args", JSON.stringify(call.args));
|
|
180
|
-
return url.toString();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Expose should be called before your mounting occurs. It mostly just exists to ensure you include the class type,
|
|
184
|
-
* so the class type's module construction runs, which should trigger register. Otherwise you would have
|
|
185
|
-
* to add additional imports to ensure the register call runs.
|
|
186
|
-
*/
|
|
187
|
-
public static expose(socketRegistered: SocketRegistered) {
|
|
188
|
-
exposeClass(socketRegistered);
|
|
189
|
-
SocketFunction.exposedClasses.add(socketRegistered._classGuid);
|
|
190
|
-
|
|
191
|
-
if (this.hasMounted) {
|
|
192
|
-
let mountCallbacks = SocketFunction.onMountCallbacks.get(socketRegistered._classGuid);
|
|
193
|
-
for (let onMount of mountCallbacks || []) {
|
|
194
|
-
Promise.resolve(onMount()).catch(e => {
|
|
195
|
-
console.error("Error in onMount callback exposed after mount", e);
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
public static mountedNodeId: string = "";
|
|
202
|
-
public static mountedIP: string = "";
|
|
203
|
-
private static hasMounted = false;
|
|
204
|
-
private static onMountCallback: () => void = () => { };
|
|
205
|
-
public static mountPromise: Promise<void> = new Promise(r => this.onMountCallback = r);
|
|
206
|
-
public static async mount(config: SocketServerConfig) {
|
|
207
|
-
if (this.mountedNodeId) {
|
|
208
|
-
throw new Error("SocketFunction already mounted, mounting twice in one thread is not allowed.");
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
this.mountedIP = config.public ? "0.0.0.0" : "127.0.0.1";
|
|
212
|
-
if (config.ip) {
|
|
213
|
-
this.mountedIP = config.ip;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Wait for any additionals functions to expose themselves
|
|
217
|
-
await delay("immediate");
|
|
218
|
-
|
|
219
|
-
this.mountedNodeId = await startSocketServer(config);
|
|
220
|
-
this.hasMounted = true;
|
|
221
|
-
for (let classGuid of SocketFunction.exposedClasses) {
|
|
222
|
-
let callbacks = SocketFunction.onMountCallbacks.get(classGuid);
|
|
223
|
-
if (!callbacks) continue;
|
|
224
|
-
for (let callback of callbacks) {
|
|
225
|
-
await callback();
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
this.onMountCallback();
|
|
229
|
-
return this.mountedNodeId;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/** Sets the default call when an http request is made, but no classGuid is set. */
|
|
233
|
-
public static setDefaultHTTPCall<
|
|
234
|
-
Registered extends SocketRegistered,
|
|
235
|
-
FunctionName extends keyof Registered["nodes"][""] & string,
|
|
236
|
-
>(
|
|
237
|
-
registered: Registered,
|
|
238
|
-
functionName: FunctionName,
|
|
239
|
-
...args: Args<Registered["nodes"][""][FunctionName]>
|
|
240
|
-
) {
|
|
241
|
-
setDefaultHTTPCall({
|
|
242
|
-
classGuid: registered._classGuid,
|
|
243
|
-
functionName,
|
|
244
|
-
args,
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
public static connect(location: { address: string, port: number }): string {
|
|
249
|
-
return getNodeId(location.address, location.port);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
public static locationNode() {
|
|
253
|
-
return SocketFunction.connect({ address: location.hostname, port: +location.port });
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
public static addGlobalHook(hook: SocketFunctionHook<SocketExposedInterface>) {
|
|
257
|
-
registerGlobalHook(hook as SocketFunctionHook);
|
|
258
|
-
}
|
|
259
|
-
public static addGlobalClientHook(hook: SocketFunctionClientHook<SocketExposedInterface>) {
|
|
260
|
-
registerGlobalClientHook(hook as SocketFunctionClientHook);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
let socketContextSeqNum = 1;
|
|
266
|
-
|
|
267
|
-
export function _setSocketContext<T>(
|
|
268
|
-
caller: CallerContext,
|
|
269
|
-
code: () => T,
|
|
270
|
-
) {
|
|
271
|
-
socketContextSeqNum++;
|
|
272
|
-
let seqNum = socketContextSeqNum;
|
|
273
|
-
SocketFunction.callerContext = caller;
|
|
274
|
-
try {
|
|
275
|
-
return code();
|
|
276
|
-
} finally {
|
|
277
|
-
if (seqNum === socketContextSeqNum) {
|
|
278
|
-
SocketFunction.callerContext = undefined;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
1
|
+
/// <reference path="./require/RequireController.ts" />
|
|
2
|
+
|
|
3
|
+
import { SocketExposedInterface, SocketFunctionHook, SocketFunctionClientHook, SocketExposedShape, SocketRegistered, CallerContext, FullCallType } from "./SocketFunctionTypes";
|
|
4
|
+
import { exposeClass, registerClass, registerGlobalClientHook, registerGlobalHook, runClientHooks } from "./src/callManager";
|
|
5
|
+
import { SocketServerConfig, startSocketServer } from "./src/webSocketServer";
|
|
6
|
+
import { getCallFactory, getCreateCallFactory, getNodeId, getNodeIdLocation } from "./src/nodeCache";
|
|
7
|
+
import { getCallProxy } from "./src/nodeProxy";
|
|
8
|
+
import { Args, MaybePromise } from "./src/types";
|
|
9
|
+
import { setDefaultHTTPCall } from "./src/callHTTPHandler";
|
|
10
|
+
import debugbreak from "debugbreak";
|
|
11
|
+
import { lazy } from "./src/caching";
|
|
12
|
+
import { delay } from "./src/batching";
|
|
13
|
+
|
|
14
|
+
module.allowclient = true;
|
|
15
|
+
|
|
16
|
+
type ExtractShape<ClassType, Shape> = {
|
|
17
|
+
[key in keyof Shape]: (
|
|
18
|
+
key extends keyof ClassType
|
|
19
|
+
? ClassType[key] extends SocketExposedInterface[""]
|
|
20
|
+
? ClassType[key]
|
|
21
|
+
: ClassType[key] extends Function ? "All exposed function must be async (or return a Promise)" : never
|
|
22
|
+
: "Function is in shape, but not in class"
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
// https://stackoverflow.com/a/69756175/1117119
|
|
26
|
+
type PickByType<T, Value> = {
|
|
27
|
+
[P in keyof T as T[P] extends Value | undefined ? P : never]: T[P]
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export class SocketFunction {
|
|
31
|
+
public static logMessages = false;
|
|
32
|
+
|
|
33
|
+
public static MAX_MESSAGE_SIZE = 1024 * 1024 * 32;
|
|
34
|
+
public static compression: undefined | {
|
|
35
|
+
type: "gzip";
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
public static httpETagCache = false;
|
|
39
|
+
public static silent = true;
|
|
40
|
+
|
|
41
|
+
public static WIRE_WARN_TIME = 100;
|
|
42
|
+
|
|
43
|
+
private static onMountCallbacks = new Map<string, (() => MaybePromise<void>)[]>();
|
|
44
|
+
public static exposedClasses = new Set<string>();
|
|
45
|
+
|
|
46
|
+
public static callerContext: CallerContext | undefined;
|
|
47
|
+
public static getCaller(): CallerContext {
|
|
48
|
+
const caller = SocketFunction.callerContext;
|
|
49
|
+
if (!caller) throw new Error(`Tried to access caller when not in the synchronous phase of a function call`);
|
|
50
|
+
return caller;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// NOTE: We use callbacks we don't run into issues with cyclic dependencies
|
|
54
|
+
// (ex, using a hook in a controller where the hook also calls the controller).
|
|
55
|
+
public static register<
|
|
56
|
+
ClassInstance extends object,
|
|
57
|
+
Shape extends SocketExposedShape<SocketExposedInterface>,
|
|
58
|
+
>(
|
|
59
|
+
classGuid: string,
|
|
60
|
+
instance: ClassInstance,
|
|
61
|
+
shapeFnc: () => Shape,
|
|
62
|
+
defaultHooksFnc?: () => SocketExposedShape[""] & {
|
|
63
|
+
onMount?: () => MaybePromise<void>;
|
|
64
|
+
}
|
|
65
|
+
): SocketRegistered<ExtractShape<ClassInstance, Shape>> {
|
|
66
|
+
let getDefaultHooks = defaultHooksFnc && lazy(defaultHooksFnc);
|
|
67
|
+
const getShape = lazy(() => {
|
|
68
|
+
let shape = shapeFnc();
|
|
69
|
+
let defaultHooks = getDefaultHooks?.();
|
|
70
|
+
|
|
71
|
+
for (let value of Object.values(shape)) {
|
|
72
|
+
if (!value) continue;
|
|
73
|
+
value.clientHooks = [...(defaultHooks?.clientHooks || []), ...(value.clientHooks || [])];
|
|
74
|
+
value.hooks = [...(defaultHooks?.hooks || []), ...(value.hooks || [])];
|
|
75
|
+
value.dataImmutable = defaultHooks?.dataImmutable ?? value.dataImmutable;
|
|
76
|
+
}
|
|
77
|
+
return shape as any as SocketExposedShape;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
void Promise.resolve().then(() => {
|
|
81
|
+
registerClass(classGuid, instance as SocketExposedInterface, getShape());
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
let nodeProxy = getCallProxy(classGuid, async (call) => {
|
|
85
|
+
let nodeId = call.nodeId;
|
|
86
|
+
let functionName = call.functionName;
|
|
87
|
+
let time = Date.now();
|
|
88
|
+
if (SocketFunction.logMessages) {
|
|
89
|
+
console.log(`START\t\t\t${classGuid}.${functionName} at ${Date.now()}`);
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
let callFactory = await getCreateCallFactory(nodeId);
|
|
93
|
+
|
|
94
|
+
let shapeObj = getShape()[functionName];
|
|
95
|
+
if (!shapeObj) {
|
|
96
|
+
throw new Error(`Function ${functionName} is not in shape`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let hookResult = await runClientHooks(call, shapeObj as SocketExposedShape[""], callFactory.connectionId);
|
|
100
|
+
|
|
101
|
+
if ("overrideResult" in hookResult) {
|
|
102
|
+
return hookResult.overrideResult;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return await callFactory.performCall(call);
|
|
106
|
+
} finally {
|
|
107
|
+
time = Date.now() - time;
|
|
108
|
+
if (SocketFunction.logMessages) {
|
|
109
|
+
console.log(`FINISHED\t${time}ms\t${classGuid}.${functionName} at ${Date.now()}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
let output: SocketRegistered = {
|
|
115
|
+
nodes: nodeProxy,
|
|
116
|
+
_classGuid: classGuid,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
void Promise.resolve().then(() => {
|
|
120
|
+
let onMount = getDefaultHooks?.().onMount;
|
|
121
|
+
if (onMount) {
|
|
122
|
+
let callbacks = SocketFunction.onMountCallbacks.get(classGuid);
|
|
123
|
+
if (!callbacks) {
|
|
124
|
+
callbacks = [];
|
|
125
|
+
SocketFunction.onMountCallbacks.set(classGuid, callbacks);
|
|
126
|
+
}
|
|
127
|
+
callbacks.push(onMount);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return output as any;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public static onNextDisconnect(nodeId: string, callback: () => void) {
|
|
135
|
+
(async () => {
|
|
136
|
+
let factory = await getCallFactory(nodeId);
|
|
137
|
+
if (!factory) {
|
|
138
|
+
callback();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
factory.onNextDisconnect(callback);
|
|
143
|
+
})().catch(() => {
|
|
144
|
+
callback();
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
public static getLastDisconnectTime(nodeId: string): number | undefined {
|
|
148
|
+
let factory = getCallFactory(nodeId);
|
|
149
|
+
if (!factory) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
if (factory instanceof Promise) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
return factory.lastClosed;
|
|
156
|
+
}
|
|
157
|
+
public static isNodeConnected(nodeId: string): boolean {
|
|
158
|
+
let factory = getCallFactory(nodeId);
|
|
159
|
+
if (!factory) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
if (factory instanceof Promise) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
return !!factory.isConnected;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** NOTE: Only works if the nodeIs used is from SocketFunction.connect (we can't convert arbitrary nodeIds into urls,
|
|
169
|
+
* as we have no way of knowing how to contain a nodeId).
|
|
170
|
+
* */
|
|
171
|
+
public static getHTTPCallLink(call: FullCallType): string {
|
|
172
|
+
let location = getNodeIdLocation(call.nodeId);
|
|
173
|
+
if (!location) {
|
|
174
|
+
throw new Error(`Cannot find call location for nodeId, and so do not know where call location is. NodeId ${call.nodeId}`);
|
|
175
|
+
}
|
|
176
|
+
let url = new URL(`https://${location.address}:${location.port}`);
|
|
177
|
+
url.searchParams.set("classGuid", call.classGuid);
|
|
178
|
+
url.searchParams.set("functionName", call.functionName);
|
|
179
|
+
url.searchParams.set("args", JSON.stringify(call.args));
|
|
180
|
+
return url.toString();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Expose should be called before your mounting occurs. It mostly just exists to ensure you include the class type,
|
|
184
|
+
* so the class type's module construction runs, which should trigger register. Otherwise you would have
|
|
185
|
+
* to add additional imports to ensure the register call runs.
|
|
186
|
+
*/
|
|
187
|
+
public static expose(socketRegistered: SocketRegistered) {
|
|
188
|
+
exposeClass(socketRegistered);
|
|
189
|
+
SocketFunction.exposedClasses.add(socketRegistered._classGuid);
|
|
190
|
+
|
|
191
|
+
if (this.hasMounted) {
|
|
192
|
+
let mountCallbacks = SocketFunction.onMountCallbacks.get(socketRegistered._classGuid);
|
|
193
|
+
for (let onMount of mountCallbacks || []) {
|
|
194
|
+
Promise.resolve(onMount()).catch(e => {
|
|
195
|
+
console.error("Error in onMount callback exposed after mount", e);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
public static mountedNodeId: string = "";
|
|
202
|
+
public static mountedIP: string = "";
|
|
203
|
+
private static hasMounted = false;
|
|
204
|
+
private static onMountCallback: () => void = () => { };
|
|
205
|
+
public static mountPromise: Promise<void> = new Promise(r => this.onMountCallback = r);
|
|
206
|
+
public static async mount(config: SocketServerConfig) {
|
|
207
|
+
if (this.mountedNodeId) {
|
|
208
|
+
throw new Error("SocketFunction already mounted, mounting twice in one thread is not allowed.");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
this.mountedIP = config.public ? "0.0.0.0" : "127.0.0.1";
|
|
212
|
+
if (config.ip) {
|
|
213
|
+
this.mountedIP = config.ip;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Wait for any additionals functions to expose themselves
|
|
217
|
+
await delay("immediate");
|
|
218
|
+
|
|
219
|
+
this.mountedNodeId = await startSocketServer(config);
|
|
220
|
+
this.hasMounted = true;
|
|
221
|
+
for (let classGuid of SocketFunction.exposedClasses) {
|
|
222
|
+
let callbacks = SocketFunction.onMountCallbacks.get(classGuid);
|
|
223
|
+
if (!callbacks) continue;
|
|
224
|
+
for (let callback of callbacks) {
|
|
225
|
+
await callback();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
this.onMountCallback();
|
|
229
|
+
return this.mountedNodeId;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Sets the default call when an http request is made, but no classGuid is set. */
|
|
233
|
+
public static setDefaultHTTPCall<
|
|
234
|
+
Registered extends SocketRegistered,
|
|
235
|
+
FunctionName extends keyof Registered["nodes"][""] & string,
|
|
236
|
+
>(
|
|
237
|
+
registered: Registered,
|
|
238
|
+
functionName: FunctionName,
|
|
239
|
+
...args: Args<Registered["nodes"][""][FunctionName]>
|
|
240
|
+
) {
|
|
241
|
+
setDefaultHTTPCall({
|
|
242
|
+
classGuid: registered._classGuid,
|
|
243
|
+
functionName,
|
|
244
|
+
args,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
public static connect(location: { address: string, port: number }): string {
|
|
249
|
+
return getNodeId(location.address, location.port);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
public static locationNode() {
|
|
253
|
+
return SocketFunction.connect({ address: location.hostname, port: +location.port });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
public static addGlobalHook(hook: SocketFunctionHook<SocketExposedInterface>) {
|
|
257
|
+
registerGlobalHook(hook as SocketFunctionHook);
|
|
258
|
+
}
|
|
259
|
+
public static addGlobalClientHook(hook: SocketFunctionClientHook<SocketExposedInterface>) {
|
|
260
|
+
registerGlobalClientHook(hook as SocketFunctionClientHook);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
let socketContextSeqNum = 1;
|
|
266
|
+
|
|
267
|
+
export function _setSocketContext<T>(
|
|
268
|
+
caller: CallerContext,
|
|
269
|
+
code: () => T,
|
|
270
|
+
) {
|
|
271
|
+
socketContextSeqNum++;
|
|
272
|
+
let seqNum = socketContextSeqNum;
|
|
273
|
+
SocketFunction.callerContext = caller;
|
|
274
|
+
try {
|
|
275
|
+
return code();
|
|
276
|
+
} finally {
|
|
277
|
+
if (seqNum === socketContextSeqNum) {
|
|
278
|
+
SocketFunction.callerContext = undefined;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
281
|
}
|