socket-function 0.5.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/.eslintrc.js +51 -0
- package/.vscode/settings.json +26 -0
- package/CallInstance.ts +323 -0
- package/SocketFunction.ts +131 -0
- package/SocketFunctionTypes.ts +80 -0
- package/args.ts +22 -0
- package/caching.ts +301 -0
- package/callManager.ts +108 -0
- package/index.ts +0 -0
- package/misc.ts +27 -0
- package/nodeAuthentication.ts +110 -0
- package/nodeCache.ts +79 -0
- package/nodeProxy.ts +36 -0
- package/package.json +17 -0
- package/socketServer.ts +74 -0
- package/spec.txt +104 -0
- package/storagePath.ts +11 -0
- package/test.ts +87 -0
- package/tsconfig.json +27 -0
- package/types.ts +9 -0
package/socketServer.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import https from "https";
|
|
2
|
+
import http from "http";
|
|
3
|
+
import net from "net";
|
|
4
|
+
import * as ws from "ws";
|
|
5
|
+
import { performLocalCall } from "./callManager";
|
|
6
|
+
import { CallerContext, CallType, NetworkLocation } from "./SocketFunctionTypes";
|
|
7
|
+
import { callFactoryFromWS } from "./CallInstance";
|
|
8
|
+
import { registerNodeClient } from "./nodeCache";
|
|
9
|
+
import { getCertKeyPair } from "./nodeAuthentication";
|
|
10
|
+
|
|
11
|
+
export type SocketServerConfig = {
|
|
12
|
+
port: number;
|
|
13
|
+
// public sets ip to "0.0.0.0", otherwise it defaults to "127.0.0.1", which
|
|
14
|
+
// causes the server to only accept local connections.
|
|
15
|
+
public?: boolean;
|
|
16
|
+
ip?: string;
|
|
17
|
+
} & (
|
|
18
|
+
https.ServerOptions
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export async function startSocketServer(
|
|
22
|
+
config: SocketServerConfig
|
|
23
|
+
) {
|
|
24
|
+
let isSecure = "cert" in config || "key" in config || "pfx" in config;
|
|
25
|
+
if (!isSecure) {
|
|
26
|
+
let { key, cert } = getCertKeyPair();
|
|
27
|
+
config.key = key;
|
|
28
|
+
config.cert = cert;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// TODO: Only allow unauthorized for ip certificates, and then for domains use the domain as the nodeId,
|
|
32
|
+
// so it is easy to read, and consistent.
|
|
33
|
+
let server = https.createServer({
|
|
34
|
+
...config,
|
|
35
|
+
rejectUnauthorized: false,
|
|
36
|
+
requestCert: true
|
|
37
|
+
});
|
|
38
|
+
let listenPromise = new Promise<void>((resolve, error) => {
|
|
39
|
+
server.on("listening", () => {
|
|
40
|
+
resolve();
|
|
41
|
+
});
|
|
42
|
+
server.on("error", e => {
|
|
43
|
+
error(e);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
let host = config.ip ?? "127.0.0.1";
|
|
49
|
+
if (config.public) {
|
|
50
|
+
host = "0.0.0.0";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
server.on("request", (request, response) => {
|
|
54
|
+
// TODO: Handle HTTP requests
|
|
55
|
+
// - HTTP CAN have a nodeId, simply through setting cookies
|
|
56
|
+
// - Cookies could always be set via a request before we open
|
|
57
|
+
// the websocket connection?
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const webSocketServer = new ws.Server({
|
|
61
|
+
noServer: true,
|
|
62
|
+
});
|
|
63
|
+
server.on("upgrade", (request, socket, upgradeHead) => {
|
|
64
|
+
webSocketServer.handleUpgrade(request, socket, upgradeHead, async (ws) => {
|
|
65
|
+
let clientCallFactory = await callFactoryFromWS(ws);
|
|
66
|
+
registerNodeClient(clientCallFactory);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
console.log(`Listening on ${host}:${config.port}`);
|
|
71
|
+
server.listen(config.port, host);
|
|
72
|
+
|
|
73
|
+
return await listenPromise;
|
|
74
|
+
}
|
package/spec.txt
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
spec.txt
|
|
2
|
+
|
|
3
|
+
- Implement SocketFunction for NodeJS => NodeJS
|
|
4
|
+
- Publish it, and make sure we can include it correctly in non-ts projects
|
|
5
|
+
(so we need to publish the dist folders that typenode creates).
|
|
6
|
+
- We might need to emit source maps as well?
|
|
7
|
+
- Maybe we should just compile with typescript?
|
|
8
|
+
- Support HTTP responses in SocketFunction
|
|
9
|
+
- Expose a http://127.0.0.1/RequireController-6016c77f-6863-47b5-a421-2abdea637436?html=./index.html&js=./index.ts endpoint
|
|
10
|
+
- RequireController will have to look through all imports, and send the files clientside
|
|
11
|
+
- Use allowclient to allow whitelisting of files, and setFlag to allow nested values. ALso compileDirFlags.ts
|
|
12
|
+
- Add handling for .css files by calling compileTransform in typenode to add a handler for .css (after adding it to require.extensions).
|
|
13
|
+
- Support default HTTP function in SocketFunction, via functions.httpDefault(() => {}), so we can expose http://127.0.0.1
|
|
14
|
+
|
|
15
|
+
- Other libraries
|
|
16
|
+
- JSON buffer serialize, which generates an object, that allows for rehydration of buffers
|
|
17
|
+
- Also... static classes (maybe even static resources), so structures can be sent
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
================== SocketFunction ==================
|
|
22
|
+
|
|
23
|
+
- Uses proxies, so that functions can be called before we know the shape of interface
|
|
24
|
+
- Headers
|
|
25
|
+
- Support enabling "Access-Control-Allow-Credentials"/Request.credentials=include, with a hardcoded list of domains
|
|
26
|
+
- Support Access-Control-Allow-Origin, with a hardcoded list of domains
|
|
27
|
+
- Always set
|
|
28
|
+
- response.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
29
|
+
- response.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
|
|
30
|
+
- response.setHeader("Cross-Origin-Resource-Policy", "same-site");
|
|
31
|
+
- Remember to set headers for OPTIONS, but then NOOP
|
|
32
|
+
|
|
33
|
+
// NOTE: It is not possible to expose different services over different ports in the same process.
|
|
34
|
+
// Just run different processes if you want different services.
|
|
35
|
+
SocketFunction.expose(ExampleController);
|
|
36
|
+
// Global hooks are useful for authentication
|
|
37
|
+
SocketFunction.addGlobalHook<ExampleContext>(null as SocketFunctionHook);
|
|
38
|
+
// Mount only after exposing controllers and setting up hooks
|
|
39
|
+
SocketFunction.mount({ port: 40981 });
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
// callerId is set before each function (and part of the hook context)
|
|
43
|
+
let callerId = ExampleController[socket].callerId;
|
|
44
|
+
let result = await ExampleController[socket].nodes[callerId].exampleFunction("hi");
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
// An object with context information is available in each call (so arguments don't have to be modified)
|
|
48
|
+
// - register will have a second generic argument that is context, so this will be typed
|
|
49
|
+
ExampleController[socket].callContext
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
// Clientside may also wish to expose controllers, possibly the same, or different.
|
|
54
|
+
SocketFunction.expose(ExampleControllerClient);
|
|
55
|
+
// We might also want global clientside hooks (for authentication)
|
|
56
|
+
SocketFunction.addGlobalClientHook<ExampleContext>(null as SocketFunctionHook);
|
|
57
|
+
let serverId = await SocketFunctions.connect({ host: "example.com", port: 40981 });
|
|
58
|
+
// Cached, so it can be put in a helper function and called every time a call is made
|
|
59
|
+
let serverId = SocketFunctions.connectSync({ host: "example.com", port: 40981 });
|
|
60
|
+
ExampleController[socket].nodes[serverId].exampleFunction("hi server");
|
|
61
|
+
// If you have multiple servers each with many endpoints, you can make helper functions like this:
|
|
62
|
+
function exampleControllers() {
|
|
63
|
+
let serverId = SocketFunctions.connectSync({ host: "example.com", port: 40981 });
|
|
64
|
+
return {
|
|
65
|
+
ExampleController: ExampleController[socket].nodes[serverId],
|
|
66
|
+
... etc, with all controllers mapped like this:
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
export class ExampleController {
|
|
72
|
+
// Uses both types AND shape configuration, to prevent functions from accidentally being exposed
|
|
73
|
+
// on the public internet...
|
|
74
|
+
// - Type checking is done to ensure no functions are exposed that aren't in your type
|
|
75
|
+
[socket] = SocketFunction.register<ExampleController, ExampleContext>("ExampleController-2a4b1bd1-d00f-4812-be32-c4466f3c354a", {
|
|
76
|
+
exampleFunction: {
|
|
77
|
+
// Hooks wrap the call, allowing them to cancel it, change arguments, change the output, run it
|
|
78
|
+
// on another thread, check permission, etc, etc
|
|
79
|
+
// - Hooks are asynchronously, so they can even trigger other calls, etc
|
|
80
|
+
// - Context is passed to hooks
|
|
81
|
+
hooks: [] as SocketFunctionHook[],
|
|
82
|
+
// Client hooks run before a call, on the client. They have a different context,
|
|
83
|
+
// because they won't have information such as caller ip, but they can wrap calls
|
|
84
|
+
// in mostly the similar way
|
|
85
|
+
clientHooks: [] as SocketFunctionClientHook[]
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
async exampleFunction(arg1: string) {
|
|
90
|
+
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ALSO, a shim can be created to avoid exposing your source code to API users (such as webpage).
|
|
95
|
+
// - If you want, you can have your implementation import your config shape from the client file,
|
|
96
|
+
// that way you only need to write it once.
|
|
97
|
+
import type * as Base from "./ExampleController";
|
|
98
|
+
export class ExampleController {
|
|
99
|
+
[socket] = SocketFunction.register<Base.ExampleController, ExampleContext>("ExampleController-2a4b1bd1-d00f-4812-be32-c4466f3c354a", {
|
|
100
|
+
exampleFunction: {
|
|
101
|
+
clientHooks: [] as SocketFunctionClientHook[]
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
package/storagePath.ts
ADDED
package/test.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { getArgs } from "./args";
|
|
2
|
+
import { SocketFunction } from "./SocketFunction";
|
|
3
|
+
|
|
4
|
+
//todonext;
|
|
5
|
+
// Test the server and client
|
|
6
|
+
// - I guess we will want to be able to namespace identifies so we can test
|
|
7
|
+
// multiple on the same machine... Let's not use yargs, just argv parsing should be okay?
|
|
8
|
+
|
|
9
|
+
class Test {
|
|
10
|
+
memberVariable = 5;
|
|
11
|
+
|
|
12
|
+
async add(lhs: number, rhs: number) {
|
|
13
|
+
return lhs + rhs;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async callMe() {
|
|
17
|
+
let caller = TestClass.context.caller?.nodeId;
|
|
18
|
+
if (!caller) {
|
|
19
|
+
throw new Error("No caller?");
|
|
20
|
+
}
|
|
21
|
+
console.log(`Caller is ${caller}`);
|
|
22
|
+
void (async () => {
|
|
23
|
+
let seqNum = 1;
|
|
24
|
+
while (true) {
|
|
25
|
+
console.log(`Calling client at ${seqNum}`);
|
|
26
|
+
await TestClass.nodes[caller].callBack();
|
|
27
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
28
|
+
seqNum++;
|
|
29
|
+
}
|
|
30
|
+
})();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async callBack() {
|
|
34
|
+
console.log(`Got callback at ${Date.now()}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const TestClass = SocketFunction.register(
|
|
39
|
+
"80d9f328-72df-4baa-8be8-019c1003d4a2",
|
|
40
|
+
Test,
|
|
41
|
+
{
|
|
42
|
+
add: {
|
|
43
|
+
// hooks: [
|
|
44
|
+
// async (config) => {
|
|
45
|
+
|
|
46
|
+
// }
|
|
47
|
+
// ]
|
|
48
|
+
},
|
|
49
|
+
callMe: {
|
|
50
|
+
clientHooks: [
|
|
51
|
+
async (config) => {
|
|
52
|
+
config.call.reconnectTimeout = 2000;
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
callBack: {
|
|
57
|
+
|
|
58
|
+
},
|
|
59
|
+
//fncNotAsync: {},
|
|
60
|
+
//notAFnc: {},
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
void main();
|
|
65
|
+
|
|
66
|
+
//todonext
|
|
67
|
+
// - Get case where there will never be a reconnection working
|
|
68
|
+
|
|
69
|
+
async function main() {
|
|
70
|
+
SocketFunction.expose(Test);
|
|
71
|
+
const port = 2542;
|
|
72
|
+
if (getArgs().identity === "server") {
|
|
73
|
+
await SocketFunction.mount({ port });
|
|
74
|
+
} else {
|
|
75
|
+
let serverId = await SocketFunction.connect({ port, address: "localhost" });
|
|
76
|
+
let test = await TestClass.nodes[serverId].add(1, 2);
|
|
77
|
+
console.log(`${test}=${1 + 2}`);
|
|
78
|
+
|
|
79
|
+
// while (true) {
|
|
80
|
+
// let test = await TestClass.nodes[serverId].add(1, 2);
|
|
81
|
+
// console.log(`${test}=${1 + 2}`);
|
|
82
|
+
// await new Promise(resolve => setTimeout(resolve, 1000));
|
|
83
|
+
// }
|
|
84
|
+
|
|
85
|
+
await TestClass.nodes[serverId].callMe();
|
|
86
|
+
}
|
|
87
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"esModuleInterop": true,
|
|
6
|
+
"allowSyntheticDefaultImports": true,
|
|
7
|
+
"moduleResolution": "node",
|
|
8
|
+
"target": "es2018",
|
|
9
|
+
"lib": [
|
|
10
|
+
"ESNext",
|
|
11
|
+
"dom",
|
|
12
|
+
"dom.iterable"
|
|
13
|
+
],
|
|
14
|
+
"jsx": "react",
|
|
15
|
+
"alwaysStrict": true,
|
|
16
|
+
"jsxFactory": "preact.createElement",
|
|
17
|
+
"jsxFragmentFactory": "preact.Fragment",
|
|
18
|
+
"types": [
|
|
19
|
+
"node",
|
|
20
|
+
],
|
|
21
|
+
"experimentalDecorators": true,
|
|
22
|
+
"emitDecoratorMetadata": false,
|
|
23
|
+
"skipLibCheck": true,
|
|
24
|
+
"inlineSourceMap": true,
|
|
25
|
+
"inlineSources": true,
|
|
26
|
+
},
|
|
27
|
+
}
|
package/types.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type MaybePromise<T> = T | Promise<T>;
|
|
2
|
+
|
|
3
|
+
export type Args<T> = T extends (...args: infer V) => any ? V : never;
|
|
4
|
+
|
|
5
|
+
export type AnyFunction = (...args: any) => any;
|
|
6
|
+
|
|
7
|
+
export function canHaveChildren(value: unknown): value is object {
|
|
8
|
+
return typeof value === "object" && value !== null || typeof value === "function";
|
|
9
|
+
}
|