socket-function 0.8.36 → 0.8.37
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 +50 -23
- package/SocketFunctionTypes.ts +12 -22
- package/package.json +1 -1
- package/src/CallFactory.ts +45 -7
- package/src/callManager.ts +6 -7
- package/src/nodeCache.ts +4 -1
package/SocketFunction.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/// <reference path="./require/RequireController.ts" />
|
|
2
2
|
|
|
3
|
-
import { SocketExposedInterface,
|
|
3
|
+
import { SocketExposedInterface, SocketFunctionHook, SocketFunctionClientHook, SocketExposedShape, SocketRegistered, CallerContext, FullCallType } from "./SocketFunctionTypes";
|
|
4
4
|
import { exposeClass, registerClass, registerGlobalClientHook, registerGlobalHook, runClientHooks } from "./src/callManager";
|
|
5
5
|
import { SocketServerConfig, startSocketServer } from "./src/webSocketServer";
|
|
6
|
-
import {
|
|
6
|
+
import { getCallFactory, getCreateCallFactory, getNodeId, getNodeIdLocation } from "./src/nodeCache";
|
|
7
7
|
import { getCallProxy } from "./src/nodeProxy";
|
|
8
8
|
import { Args, MaybePromise } from "./src/types";
|
|
9
9
|
import { setDefaultHTTPCall } from "./src/callHTTPHandler";
|
|
@@ -36,12 +36,18 @@ export class SocketFunction {
|
|
|
36
36
|
private static onMountCallbacks = new Map<string, (() => MaybePromise<void>)[]>();
|
|
37
37
|
public static exposedClasses = new Set<string>();
|
|
38
38
|
|
|
39
|
+
public static callerContext: CallerContext | undefined;
|
|
40
|
+
public static getCaller(): CallerContext {
|
|
41
|
+
const caller = SocketFunction.callerContext;
|
|
42
|
+
if (!caller) throw new Error(`Tried to access caller when not in the synchronous phase of a function call`);
|
|
43
|
+
return caller;
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
// NOTE: We use callbacks we don't run into issues with cyclic dependencies
|
|
40
47
|
// (ex, using a hook in a controller where the hook also calls the controller).
|
|
41
48
|
public static register<
|
|
42
49
|
ClassInstance extends object,
|
|
43
|
-
Shape extends SocketExposedShape<SocketExposedInterface
|
|
44
|
-
CallContext extends CallContextType
|
|
50
|
+
Shape extends SocketExposedShape<SocketExposedInterface>,
|
|
45
51
|
>(
|
|
46
52
|
classGuid: string,
|
|
47
53
|
instance: ClassInstance,
|
|
@@ -49,7 +55,7 @@ export class SocketFunction {
|
|
|
49
55
|
defaultHooksFnc?: () => SocketExposedShape[""] & {
|
|
50
56
|
onMount?: () => MaybePromise<void>;
|
|
51
57
|
}
|
|
52
|
-
): SocketRegistered<ExtractShape<ClassInstance, Shape
|
|
58
|
+
): SocketRegistered<ExtractShape<ClassInstance, Shape>> {
|
|
53
59
|
let getDefaultHooks = defaultHooksFnc && lazy(defaultHooksFnc);
|
|
54
60
|
const getShape = lazy(() => {
|
|
55
61
|
let shape = shapeFnc();
|
|
@@ -76,7 +82,7 @@ export class SocketFunction {
|
|
|
76
82
|
console.log(`START\t\t\t${classGuid}.${functionName}`);
|
|
77
83
|
}
|
|
78
84
|
try {
|
|
79
|
-
let callFactory = await
|
|
85
|
+
let callFactory = await getCreateCallFactory(nodeId, SocketFunction.mountedNodeId);
|
|
80
86
|
|
|
81
87
|
let shapeObj = getShape()[functionName];
|
|
82
88
|
if (!shapeObj) {
|
|
@@ -99,7 +105,6 @@ export class SocketFunction {
|
|
|
99
105
|
});
|
|
100
106
|
|
|
101
107
|
let output: SocketRegistered = {
|
|
102
|
-
context: curSocketContext,
|
|
103
108
|
nodes: nodeProxy,
|
|
104
109
|
_classGuid: classGuid,
|
|
105
110
|
};
|
|
@@ -119,6 +124,40 @@ export class SocketFunction {
|
|
|
119
124
|
return output as any;
|
|
120
125
|
}
|
|
121
126
|
|
|
127
|
+
public static onNextDisconnect(nodeId: string, callback: () => void) {
|
|
128
|
+
(async () => {
|
|
129
|
+
let factory = await getCallFactory(nodeId);
|
|
130
|
+
if (!factory) {
|
|
131
|
+
callback();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
factory.onNextDisconnect(callback);
|
|
136
|
+
})().catch(() => {
|
|
137
|
+
callback();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
public static getLastDisconnectTime(nodeId: string): number | undefined {
|
|
141
|
+
let factory = getCallFactory(nodeId);
|
|
142
|
+
if (!factory) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
if (factory instanceof Promise) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
return factory.lastClosed;
|
|
149
|
+
}
|
|
150
|
+
public static isNodeConnected(nodeId: string): boolean {
|
|
151
|
+
let factory = getCallFactory(nodeId);
|
|
152
|
+
if (!factory) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
if (factory instanceof Promise) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return !!factory.isConnected;
|
|
159
|
+
}
|
|
160
|
+
|
|
122
161
|
/** NOTE: Only works if the nodeIs used is from SocketFunction.connect (we can't convert arbitrary nodeIds into urls,
|
|
123
162
|
* as we have no way of knowing how to contain a nodeId).
|
|
124
163
|
* */
|
|
@@ -190,41 +229,29 @@ export class SocketFunction {
|
|
|
190
229
|
return getNodeId(location.address, location.port);
|
|
191
230
|
}
|
|
192
231
|
|
|
193
|
-
public static addGlobalHook
|
|
232
|
+
public static addGlobalHook(hook: SocketFunctionHook<SocketExposedInterface>) {
|
|
194
233
|
registerGlobalHook(hook as SocketFunctionHook);
|
|
195
234
|
}
|
|
196
|
-
public static addGlobalClientHook
|
|
235
|
+
public static addGlobalClientHook(hook: SocketFunctionClientHook<SocketExposedInterface>) {
|
|
197
236
|
registerGlobalClientHook(hook as SocketFunctionClientHook);
|
|
198
237
|
}
|
|
199
238
|
}
|
|
200
239
|
|
|
201
240
|
|
|
202
|
-
const curSocketContext: SocketRegistered["context"] = {
|
|
203
|
-
curContext: undefined,
|
|
204
|
-
caller: undefined,
|
|
205
|
-
getCaller() {
|
|
206
|
-
const caller = curSocketContext.caller;
|
|
207
|
-
if (!caller) throw new Error(`Tried to access caller when not in the synchronous phase of a function call`);
|
|
208
|
-
return caller;
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
241
|
let socketContextSeqNum = 1;
|
|
212
242
|
|
|
213
243
|
export function _setSocketContext<T>(
|
|
214
|
-
callContext: CallContextType,
|
|
215
244
|
caller: CallerContext,
|
|
216
245
|
code: () => T,
|
|
217
246
|
) {
|
|
218
247
|
socketContextSeqNum++;
|
|
219
248
|
let seqNum = socketContextSeqNum;
|
|
220
|
-
|
|
221
|
-
curSocketContext.caller = caller;
|
|
249
|
+
SocketFunction.callerContext = caller;
|
|
222
250
|
try {
|
|
223
251
|
return code();
|
|
224
252
|
} finally {
|
|
225
253
|
if (seqNum === socketContextSeqNum) {
|
|
226
|
-
|
|
227
|
-
curSocketContext.caller = undefined;
|
|
254
|
+
SocketFunction.callerContext = undefined;
|
|
228
255
|
}
|
|
229
256
|
}
|
|
230
257
|
}
|
package/SocketFunctionTypes.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module.allowclient = true;
|
|
4
4
|
|
|
5
|
+
import { SocketFunction } from "./SocketFunction";
|
|
5
6
|
import { getCallObj } from "./src/nodeProxy";
|
|
6
7
|
import { Args, MaybePromise } from "./src/types";
|
|
7
8
|
|
|
@@ -21,14 +22,14 @@ export type SocketExposedInterfaceClass = {
|
|
|
21
22
|
new(): unknown;
|
|
22
23
|
prototype: unknown;
|
|
23
24
|
};
|
|
24
|
-
export interface SocketExposedShape<ExposedType extends SocketExposedInterface = SocketExposedInterface
|
|
25
|
+
export interface SocketExposedShape<ExposedType extends SocketExposedInterface = SocketExposedInterface> {
|
|
25
26
|
[functionName: string]: {
|
|
26
27
|
/** Indicates with the same input, we give the same output, forever,
|
|
27
28
|
* independent of code changes. This only works for data storage.
|
|
28
29
|
*/
|
|
29
30
|
dataImmutable?: boolean;
|
|
30
|
-
hooks?: SocketFunctionHook<ExposedType
|
|
31
|
-
clientHooks?: SocketFunctionClientHook<ExposedType
|
|
31
|
+
hooks?: SocketFunctionHook<ExposedType>[];
|
|
32
|
+
clientHooks?: SocketFunctionClientHook<ExposedType>[];
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -41,32 +42,27 @@ export interface FullCallType extends CallType {
|
|
|
41
42
|
nodeId: string;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
export interface SocketFunctionHook<ExposedType extends SocketExposedInterface = SocketExposedInterface
|
|
45
|
-
(config: HookContext<ExposedType
|
|
45
|
+
export interface SocketFunctionHook<ExposedType extends SocketExposedInterface = SocketExposedInterface> {
|
|
46
|
+
(config: HookContext<ExposedType>): MaybePromise<void>;
|
|
46
47
|
/** NOTE: This is useful when we need a clientside hook to set up state specifically for our serverside hook. */
|
|
47
|
-
clientHook?: SocketFunctionClientHook<ExposedType
|
|
48
|
+
clientHook?: SocketFunctionClientHook<ExposedType>;
|
|
48
49
|
}
|
|
49
|
-
export type HookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface
|
|
50
|
+
export type HookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface> = {
|
|
50
51
|
call: FullCallType;
|
|
51
|
-
context: SocketRegistered<ExposedType, CallContext>["context"];
|
|
52
52
|
// If the result is overriden, we continue evaluating hooks BUT DO NOT perform the final call
|
|
53
53
|
overrideResult?: unknown;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
export type ClientHookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface
|
|
56
|
+
export type ClientHookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface> = {
|
|
57
57
|
call: FullCallType;
|
|
58
58
|
// If the result is overriden, we continue evaluating hooks BUT DO NOT perform the final call
|
|
59
59
|
overrideResult?: unknown;
|
|
60
60
|
};
|
|
61
|
-
export interface SocketFunctionClientHook<ExposedType extends SocketExposedInterface = SocketExposedInterface
|
|
62
|
-
(config: ClientHookContext<ExposedType
|
|
61
|
+
export interface SocketFunctionClientHook<ExposedType extends SocketExposedInterface = SocketExposedInterface> {
|
|
62
|
+
(config: ClientHookContext<ExposedType>): MaybePromise<void>;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
export
|
|
66
|
-
[key: string]: unknown;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export interface SocketRegistered<ExposedType = any, DynamicCallContext extends CallContextType = CallContextType> {
|
|
65
|
+
export interface SocketRegistered<ExposedType = any> {
|
|
70
66
|
nodes: {
|
|
71
67
|
// NOTE: Don't pass around nodeId to other nodes, instead pass around NetworkLocation (which they
|
|
72
68
|
// then turn into a nodeId, which they can then check permissions on themself).
|
|
@@ -76,12 +72,6 @@ export interface SocketRegistered<ExposedType = any, DynamicCallContext extends
|
|
|
76
72
|
}
|
|
77
73
|
};
|
|
78
74
|
};
|
|
79
|
-
context: {
|
|
80
|
-
// If undefined we are not synchronously in a call
|
|
81
|
-
curContext: DynamicCallContext | undefined;
|
|
82
|
-
caller: CallerContext | undefined;
|
|
83
|
-
getCaller(): CallerContext;
|
|
84
|
-
};
|
|
85
75
|
_classGuid: string;
|
|
86
76
|
}
|
|
87
77
|
export type CallerContext = Readonly<CallerContextBase>;
|
package/package.json
CHANGED
package/src/CallFactory.ts
CHANGED
|
@@ -10,6 +10,8 @@ import { getClientNodeId, getNodeIdLocation, registerNodeClient } from "./nodeCa
|
|
|
10
10
|
import debugbreak from "debugbreak";
|
|
11
11
|
import { lazy } from "./caching";
|
|
12
12
|
|
|
13
|
+
const MIN_RETRY_DELAY = 1000;
|
|
14
|
+
|
|
13
15
|
type InternalCallType = FullCallType & {
|
|
14
16
|
seqNum: number;
|
|
15
17
|
isReturn: false;
|
|
@@ -28,10 +30,13 @@ type InternalReturnType = {
|
|
|
28
30
|
|
|
29
31
|
export interface CallFactory {
|
|
30
32
|
nodeId: string;
|
|
33
|
+
lastClosed: number;
|
|
34
|
+
closedForever?: boolean;
|
|
35
|
+
isConnected?: boolean;
|
|
31
36
|
// NOTE: May or may not have reconnection or retry logic inside of performCall.
|
|
32
37
|
// Trigger performLocalCall on the other side of the connection
|
|
33
38
|
performCall(call: CallType): Promise<unknown>;
|
|
34
|
-
|
|
39
|
+
onNextDisconnect(callback: () => void): void;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
export interface SenderInterface {
|
|
@@ -59,6 +64,8 @@ export async function createCallFactory(
|
|
|
59
64
|
const createWebsocket = createWebsocketFactory();
|
|
60
65
|
const registerOnce = lazy(() => registerNodeClient(callFactory));
|
|
61
66
|
|
|
67
|
+
const canReconnect = !!getNodeIdLocation(nodeId);
|
|
68
|
+
|
|
62
69
|
let lastReceivedSeqNum = 0;
|
|
63
70
|
|
|
64
71
|
let pendingCalls: Map<number, {
|
|
@@ -71,19 +78,23 @@ export async function createCallFactory(
|
|
|
71
78
|
// in return calls.
|
|
72
79
|
let nextSeqNum = Math.random();
|
|
73
80
|
|
|
81
|
+
let lastConnectionAttempt = 0;
|
|
82
|
+
|
|
74
83
|
let callerContext: CallerContextBase = {
|
|
75
84
|
nodeId,
|
|
76
85
|
localNodeId
|
|
77
86
|
};
|
|
78
87
|
|
|
88
|
+
let disconnectCallbacks: (() => void)[] = [];
|
|
89
|
+
function onNextDisconnect(callback: () => void): void {
|
|
90
|
+
disconnectCallbacks.push(callback);
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
let callFactory: CallFactory = {
|
|
80
94
|
nodeId,
|
|
81
|
-
|
|
95
|
+
lastClosed: 0,
|
|
96
|
+
onNextDisconnect,
|
|
82
97
|
async performCall(call: CallType) {
|
|
83
|
-
if (callFactory.closedForever) {
|
|
84
|
-
throw new Error(`Connection lost to ${niceConnectionName}`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
98
|
let seqNum = nextSeqNum++;
|
|
88
99
|
let fullCall: InternalCallType = {
|
|
89
100
|
nodeId,
|
|
@@ -126,7 +137,11 @@ export async function createCallFactory(
|
|
|
126
137
|
registerOnce();
|
|
127
138
|
|
|
128
139
|
function onClose(error: string) {
|
|
140
|
+
callFactory.lastClosed = Date.now();
|
|
129
141
|
webSocketPromise = undefined;
|
|
142
|
+
if (!canReconnect) {
|
|
143
|
+
callFactory.closedForever = true;
|
|
144
|
+
}
|
|
130
145
|
for (let [key, call] of pendingCalls) {
|
|
131
146
|
pendingCalls.delete(key);
|
|
132
147
|
call.callback({
|
|
@@ -138,6 +153,14 @@ export async function createCallFactory(
|
|
|
138
153
|
compressed: false,
|
|
139
154
|
});
|
|
140
155
|
}
|
|
156
|
+
|
|
157
|
+
let callbacks = disconnectCallbacks;
|
|
158
|
+
disconnectCallbacks = [];
|
|
159
|
+
for (let callback of callbacks) {
|
|
160
|
+
try {
|
|
161
|
+
callback();
|
|
162
|
+
} catch { }
|
|
163
|
+
}
|
|
141
164
|
}
|
|
142
165
|
|
|
143
166
|
newWebSocket.addEventListener("error", e => {
|
|
@@ -159,6 +182,7 @@ export async function createCallFactory(
|
|
|
159
182
|
await new Promise<void>(resolve => {
|
|
160
183
|
newWebSocket.addEventListener("open", () => {
|
|
161
184
|
console.log(`Connection established to ${niceConnectionName}`);
|
|
185
|
+
callFactory.isConnected = true;
|
|
162
186
|
resolve();
|
|
163
187
|
});
|
|
164
188
|
newWebSocket.addEventListener("close", () => resolve());
|
|
@@ -166,15 +190,29 @@ export async function createCallFactory(
|
|
|
166
190
|
});
|
|
167
191
|
} else if (newWebSocket.readyState !== 1 /* OPEN */) {
|
|
168
192
|
onClose(`Websocket received in closed state`);
|
|
193
|
+
callFactory.isConnected = true;
|
|
169
194
|
}
|
|
170
195
|
}
|
|
171
196
|
|
|
172
197
|
async function send(data: Buffer) {
|
|
173
|
-
|
|
198
|
+
if (!webSocketPromise) {
|
|
199
|
+
if (canReconnect) {
|
|
200
|
+
webSocketPromise = tryToReconnect();
|
|
201
|
+
} else {
|
|
202
|
+
throw new Error(`Cannot send data to ${niceConnectionName} as the connection has closed`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
174
205
|
let webSocket = await webSocketPromise;
|
|
175
206
|
webSocket.send(data);
|
|
176
207
|
}
|
|
177
208
|
async function tryToReconnect(): Promise<SenderInterface> {
|
|
209
|
+
// Don't try to reconnect too often!
|
|
210
|
+
let timeSinceLastAttempt = Date.now() - lastConnectionAttempt;
|
|
211
|
+
if (timeSinceLastAttempt < MIN_RETRY_DELAY) {
|
|
212
|
+
await new Promise(r => setTimeout(r, MIN_RETRY_DELAY - timeSinceLastAttempt));
|
|
213
|
+
}
|
|
214
|
+
lastConnectionAttempt = Date.now();
|
|
215
|
+
|
|
178
216
|
let newWebSocket = createWebsocket(nodeId);
|
|
179
217
|
await initializeWebsocket(newWebSocket);
|
|
180
218
|
|
package/src/callManager.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CallerContext, CallType, ClientHookContext, FullCallType, HookContext, SocketExposedInterface, SocketExposedInterfaceClass, SocketExposedShape, SocketFunctionClientHook, SocketFunctionHook, SocketRegistered } from "../SocketFunctionTypes";
|
|
2
2
|
import { _setSocketContext } from "../SocketFunction";
|
|
3
3
|
import { isNode } from "./misc";
|
|
4
4
|
import debugbreak from "debugbreak";
|
|
@@ -41,14 +41,13 @@ export async function performLocalCall(
|
|
|
41
41
|
throw new Error(`Function ${call.functionName} does not exist`);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
let
|
|
45
|
-
let serverContext = await runServerHooks(call, { caller, curContext, getCaller: () => caller }, functionShape);
|
|
44
|
+
let serverContext = await runServerHooks(call, caller, functionShape);
|
|
46
45
|
if ("overrideResult" in serverContext) {
|
|
47
46
|
return serverContext.overrideResult;
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
// NOTE: We purposely don't await inside _setSocketContext, so the context is reset synchronously
|
|
51
|
-
let result = _setSocketContext(
|
|
50
|
+
let result = _setSocketContext(caller, () => {
|
|
52
51
|
return controller[call.functionName](...call.args);
|
|
53
52
|
});
|
|
54
53
|
|
|
@@ -119,12 +118,12 @@ export async function runClientHooks(
|
|
|
119
118
|
|
|
120
119
|
async function runServerHooks(
|
|
121
120
|
callType: FullCallType,
|
|
122
|
-
|
|
121
|
+
caller: CallerContext,
|
|
123
122
|
hooks: SocketExposedShape[""],
|
|
124
123
|
): Promise<HookContext> {
|
|
125
|
-
let hookContext: HookContext = { call: callType
|
|
124
|
+
let hookContext: HookContext = { call: callType };
|
|
126
125
|
for (let hook of globalHooks.concat(hooks.hooks || [])) {
|
|
127
|
-
await hook(hookContext);
|
|
126
|
+
await _setSocketContext(caller, () => hook(hookContext));
|
|
128
127
|
if ("overrideResult" in hookContext) {
|
|
129
128
|
break;
|
|
130
129
|
}
|
package/src/nodeCache.ts
CHANGED
|
@@ -59,7 +59,7 @@ export function registerNodeClient(callFactory: CallFactory) {
|
|
|
59
59
|
startCleanupLoop();
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
export function
|
|
62
|
+
export function getCreateCallFactory(nodeId: string, mountedNodeId: string): MaybePromise<CallFactory> {
|
|
63
63
|
let callFactory = nodeCache.get(nodeId);
|
|
64
64
|
if (callFactory === undefined) {
|
|
65
65
|
callFactory = createCallFactory(undefined, nodeId, mountedNodeId);
|
|
@@ -67,6 +67,9 @@ export function getCreateCallFactoryLocation(nodeId: string, mountedNodeId: stri
|
|
|
67
67
|
}
|
|
68
68
|
return callFactory;
|
|
69
69
|
}
|
|
70
|
+
export function getCallFactory(nodeId: string): MaybePromise<CallFactory | undefined> {
|
|
71
|
+
return nodeCache.get(nodeId);
|
|
72
|
+
}
|
|
70
73
|
|
|
71
74
|
const startCleanupLoop = lazy(() => {
|
|
72
75
|
(async () => {
|