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.
@@ -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
@@ -0,0 +1,11 @@
1
+ import os from "os";
2
+ import fs from "fs";
3
+ import { lazy } from "./caching";
4
+
5
+ export const getAppFolder = lazy(() => {
6
+ const path = os.homedir() + "/socket-function/";
7
+ if(!fs.existsSync(path)) {
8
+ fs.mkdirSync(path);
9
+ }
10
+ return path;
11
+ });
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
+ }