socket-function 0.8.35 → 0.8.36
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 +65 -32
- package/SocketFunctionTypes.ts +4 -7
- package/hot/HotReloadController.ts +2 -2
- package/package.json +5 -6
- package/require/CSSShim.ts +1 -0
- package/require/RequireController.ts +4 -3
- package/require/compileFlags.ts +1 -0
- package/src/CallFactory.ts +60 -130
- package/src/caching.ts +4 -0
- package/src/callManager.ts +13 -1
- package/src/misc.ts +3 -0
- package/src/nodeCache.ts +12 -2
- package/src/webSocketServer.ts +69 -14
- package/src/{nodeAuthentication.ts → websocketFactory.ts} +1 -0
- package/test/client.ts +2 -2
- package/test/shared.ts +3 -9
package/SocketFunction.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
/// <reference path="./require/RequireController.ts" />
|
|
2
|
+
|
|
1
3
|
import { SocketExposedInterface, CallContextType, SocketFunctionHook, SocketFunctionClientHook, SocketExposedShape, SocketRegistered, CallerContext, FullCallType } from "./SocketFunctionTypes";
|
|
2
4
|
import { exposeClass, registerClass, registerGlobalClientHook, registerGlobalHook, runClientHooks } from "./src/callManager";
|
|
3
5
|
import { SocketServerConfig, startSocketServer } from "./src/webSocketServer";
|
|
4
6
|
import { getCreateCallFactoryLocation, getNodeId, getNodeIdLocation } from "./src/nodeCache";
|
|
5
7
|
import { getCallProxy } from "./src/nodeProxy";
|
|
6
|
-
import { Args } from "./src/types";
|
|
8
|
+
import { Args, MaybePromise } from "./src/types";
|
|
7
9
|
import { setDefaultHTTPCall } from "./src/callHTTPHandler";
|
|
10
|
+
import debugbreak from "debugbreak";
|
|
11
|
+
import { lazy } from "./src/caching";
|
|
8
12
|
|
|
9
13
|
module.allowclient = true;
|
|
10
14
|
|
|
@@ -29,6 +33,11 @@ export class SocketFunction {
|
|
|
29
33
|
};
|
|
30
34
|
public static httpETagCache = false;
|
|
31
35
|
|
|
36
|
+
private static onMountCallbacks = new Map<string, (() => MaybePromise<void>)[]>();
|
|
37
|
+
public static exposedClasses = new Set<string>();
|
|
38
|
+
|
|
39
|
+
// NOTE: We use callbacks we don't run into issues with cyclic dependencies
|
|
40
|
+
// (ex, using a hook in a controller where the hook also calls the controller).
|
|
32
41
|
public static register<
|
|
33
42
|
ClassInstance extends object,
|
|
34
43
|
Shape extends SocketExposedShape<SocketExposedInterface, CallContext>,
|
|
@@ -36,20 +45,28 @@ export class SocketFunction {
|
|
|
36
45
|
>(
|
|
37
46
|
classGuid: string,
|
|
38
47
|
instance: ClassInstance,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
(
|
|
43
|
-
SocketRegistered<ExtractShape<ClassInstance, Shape>, CallContext>
|
|
44
|
-
) {
|
|
45
|
-
|
|
46
|
-
for (let value of Object.values(shape)) {
|
|
47
|
-
if (!value) continue;
|
|
48
|
-
value.clientHooks = [...(defaultHooks?.clientHooks || []), ...(value.clientHooks || [])];
|
|
49
|
-
value.hooks = [...(defaultHooks?.hooks || []), ...(value.hooks || [])];
|
|
50
|
-
value.dataImmutable = defaultHooks?.dataImmutable ?? value.dataImmutable;
|
|
48
|
+
shapeFnc: () => Shape,
|
|
49
|
+
defaultHooksFnc?: () => SocketExposedShape[""] & {
|
|
50
|
+
onMount?: () => MaybePromise<void>;
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
): SocketRegistered<ExtractShape<ClassInstance, Shape>, CallContext> {
|
|
53
|
+
let getDefaultHooks = defaultHooksFnc && lazy(defaultHooksFnc);
|
|
54
|
+
const getShape = lazy(() => {
|
|
55
|
+
let shape = shapeFnc();
|
|
56
|
+
let defaultHooks = getDefaultHooks?.();
|
|
57
|
+
|
|
58
|
+
for (let value of Object.values(shape)) {
|
|
59
|
+
if (!value) continue;
|
|
60
|
+
value.clientHooks = [...(defaultHooks?.clientHooks || []), ...(value.clientHooks || [])];
|
|
61
|
+
value.hooks = [...(defaultHooks?.hooks || []), ...(value.hooks || [])];
|
|
62
|
+
value.dataImmutable = defaultHooks?.dataImmutable ?? value.dataImmutable;
|
|
63
|
+
}
|
|
64
|
+
return shape as any as SocketExposedShape;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
setImmediate(() => {
|
|
68
|
+
registerClass(classGuid, instance as SocketExposedInterface, getShape());
|
|
69
|
+
});
|
|
53
70
|
|
|
54
71
|
let nodeProxy = getCallProxy(classGuid, async (call) => {
|
|
55
72
|
let nodeId = call.nodeId;
|
|
@@ -61,7 +78,7 @@ export class SocketFunction {
|
|
|
61
78
|
try {
|
|
62
79
|
let callFactory = await getCreateCallFactoryLocation(nodeId, SocketFunction.mountedNodeId);
|
|
63
80
|
|
|
64
|
-
let shapeObj =
|
|
81
|
+
let shapeObj = getShape()[functionName];
|
|
65
82
|
if (!shapeObj) {
|
|
66
83
|
throw new Error(`Function ${functionName} is not in shape`);
|
|
67
84
|
}
|
|
@@ -72,20 +89,6 @@ export class SocketFunction {
|
|
|
72
89
|
return hookResult.overrideResult;
|
|
73
90
|
}
|
|
74
91
|
|
|
75
|
-
if (hookResult.callTimeout !== undefined) {
|
|
76
|
-
let timeout = hookResult.callTimeout;
|
|
77
|
-
let time = Date.now();
|
|
78
|
-
let timeoutPromise = new Promise((resolve, reject) => {
|
|
79
|
-
setTimeout(() => {
|
|
80
|
-
reject(new Error(`Call timed out after ${Date.now() - time}ms`));
|
|
81
|
-
}, timeout);
|
|
82
|
-
});
|
|
83
|
-
return await Promise.race([
|
|
84
|
-
callFactory.performCall(call),
|
|
85
|
-
timeoutPromise,
|
|
86
|
-
]);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
92
|
return await callFactory.performCall(call);
|
|
90
93
|
} finally {
|
|
91
94
|
time = Date.now() - time;
|
|
@@ -101,6 +104,18 @@ export class SocketFunction {
|
|
|
101
104
|
_classGuid: classGuid,
|
|
102
105
|
};
|
|
103
106
|
|
|
107
|
+
setImmediate(() => {
|
|
108
|
+
let onMount = getDefaultHooks?.().onMount;
|
|
109
|
+
if (onMount) {
|
|
110
|
+
let callbacks = SocketFunction.onMountCallbacks.get(classGuid);
|
|
111
|
+
if (!callbacks) {
|
|
112
|
+
callbacks = [];
|
|
113
|
+
SocketFunction.onMountCallbacks.set(classGuid, callbacks);
|
|
114
|
+
}
|
|
115
|
+
callbacks.push(onMount);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
104
119
|
return output as any;
|
|
105
120
|
}
|
|
106
121
|
|
|
@@ -125,15 +140,33 @@ export class SocketFunction {
|
|
|
125
140
|
*/
|
|
126
141
|
public static expose(socketRegistered: SocketRegistered) {
|
|
127
142
|
exposeClass(socketRegistered);
|
|
143
|
+
SocketFunction.exposedClasses.add(socketRegistered._classGuid);
|
|
144
|
+
|
|
145
|
+
if (this.hasMounted) {
|
|
146
|
+
let mountCallbacks = SocketFunction.onMountCallbacks.get(socketRegistered._classGuid);
|
|
147
|
+
for (let onMount of mountCallbacks || []) {
|
|
148
|
+
Promise.resolve(onMount()).catch(e => {
|
|
149
|
+
console.error("Error in onMount callback exposed after mount", e);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
128
153
|
}
|
|
129
154
|
|
|
130
155
|
public static mountedNodeId: string = "NOTMOUNTED";
|
|
131
|
-
|
|
156
|
+
private static hasMounted = false;
|
|
157
|
+
public static async mount(config: SocketServerConfig) {
|
|
132
158
|
if (this.mountedNodeId !== "NOTMOUNTED") {
|
|
133
159
|
throw new Error("SocketFunction already mounted, mounting twice in one thread is not allowed.");
|
|
134
160
|
}
|
|
135
|
-
this.mountedNodeId = config
|
|
136
|
-
|
|
161
|
+
this.mountedNodeId = await startSocketServer(config);
|
|
162
|
+
this.hasMounted = true;
|
|
163
|
+
for (let classGuid of SocketFunction.exposedClasses) {
|
|
164
|
+
let callbacks = SocketFunction.onMountCallbacks.get(classGuid);
|
|
165
|
+
if (!callbacks) continue;
|
|
166
|
+
for (let callback of callbacks) {
|
|
167
|
+
await callback();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
137
170
|
return this.mountedNodeId;
|
|
138
171
|
}
|
|
139
172
|
|
package/SocketFunctionTypes.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/// <reference path="./require/RequireController.ts" />
|
|
2
|
+
|
|
1
3
|
module.allowclient = true;
|
|
2
4
|
|
|
3
5
|
import { getCallObj } from "./src/nodeProxy";
|
|
@@ -34,9 +36,6 @@ export interface CallType {
|
|
|
34
36
|
classGuid: string;
|
|
35
37
|
functionName: string;
|
|
36
38
|
args: unknown[];
|
|
37
|
-
// NOTE: When making calls this needs to be set in the client hook.
|
|
38
|
-
// To set a timeout on returns, you can set it in the server hook.
|
|
39
|
-
reconnectTimeout?: number;
|
|
40
39
|
}
|
|
41
40
|
export interface FullCallType extends CallType {
|
|
42
41
|
nodeId: string;
|
|
@@ -44,6 +43,8 @@ export interface FullCallType extends CallType {
|
|
|
44
43
|
|
|
45
44
|
export interface SocketFunctionHook<ExposedType extends SocketExposedInterface = SocketExposedInterface, CallContext extends CallContextType = CallContextType> {
|
|
46
45
|
(config: HookContext<ExposedType, CallContext>): MaybePromise<void>;
|
|
46
|
+
/** NOTE: This is useful when we need a clientside hook to set up state specifically for our serverside hook. */
|
|
47
|
+
clientHook?: SocketFunctionClientHook<ExposedType, CallContext>;
|
|
47
48
|
}
|
|
48
49
|
export type HookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface, CallContext extends CallContextType = CallContextType> = {
|
|
49
50
|
call: FullCallType;
|
|
@@ -54,10 +55,6 @@ export type HookContext<ExposedType extends SocketExposedInterface = SocketExpos
|
|
|
54
55
|
|
|
55
56
|
export type ClientHookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface, CallContext extends CallContextType = CallContextType> = {
|
|
56
57
|
call: FullCallType;
|
|
57
|
-
/** If the calls takes longer than this (for ANY reason), we return with an error.
|
|
58
|
-
* - Different from reconnectTimeout, which only errors if we lose the connection.
|
|
59
|
-
*/
|
|
60
|
-
callTimeout?: number;
|
|
61
58
|
// If the result is overriden, we continue evaluating hooks BUT DO NOT perform the final call
|
|
62
59
|
overrideResult?: unknown;
|
|
63
60
|
};
|
|
@@ -64,8 +64,8 @@ class HotReloadControllerBase {
|
|
|
64
64
|
export const HotReloadController = SocketFunction.register(
|
|
65
65
|
"HotReloadController-032b2250-3aac-4187-8c95-75412742b8f5",
|
|
66
66
|
new HotReloadControllerBase(),
|
|
67
|
-
{
|
|
67
|
+
() => ({
|
|
68
68
|
watchFiles: {},
|
|
69
69
|
fileUpdated: {}
|
|
70
|
-
}
|
|
70
|
+
})
|
|
71
71
|
);
|
package/package.json
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "socket-function",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.36",
|
|
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/cookie": "^0.5.1",
|
|
9
|
-
"@types/node": "^18.0.0",
|
|
10
|
-
"@types/node-forge": "^1.3.1",
|
|
11
|
-
"@types/ws": "^8.5.3",
|
|
12
8
|
"cookie": "^0.5.0",
|
|
13
9
|
"mobx": "^6.6.2",
|
|
14
10
|
"node-forge": "https://github.com/sliftist/forge#name",
|
|
@@ -21,7 +17,10 @@
|
|
|
21
17
|
"type": "yarn tsc --noEmit"
|
|
22
18
|
},
|
|
23
19
|
"devDependencies": {
|
|
20
|
+
"@types/cookie": "^0.5.1",
|
|
21
|
+
"@types/node-forge": "^1.3.1",
|
|
22
|
+
"@types/ws": "^8.5.3",
|
|
24
23
|
"debugbreak": "^0.6.5",
|
|
25
|
-
"typedev": "^0.1.
|
|
24
|
+
"typedev": "^0.1.1"
|
|
26
25
|
}
|
|
27
26
|
}
|
package/require/CSSShim.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference path="../../typenode/index.d.ts" />
|
|
1
2
|
import debugbreak from "debugbreak";
|
|
2
3
|
import fs from "fs";
|
|
3
4
|
import { SocketFunction } from "../SocketFunction";
|
|
@@ -9,7 +10,7 @@ module.allowclient = true;
|
|
|
9
10
|
declare global {
|
|
10
11
|
namespace NodeJS {
|
|
11
12
|
interface Module {
|
|
12
|
-
/**
|
|
13
|
+
/** Indicates the module is allowed clientside. */
|
|
13
14
|
allowclient?: boolean;
|
|
14
15
|
|
|
15
16
|
/** Indicates the module is definitely not allowed clientside */
|
|
@@ -237,10 +238,10 @@ export function setRequireBootRequire(path: string) {
|
|
|
237
238
|
export const RequireController = SocketFunction.register(
|
|
238
239
|
"RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d",
|
|
239
240
|
baseController,
|
|
240
|
-
{
|
|
241
|
+
() => ({
|
|
241
242
|
getModules: {},
|
|
242
243
|
requireHTML: {},
|
|
243
244
|
bufferJS: {},
|
|
244
245
|
requireJS: {},
|
|
245
|
-
}
|
|
246
|
+
})
|
|
246
247
|
);
|
package/require/compileFlags.ts
CHANGED
package/src/CallFactory.ts
CHANGED
|
@@ -2,13 +2,13 @@ import { CallerContext, CallerContextBase, CallType, FullCallType } from "../Soc
|
|
|
2
2
|
import * as ws from "ws";
|
|
3
3
|
import { performLocalCall } from "./callManager";
|
|
4
4
|
import { convertErrorStackToError, formatNumberSuffixed, isNode } from "./misc";
|
|
5
|
-
import { createWebsocketFactory, getTLSSocket } from "./
|
|
5
|
+
import { createWebsocketFactory, getTLSSocket } from "./websocketFactory";
|
|
6
6
|
import { SocketFunction } from "../SocketFunction";
|
|
7
7
|
import { gzip } from "zlib";
|
|
8
8
|
import * as tls from "tls";
|
|
9
9
|
import { getClientNodeId, getNodeIdLocation, registerNodeClient } from "./nodeCache";
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import debugbreak from "debugbreak";
|
|
11
|
+
import { lazy } from "./caching";
|
|
12
12
|
|
|
13
13
|
type InternalCallType = FullCallType & {
|
|
14
14
|
seqNum: number;
|
|
@@ -45,6 +45,8 @@ export interface SenderInterface {
|
|
|
45
45
|
addEventListener(event: "close", listener: () => void): void;
|
|
46
46
|
addEventListener(event: "error", listener: (err: { message: string }) => void): void;
|
|
47
47
|
addEventListener(event: "message", listener: (data: ws.RawData | ws.MessageEvent | string) => void): void;
|
|
48
|
+
|
|
49
|
+
readyState: number;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export async function createCallFactory(
|
|
@@ -55,19 +57,13 @@ export async function createCallFactory(
|
|
|
55
57
|
let niceConnectionName = nodeId;
|
|
56
58
|
|
|
57
59
|
const createWebsocket = createWebsocketFactory();
|
|
58
|
-
|
|
59
|
-
let retriesEnabled = !!getNodeIdLocation(nodeId);
|
|
60
|
+
const registerOnce = lazy(() => registerNodeClient(callFactory));
|
|
60
61
|
|
|
61
62
|
let lastReceivedSeqNum = 0;
|
|
62
63
|
|
|
63
|
-
let reconnectingPromise: Promise<void> | undefined;
|
|
64
|
-
let reconnectAttempts = 0;
|
|
65
|
-
|
|
66
|
-
|
|
67
64
|
let pendingCalls: Map<number, {
|
|
68
65
|
data: Buffer;
|
|
69
66
|
call: InternalCallType;
|
|
70
|
-
reconnectTimeout: number | undefined;
|
|
71
67
|
callback: (resultJSON: InternalReturnType) => void;
|
|
72
68
|
}> = new Map();
|
|
73
69
|
// NOTE: It is important to make this as random as possible, to prevent
|
|
@@ -111,144 +107,78 @@ export async function createCallFactory(
|
|
|
111
107
|
resolve(result.result);
|
|
112
108
|
}
|
|
113
109
|
};
|
|
114
|
-
pendingCalls.set(seqNum, { callback, data, call: fullCall
|
|
110
|
+
pendingCalls.set(seqNum, { callback, data, call: fullCall });
|
|
115
111
|
});
|
|
116
112
|
|
|
117
|
-
await
|
|
113
|
+
await send(data);
|
|
118
114
|
|
|
119
115
|
return await resultPromise;
|
|
120
116
|
}
|
|
121
117
|
};
|
|
122
118
|
|
|
123
|
-
let
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
webSocket = webSocketBase;
|
|
128
|
-
setupWebsocket(webSocketBase);
|
|
119
|
+
let webSocketPromise: Promise<SenderInterface> | undefined;
|
|
120
|
+
if (webSocketBase) {
|
|
121
|
+
webSocketPromise = Promise.resolve(webSocketBase);
|
|
122
|
+
await initializeWebsocket(webSocketBase);
|
|
129
123
|
}
|
|
130
124
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
retriesEnabled = false;
|
|
147
|
-
resolve(webSocket);
|
|
148
|
-
}, reconnectTimeout)
|
|
149
|
-
)
|
|
150
|
-
]);
|
|
151
|
-
} else {
|
|
152
|
-
await reconnectingPromise;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (!retriesEnabled) {
|
|
157
|
-
webSocket.send(data);
|
|
158
|
-
break;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
webSocket.send(data);
|
|
163
|
-
break;
|
|
164
|
-
} catch (e) {
|
|
165
|
-
// Ignore errors, as we will catch them synchronously in the next loop.
|
|
166
|
-
void (tryToReconnect());
|
|
125
|
+
async function initializeWebsocket(newWebSocket: SenderInterface) {
|
|
126
|
+
registerOnce();
|
|
127
|
+
|
|
128
|
+
function onClose(error: string) {
|
|
129
|
+
webSocketPromise = undefined;
|
|
130
|
+
for (let [key, call] of pendingCalls) {
|
|
131
|
+
pendingCalls.delete(key);
|
|
132
|
+
call.callback({
|
|
133
|
+
isReturn: true,
|
|
134
|
+
result: undefined,
|
|
135
|
+
error: error,
|
|
136
|
+
seqNum: call.call.seqNum,
|
|
137
|
+
resultSize: 0,
|
|
138
|
+
compressed: false,
|
|
139
|
+
});
|
|
167
140
|
}
|
|
168
141
|
}
|
|
169
|
-
}
|
|
170
|
-
function tryToReconnect(): Promise<void> {
|
|
171
|
-
if (reconnectingPromise) return reconnectingPromise;
|
|
172
|
-
return reconnectingPromise = (async () => {
|
|
173
|
-
while (true) {
|
|
174
|
-
if (!retriesEnabled) {
|
|
175
|
-
callFactory.closedForever = true;
|
|
176
|
-
console.log(`Cannot reconnect to ${niceConnectionName}, aborting pendingCalls: ${pendingCalls.size}`);
|
|
177
|
-
for (let call of pendingCalls.values()) {
|
|
178
|
-
call.callback({
|
|
179
|
-
isReturn: true,
|
|
180
|
-
result: undefined,
|
|
181
|
-
error: `Connection lost to ${niceConnectionName}`,
|
|
182
|
-
seqNum: call.call.seqNum,
|
|
183
|
-
resultSize: 0,
|
|
184
|
-
compressed: false,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
let newWebSocket = createWebsocket(nodeId);
|
|
191
|
-
|
|
192
|
-
let connectError = await new Promise<string | undefined>(resolve => {
|
|
193
|
-
newWebSocket.addEventListener("open", () => {
|
|
194
|
-
resolve(undefined);
|
|
195
|
-
});
|
|
196
|
-
newWebSocket.addEventListener("close", () => {
|
|
197
|
-
resolve("Connection closed for non-error reason?");
|
|
198
|
-
});
|
|
199
|
-
newWebSocket.addEventListener("error", e => {
|
|
200
|
-
resolve(String(e.message));
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
setupWebsocket(newWebSocket);
|
|
205
142
|
|
|
206
|
-
|
|
207
|
-
|
|
143
|
+
newWebSocket.addEventListener("error", e => {
|
|
144
|
+
// NOTE: No more logging, as we throw, so the caller should be logging the
|
|
145
|
+
// error (or swallowing it, if that is what it wants to do).
|
|
146
|
+
//console.log(`Websocket error for ${niceConnectionName}`, e.message);
|
|
147
|
+
onClose(`Connection error for ${niceConnectionName}: ${e.message}`);
|
|
148
|
+
});
|
|
208
149
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
150
|
+
newWebSocket.addEventListener("close", async () => {
|
|
151
|
+
//console.log(`Websocket closed ${niceConnectionName}`);
|
|
152
|
+
onClose(`Connection closed to ${niceConnectionName}`);
|
|
153
|
+
});
|
|
212
154
|
|
|
213
|
-
|
|
155
|
+
newWebSocket.addEventListener("message", onMessage);
|
|
214
156
|
|
|
215
|
-
for (let call of pendingCalls.values()) {
|
|
216
|
-
sendWithRetry(call.reconnectTimeout, call.data).catch(e => {
|
|
217
|
-
call.callback({
|
|
218
|
-
isReturn: true,
|
|
219
|
-
result: undefined,
|
|
220
|
-
error: String(e),
|
|
221
|
-
seqNum: call.call.seqNum,
|
|
222
|
-
resultSize: 0,
|
|
223
|
-
compressed: false,
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
157
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
158
|
+
if (newWebSocket.readyState === 0 /* CONNECTING */) {
|
|
159
|
+
await new Promise<void>(resolve => {
|
|
160
|
+
newWebSocket.addEventListener("open", () => {
|
|
161
|
+
console.log(`Connection established to ${niceConnectionName}`);
|
|
162
|
+
resolve();
|
|
163
|
+
});
|
|
164
|
+
newWebSocket.addEventListener("close", () => resolve());
|
|
165
|
+
newWebSocket.addEventListener("error", () => resolve());
|
|
166
|
+
});
|
|
167
|
+
} else if (newWebSocket.readyState !== 1 /* OPEN */) {
|
|
168
|
+
onClose(`Websocket received in closed state`);
|
|
169
|
+
}
|
|
235
170
|
}
|
|
236
171
|
|
|
237
|
-
function
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
webSocket.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
console.log(`Websocket closed ${niceConnectionName}`);
|
|
246
|
-
if (retriesEnabled) {
|
|
247
|
-
await tryToReconnect();
|
|
248
|
-
}
|
|
249
|
-
});
|
|
172
|
+
async function send(data: Buffer) {
|
|
173
|
+
webSocketPromise = webSocketPromise || tryToReconnect();
|
|
174
|
+
let webSocket = await webSocketPromise;
|
|
175
|
+
webSocket.send(data);
|
|
176
|
+
}
|
|
177
|
+
async function tryToReconnect(): Promise<SenderInterface> {
|
|
178
|
+
let newWebSocket = createWebsocket(nodeId);
|
|
179
|
+
await initializeWebsocket(newWebSocket);
|
|
250
180
|
|
|
251
|
-
|
|
181
|
+
return newWebSocket;
|
|
252
182
|
}
|
|
253
183
|
|
|
254
184
|
|
|
@@ -328,7 +258,7 @@ export async function createCallFactory(
|
|
|
328
258
|
} else {
|
|
329
259
|
result = Buffer.from(JSON.stringify(response));
|
|
330
260
|
}
|
|
331
|
-
await
|
|
261
|
+
await send(result);
|
|
332
262
|
}
|
|
333
263
|
return;
|
|
334
264
|
}
|
package/src/caching.ts
CHANGED
|
@@ -33,6 +33,7 @@ export function cacheEmptyArray<T>(array: T[]): T[] {
|
|
|
33
33
|
|
|
34
34
|
export function cache<Output, Key>(getValue: (key: Key) => Output): {
|
|
35
35
|
(key: Key): Output;
|
|
36
|
+
clear(key: Key): void;
|
|
36
37
|
} {
|
|
37
38
|
let startingCalculating = new Set<Key>();
|
|
38
39
|
let values = new Map<Key, Output>();
|
|
@@ -51,6 +52,9 @@ export function cache<Output, Key>(getValue: (key: Key) => Output): {
|
|
|
51
52
|
values.set(key, value);
|
|
52
53
|
return value;
|
|
53
54
|
}
|
|
55
|
+
cache.clear = (key: Key) => {
|
|
56
|
+
values.delete(key);
|
|
57
|
+
};
|
|
54
58
|
return cache;
|
|
55
59
|
}
|
|
56
60
|
|
package/src/callManager.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { CallContextType, CallerContext, CallType, ClientHookContext, FullCallType, HookContext, SocketExposedInterface, SocketExposedInterfaceClass, SocketExposedShape, SocketFunctionClientHook, SocketFunctionHook, SocketRegistered } from "../SocketFunctionTypes";
|
|
2
2
|
import { _setSocketContext } from "../SocketFunction";
|
|
3
|
+
import { isNode } from "./misc";
|
|
4
|
+
import debugbreak from "debugbreak";
|
|
3
5
|
|
|
4
6
|
let classes: {
|
|
5
7
|
[classGuid: string]: {
|
|
@@ -96,7 +98,17 @@ export async function runClientHooks(
|
|
|
96
98
|
hooks: SocketExposedShape[""],
|
|
97
99
|
): Promise<ClientHookContext> {
|
|
98
100
|
let context: ClientHookContext = { call: callType };
|
|
99
|
-
|
|
101
|
+
|
|
102
|
+
let clientHooks = (
|
|
103
|
+
globalClientHooks
|
|
104
|
+
.concat(hooks.clientHooks || [])
|
|
105
|
+
);
|
|
106
|
+
for (let otherClientHook of globalHooks.concat(hooks.hooks || []).map(x => x.clientHook)) {
|
|
107
|
+
if (otherClientHook) {
|
|
108
|
+
clientHooks.push(otherClientHook);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (let hook of clientHooks) {
|
|
100
112
|
await hook(context);
|
|
101
113
|
if ("overrideResult" in context) {
|
|
102
114
|
break;
|
package/src/misc.ts
CHANGED
package/src/nodeCache.ts
CHANGED
|
@@ -19,11 +19,21 @@ export function getNodeId(domain: string, port: number): string {
|
|
|
19
19
|
|
|
20
20
|
/** A nodeId not available for reconnecting. */
|
|
21
21
|
export function getClientNodeId(address: string): string {
|
|
22
|
-
return `
|
|
22
|
+
return `client:${address}:${Date.now()}:${Math.random()}`;
|
|
23
|
+
}
|
|
24
|
+
/** Will always be available, even if getNodeIdLocation is not (as we don't always have the port,
|
|
25
|
+
* but we should always have an address).
|
|
26
|
+
* - Rarely used, as for logging you can just log the nodeId. ALSO, it isn't sufficient to reconnect, as the port is also needed!
|
|
27
|
+
* */
|
|
28
|
+
export function getNodeIdIP(nodeId: string): string {
|
|
29
|
+
if (nodeId.startsWith("client:")) {
|
|
30
|
+
return nodeId.split(":")[1];
|
|
31
|
+
}
|
|
32
|
+
return getNodeIdLocation(nodeId)!.address;
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
export function getNodeIdLocation(nodeId: string): { address: string, port: number; } | undefined {
|
|
26
|
-
if (nodeId.startsWith("
|
|
36
|
+
if (nodeId.startsWith("client:")) {
|
|
27
37
|
return undefined;
|
|
28
38
|
}
|
|
29
39
|
let [address, port] = nodeId.split(":");
|
package/src/webSocketServer.ts
CHANGED
|
@@ -9,15 +9,18 @@ import { getTrustedCertificates, watchTrustedCertificates } from "./certStore";
|
|
|
9
9
|
import { createCallFactory } from "./CallFactory";
|
|
10
10
|
import { parseSNIExtension, parseTLSHello, SNIType } from "./tlsParsing";
|
|
11
11
|
import debugbreak from "debugbreak";
|
|
12
|
+
import { getNodeId } from "./nodeCache";
|
|
13
|
+
import crypto from "crypto";
|
|
14
|
+
import { Watchable } from "./misc";
|
|
12
15
|
|
|
13
16
|
export type SocketServerConfig = (
|
|
14
17
|
https.ServerOptions & {
|
|
15
|
-
nodeId?: string;
|
|
16
|
-
|
|
17
18
|
key: string | Buffer;
|
|
18
19
|
cert: string | Buffer;
|
|
19
20
|
|
|
20
21
|
port: number;
|
|
22
|
+
/** You can also set `port: 0` if you don't care what port you want at all. */
|
|
23
|
+
useAvailablePortIfPortInUse?: boolean;
|
|
21
24
|
|
|
22
25
|
// public sets ip to "0.0.0.0", otherwise it defaults to "127.0.0.1", which
|
|
23
26
|
// causes the server to only accept local connections.
|
|
@@ -26,24 +29,38 @@ export type SocketServerConfig = (
|
|
|
26
29
|
|
|
27
30
|
/** If the SNI matches this domain, we use a different key/cert. */
|
|
28
31
|
SNICerts?: {
|
|
29
|
-
[domain: string]: https.ServerOptions
|
|
32
|
+
[domain: string]: Watchable<https.ServerOptions>;
|
|
30
33
|
};
|
|
31
34
|
}
|
|
32
35
|
);
|
|
33
36
|
|
|
34
37
|
export async function startSocketServer(
|
|
35
38
|
config: SocketServerConfig
|
|
36
|
-
): Promise<
|
|
39
|
+
): Promise<string> {
|
|
37
40
|
|
|
38
41
|
const webSocketServer = new ws.Server({
|
|
39
42
|
noServer: true,
|
|
40
43
|
});
|
|
41
44
|
|
|
42
|
-
function setupHTTPSServer(
|
|
43
|
-
let
|
|
45
|
+
async function setupHTTPSServer(watchOptions: Watchable<https.ServerOptions>) {
|
|
46
|
+
let httpsServerLast: https.Server | undefined;
|
|
47
|
+
let onHttpServer: (server: https.Server) => void;
|
|
48
|
+
let httpServerPromise = new Promise<https.Server>(r => onHttpServer = r);
|
|
49
|
+
let lastOptions!: https.ServerOptions;
|
|
50
|
+
await watchOptions(value => {
|
|
51
|
+
lastOptions = { ...value, ca: getTrustedCertificates() };
|
|
52
|
+
if (!httpsServerLast) {
|
|
53
|
+
httpsServerLast = https.createServer(lastOptions);
|
|
54
|
+
} else {
|
|
55
|
+
httpsServerLast.setSecureContext(lastOptions);
|
|
56
|
+
}
|
|
57
|
+
onHttpServer(httpsServerLast);
|
|
58
|
+
});
|
|
59
|
+
let httpsServer = await httpServerPromise;
|
|
60
|
+
|
|
44
61
|
watchTrustedCertificates(() => {
|
|
45
|
-
|
|
46
|
-
httpsServer.setSecureContext(
|
|
62
|
+
lastOptions.ca = getTrustedCertificates();
|
|
63
|
+
httpsServer.setSecureContext(lastOptions);
|
|
47
64
|
});
|
|
48
65
|
|
|
49
66
|
httpsServer.on("connection", socket => {
|
|
@@ -97,11 +114,17 @@ export async function startSocketServer(
|
|
|
97
114
|
let options: https.ServerOptions = {
|
|
98
115
|
...config,
|
|
99
116
|
};
|
|
117
|
+
if (!config.cert) {
|
|
118
|
+
throw new Error("No cert specified");
|
|
119
|
+
}
|
|
120
|
+
if (!config.key) {
|
|
121
|
+
throw new Error("No key specified");
|
|
122
|
+
}
|
|
100
123
|
|
|
101
|
-
const mainHTTPSServer = setupHTTPSServer(options);
|
|
124
|
+
const mainHTTPSServer = await setupHTTPSServer(callback => callback(options));
|
|
102
125
|
let sniServers = new Map<string, https.Server>();
|
|
103
126
|
for (let [domain, obj] of Object.entries(config.SNICerts || {})) {
|
|
104
|
-
sniServers.set(domain, setupHTTPSServer(obj));
|
|
127
|
+
sniServers.set(domain, await setupHTTPSServer(obj));
|
|
105
128
|
}
|
|
106
129
|
|
|
107
130
|
let httpServer = http.createServer({}, async function (req, res) {
|
|
@@ -130,6 +153,7 @@ export async function startSocketServer(
|
|
|
130
153
|
} else {
|
|
131
154
|
let data = parseTLSHello(buffer);
|
|
132
155
|
let sni = data.extensions.filter(x => x.type === SNIType).flatMap(x => parseSNIExtension(x.data))[0];
|
|
156
|
+
console.log(`Received TCP connection with SNI ${JSON.stringify(sni)}`);
|
|
133
157
|
server = sniServers.get(sni) || mainHTTPSServer;
|
|
134
158
|
}
|
|
135
159
|
|
|
@@ -158,12 +182,43 @@ export async function startSocketServer(
|
|
|
158
182
|
host = "0.0.0.0";
|
|
159
183
|
}
|
|
160
184
|
|
|
161
|
-
|
|
162
|
-
|
|
185
|
+
let port = config.port;
|
|
186
|
+
if (config.useAvailablePortIfPortInUse && port) {
|
|
187
|
+
async function isPortInUse(port: number): Promise<boolean> {
|
|
188
|
+
return new Promise<boolean>((resolve, reject) => {
|
|
189
|
+
let server = net.createServer();
|
|
190
|
+
server.listen(port, host)
|
|
191
|
+
.on("listening", function () {
|
|
192
|
+
server.close();
|
|
193
|
+
resolve(false);
|
|
194
|
+
}).on("close", function () {
|
|
195
|
+
resolve(true);
|
|
196
|
+
}).on("error", function (e) {
|
|
197
|
+
resolve(true);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (await isPortInUse(port)) {
|
|
202
|
+
port = 0;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(`Trying to listening on ${host}:${port}`);
|
|
207
|
+
realServer.listen(port, host);
|
|
163
208
|
|
|
164
209
|
await listenPromise;
|
|
165
210
|
|
|
166
|
-
|
|
211
|
+
port = (realServer.address() as net.AddressInfo).port;
|
|
212
|
+
let nodeId = getNodeId(getCommonName(config.cert), port);
|
|
213
|
+
console.log(`Started Listening on ${nodeId}`);
|
|
214
|
+
|
|
215
|
+
return nodeId;
|
|
216
|
+
}
|
|
167
217
|
|
|
168
|
-
|
|
218
|
+
function getCommonName(cert: Buffer | string) {
|
|
219
|
+
let subject = new crypto.X509Certificate(cert).subject;
|
|
220
|
+
let subjectKVPs = new Map(subject.split(",").map(x => x.trim().split("=")).map(x => [x[0], x.slice(1).join("=")]));
|
|
221
|
+
let commonName = subjectKVPs.get("CN");
|
|
222
|
+
if (!commonName) throw new Error(`No common name in subject: ${subject}`);
|
|
223
|
+
return commonName;
|
|
169
224
|
}
|
|
@@ -4,6 +4,7 @@ import { isNode } from "./misc";
|
|
|
4
4
|
import { SenderInterface } from "./CallFactory";
|
|
5
5
|
import { getTrustedCertificates } from "./certStore";
|
|
6
6
|
import { getNodeIdLocation } from "./nodeCache";
|
|
7
|
+
import debugbreak from "debugbreak";
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
export function getTLSSocket(webSocket: ws.WebSocket) {
|
package/test/client.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/// <reference path="../require/RequireController.ts" />
|
|
2
|
+
|
|
1
3
|
// https://letx.ca:2542/?classGuid=RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d&functionName=requireHTML&args=[%22./test/test%22]
|
|
2
4
|
|
|
3
5
|
//import typescript from "typescript";
|
|
@@ -24,8 +26,6 @@ void main();
|
|
|
24
26
|
async function main() {
|
|
25
27
|
if (isNode()) return;
|
|
26
28
|
|
|
27
|
-
SocketFunction.rejectUnauthorized = false;
|
|
28
|
-
|
|
29
29
|
SocketFunction.expose(Test);
|
|
30
30
|
|
|
31
31
|
console.log("cool");
|
package/test/shared.ts
CHANGED
|
@@ -41,7 +41,7 @@ class TestBase {
|
|
|
41
41
|
export const Test = SocketFunction.register(
|
|
42
42
|
"80d9f328-72df-4baa-8be8-019c1003d4a2",
|
|
43
43
|
new TestBase(),
|
|
44
|
-
{
|
|
44
|
+
() => ({
|
|
45
45
|
add: {
|
|
46
46
|
// hooks: [
|
|
47
47
|
// async (config) => {
|
|
@@ -49,17 +49,11 @@ export const Test = SocketFunction.register(
|
|
|
49
49
|
// }
|
|
50
50
|
// ]
|
|
51
51
|
},
|
|
52
|
-
callMe: {
|
|
53
|
-
clientHooks: [
|
|
54
|
-
async (config) => {
|
|
55
|
-
config.call.reconnectTimeout = 2000;
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
},
|
|
52
|
+
callMe: {},
|
|
59
53
|
callBack: {
|
|
60
54
|
|
|
61
55
|
},
|
|
62
56
|
//fncNotAsync: {},
|
|
63
57
|
//notAFnc: {},
|
|
64
|
-
}
|
|
58
|
+
})
|
|
65
59
|
);
|