socket-function 0.5.0 → 0.7.1

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 CHANGED
@@ -1,9 +1,14 @@
1
1
  import { SocketExposedInterface, CallContextType, SocketFunctionHook, SocketFunctionClientHook, SocketExposedShape, SocketRegistered, NetworkLocation, CallerContext, SocketExposedInterfaceClass, CallType } from "./SocketFunctionTypes";
2
- import { exposeClass, registerClass, registerGlobalClientHook, registerGlobalHook, runClientHooks } from "./callManager";
3
- import { SocketServerConfig, startSocketServer } from "./socketServer";
4
- import { getCallFactoryNodeId, getCreateCallFactoryLocation } from "./nodeCache";
5
- import { getCallProxy } from "./nodeProxy";
2
+ import { exposeClass, registerClass, registerGlobalClientHook, registerGlobalHook, runClientHooks } from "./src/callManager";
3
+ import { SocketServerConfig, startSocketServer } from "./src/socketServer";
4
+ import { getCallFactoryNodeId, getCreateCallFactoryLocation, getNetworkLocationHash } from "./src/nodeCache";
5
+ import { getCallProxy } from "./src/nodeProxy";
6
+ import { Args } from "./src/types";
7
+ import { setDefaultHTTPCall } from "./src/callHTTPHandler";
8
+ import { isNode } from "./src/misc";
9
+ import { getOwnNodeId } from "./src/nodeAuthentication";
6
10
 
11
+ module.allowclient = true;
7
12
 
8
13
  type ExtractShape<ClassType, Shape> = {
9
14
  [key in keyof Shape]: (
@@ -21,27 +26,29 @@ type PickByType<T, Value> = {
21
26
 
22
27
  export class SocketFunction {
23
28
  public static register<
24
- ClassType extends SocketExposedInterfaceClass,
29
+ ClassInstance extends object,
25
30
  Shape extends SocketExposedShape<SocketExposedInterface, CallContext>,
26
31
  CallContext extends CallContextType
27
32
  >(
28
33
  classGuid: string,
29
- classType: ClassType,
34
+ instance: ClassInstance,
30
35
  shape: Shape
31
- ): (
36
+ ):
37
+ (
32
38
  // Essentially just returns SocketRegistered
33
- ExtractShape<ClassType["prototype"], Shape> extends SocketExposedInterface
34
- ? SocketRegistered<ExtractShape<ClassType["prototype"], Shape>, CallContext>
39
+ ExtractShape<ClassInstance, Shape> extends SocketExposedInterface
40
+ ? SocketRegistered<ExtractShape<ClassInstance, Shape>, CallContext>
35
41
  : {
36
42
  error: "invalid shape";
37
- } & PickByType<ExtractShape<ClassType["prototype"], Shape>, string>
43
+ } & PickByType<ExtractShape<ClassInstance, Shape>, string>
38
44
  ) {
39
- registerClass(classGuid, classType, shape as any as SocketExposedShape);
45
+
46
+ registerClass(classGuid, instance as SocketExposedInterface, shape as any as SocketExposedShape);
40
47
 
41
48
  let nodeProxy = getCallProxy(classGuid, async (nodeId, functionName, args) => {
42
- let callFactory = getCallFactoryNodeId(nodeId);
49
+ let callFactory = await getCallFactoryNodeId(nodeId);
43
50
  if (!callFactory) {
44
- throw new Error(`Cannot reach node ${nodeId}. Either it was established via an HTTP call, or was incorrect provided to us via another node, which should have provided us a NetworkLocation instead.`);
51
+ throw new Error(`Cannot reach node ${nodeId}. It might have been incorrect provided to us via another node, which should have provided us a NetworkLocation instead.`);
45
52
  }
46
53
 
47
54
  let shapeObj = shape[functionName];
@@ -67,6 +74,7 @@ export class SocketFunction {
67
74
  let output: SocketRegistered = {
68
75
  context: curSocketContext,
69
76
  nodes: nodeProxy,
77
+ _classGuid: classGuid,
70
78
  };
71
79
 
72
80
  return output as any;
@@ -76,14 +84,34 @@ export class SocketFunction {
76
84
  * so the class type's module construction runs, which should trigger register. Otherwise you would have
77
85
  * to add additional imports to ensure the register call runs.
78
86
  */
79
- public static expose(classType: SocketExposedInterfaceClass) {
80
- exposeClass(classType);
87
+ public static expose(socketRegistered: SocketRegistered) {
88
+ exposeClass(socketRegistered);
81
89
  }
82
90
 
83
91
  public static async mount(config: SocketServerConfig) {
84
92
  await startSocketServer(config);
85
93
  }
86
94
 
95
+ public static async getOwnNodeId() {
96
+ return await getOwnNodeId();
97
+ }
98
+
99
+ /** Sets the default call when an http request is made, but no classGuid is set. */
100
+ public static setDefaultHTTPCall<
101
+ Registered extends SocketRegistered,
102
+ FunctionName extends keyof Registered["nodes"][""] & string,
103
+ >(
104
+ registered: Registered,
105
+ functionName: FunctionName,
106
+ ...args: Args<Registered["nodes"][""][FunctionName]>
107
+ ) {
108
+ setDefaultHTTPCall({
109
+ classGuid: registered._classGuid,
110
+ functionName,
111
+ args,
112
+ });
113
+ }
114
+
87
115
  public static async connect(location: NetworkLocation | { address: string; port: number }): Promise<string> {
88
116
  if (!("localPort" in location)) {
89
117
  location = {
@@ -95,6 +123,20 @@ export class SocketFunction {
95
123
  return await getCreateCallFactoryLocation(location);
96
124
  }
97
125
 
126
+ public static connectSync(location: NetworkLocation | { address: string; port: number }): string {
127
+ if (!("localPort" in location)) {
128
+ location = {
129
+ address: location.address,
130
+ listeningPorts: [location.port],
131
+ localPort: 0,
132
+ };
133
+ }
134
+ let tempNodeId = "syncTempNodeId_" + getNetworkLocationHash(location);
135
+
136
+ void getCreateCallFactoryLocation(location, tempNodeId);
137
+
138
+ return tempNodeId;
139
+ }
98
140
 
99
141
  public static addGlobalHook<CallContext extends CallContextType>(hook: SocketFunctionHook<SocketExposedInterface, CallContext>) {
100
142
  registerGlobalHook(hook as SocketFunctionHook);
@@ -1,4 +1,4 @@
1
- export const socket = Symbol.for("socket");
1
+ export const socket = Symbol("socket");
2
2
 
3
3
  export type SocketExposedInterface = {
4
4
  [functionName: string]: (...args: any[]) => Promise<unknown>;
@@ -58,6 +58,7 @@ export interface SocketRegistered<ExposedType extends SocketExposedInterface = S
58
58
  curContext: DynamicCallContext | undefined;
59
59
  caller: CallerContext | undefined;
60
60
  };
61
+ _classGuid: string;
61
62
  }
62
63
  export type CallerContext = {
63
64
  // IMPORTANT! Do not pass nodeId to other nodes with the intention of having
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "socket-function",
3
- "version": "0.5.0",
3
+ "version": "0.7.1",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "dependencies": {
7
+ "@types/cookie": "^0.5.1",
7
8
  "@types/node": "^18.0.0",
8
9
  "@types/ws": "^8.5.3",
9
- "debugbreak": "^0.6.3",
10
- "typenode": "0.3.0",
10
+ "cookie": "^0.5.0",
11
+ "debugbreak": "^0.6.5",
12
+ "typenode": "^0.4.3",
11
13
  "ws": "^8.8.0"
12
14
  },
13
15
  "scripts": {
@@ -0,0 +1,18 @@
1
+ import debugbreak from "debugbreak";
2
+ import { compileTransformBefore } from "typenode";
3
+
4
+ compileTransformBefore((contents: string, path: string, module: NodeJS.Module): string => {
5
+ if (path.endsWith(".css")) {
6
+ module.allowclient = true;
7
+ function injectCSS(contents: string) {
8
+ if (typeof document === "undefined") {
9
+ return;
10
+ }
11
+ let style = document.createElement("style");
12
+ style.innerHTML = contents;
13
+ document.head.appendChild(style);
14
+ }
15
+ return `(${injectCSS.toString()})(${JSON.stringify(contents)})`;
16
+ }
17
+ return contents;
18
+ });
@@ -0,0 +1,242 @@
1
+ import debugbreak from "debugbreak";
2
+ import fs from "fs";
3
+ import { SocketFunction } from "../SocketFunction";
4
+ import { setHTTPResultHeaders } from "../src/callHTTPHandler";
5
+
6
+ module.allowclient = true;
7
+
8
+ declare global {
9
+ namespace NodeJS {
10
+ interface Module {
11
+ /** Indiciates the module is allowed clientside. */
12
+ allowclient?: boolean;
13
+
14
+ /** Indicates the module is definitely not allowed clientside */
15
+ serveronly?: boolean;
16
+
17
+ // TODO: Move seqNum into the actual compilation, and make it increment,
18
+ // so the clientside can properly handle race conditions during hot reloading.
19
+ // And... maybe it is useful in other cases?
20
+ /** Used internally by RequireController */
21
+ requireControllerSeqNum?: number;
22
+ }
23
+ }
24
+ interface Window {
25
+ clientsideBootTime: number;
26
+ }
27
+ }
28
+
29
+ export interface SerializedModule {
30
+ originalId: string;
31
+ filename: string;
32
+ // If a module is not allowed clientside it is likely requests will be empty,
33
+ // to save effort parsing requests for modules that only exist to give better
34
+ // error messages.
35
+ requests: {
36
+ // request => resolvedPath
37
+ [request: string]: string;
38
+ };
39
+ // NOTE: IF !allowclient && !serveronly, it might just mean we didn't add allowclient
40
+ // to the module yet. BUT, if serveronly, then we know for sure we don't want it client.
41
+ // So the messages and behavior will be different.
42
+ allowclient?: boolean;
43
+ serveronly?: boolean;
44
+ // Just for errors mostly
45
+ alwayssend?: boolean;
46
+
47
+ /** Only set if allowclient. */
48
+ source?: string;
49
+
50
+ seqNum: number;
51
+ }
52
+
53
+ let nextModuleSeqNum = 1;
54
+
55
+ const requireSeqNumProcessId = "requireSeqNumProcessId_" + Date.now() + "_" + Math.random();
56
+
57
+ const htmlFile = fs.readFileSync(__dirname + "/require.html").toString();
58
+ const jsFile = fs.readFileSync(__dirname + "/require.js").toString();
59
+ const bufferShim = fs.readFileSync(__dirname + "/buffer.js").toString();
60
+
61
+ const resolvedHTMLFile = (
62
+ htmlFile
63
+ .replace(`<script src="./buffer.js"></script>`, `<script>${bufferShim}</script>`)
64
+ .replace(`<script src="./require.js"></script>`, `<script>${jsFile}</script>`)
65
+ );
66
+
67
+ class RequireControllerBase {
68
+ constructor(private rootResolvePath: string) { }
69
+
70
+ public async requireHTML(bootRequirePath?: string) {
71
+ let result = resolvedHTMLFile;
72
+ if (bootRequirePath) {
73
+ result = result.replace(`<!-- ENTRY_TEMPLATE -->`, `<script>require(${JSON.stringify(bootRequirePath)});</script>`);
74
+ }
75
+ return setHTTPResultHeaders(Buffer.from(result), { "Content-Type": "text/html" });
76
+ }
77
+
78
+ public async bufferJS() {
79
+ return setHTTPResultHeaders(Buffer.from(bufferShim), { "Content-Type": "text/javascript" });
80
+ }
81
+ public async requireJS() {
82
+ return setHTTPResultHeaders(Buffer.from(jsFile), { "Content-Type": "text/javascript" });
83
+ }
84
+
85
+ public async getModules(
86
+ pathRequests: string[],
87
+ alreadyHave?: {
88
+ requireSeqNumProcessId: string;
89
+ // NOTE: Highly optimized, as otherwise this can easily be KBs (I was seeing 9KB),
90
+ // which is uploaded, and so can be quite slow on slow connections.
91
+ seqNumRanges: {
92
+ s: number;
93
+ // undefined means s + 1 (so just a single number)
94
+ e?: number;
95
+ }[];
96
+ },
97
+ ): Promise<{
98
+ requestsResolvedPaths: string[];
99
+ modules: {
100
+ [resolvedPath: string]: SerializedModule;
101
+ };
102
+ requireSeqNumProcessId: string;
103
+ }> {
104
+ let seqNums: { [seqNum: number]: 1 } = {};
105
+ if (alreadyHave?.requireSeqNumProcessId === requireSeqNumProcessId) {
106
+ for (let { s, e } of alreadyHave.seqNumRanges) {
107
+ if (e === undefined) {
108
+ e = s + 1;
109
+ }
110
+ for (let i = s; i < e; i++) {
111
+ seqNums[i] = 1;
112
+ }
113
+ }
114
+ }
115
+
116
+ let modules: {
117
+ [resolvedPath: string]: SerializedModule;
118
+ } = Object.create(null);
119
+ function addModule(module: NodeJS.Module) {
120
+ if (!module.requireControllerSeqNum) {
121
+ module.requireControllerSeqNum = nextModuleSeqNum++;
122
+ }
123
+ if (seqNums[module.requireControllerSeqNum]) {
124
+ return;
125
+ }
126
+ if (module.filename in modules) return;
127
+
128
+ // TODO: Remove unused exports. We know why the module is being requested, so we can
129
+ // actually very effectively know which exports it has which will never be used.
130
+ // - Of course, we would need to make the module specially, so if any new modules
131
+ // use it we can know... what was removed? It becomes complicated with
132
+ // lazy modules, but... it is still very important.
133
+
134
+ // IMPORTANT! Use module.filename, to strip the ".CLIENT_NAMEPSACE" extension
135
+ modules[module.filename] = {
136
+ originalId: module.id,
137
+ filename: module.filename,
138
+ // NOTE: Due to recursive sets of allowclient, it is very possible for allowclient && serveronly to be set.
139
+ allowclient: module.allowclient && !module.serveronly,
140
+ serveronly: module.serveronly,
141
+ requests: Object.create(null),
142
+ seqNum: module.requireControllerSeqNum,
143
+ };
144
+ let moduleObj = modules[module.filename];
145
+ if (moduleObj.allowclient) {
146
+ moduleObj.source = module.moduleContents;
147
+ if (module.filename.endsWith(".json") && !moduleObj.source) {
148
+ moduleObj.source = module.moduleContents = fs.readFileSync(module.filename).toString();
149
+ }
150
+ }
151
+
152
+ // NOTE: Iterate on children even if it isn't allowed client, as the module may have children
153
+ // that are allowed clientside, and that have side-effects! (Mostly for static resources)
154
+ // - Surprisingly, this only increases the returned size by about 8% (probably more like 16%
155
+ // if we turn source maps off), so... it's fine. And with compression most of the extra
156
+ // size will go away, as paths are highly repetitive.
157
+ // - And now it increases the size by much less, as we ignore any subtree which are entirely
158
+ // not allowed on the client.
159
+ for (let request in module.requires) {
160
+ let requireResolvedPath = module.requires[request];
161
+ let requiredModule = require.cache[requireResolvedPath];
162
+
163
+ if (requiredModule) {
164
+ addModule(requiredModule);
165
+ moduleObj.requests[request] = requiredModule.filename;
166
+ } else {
167
+ moduleObj.requests[request] = "";
168
+ }
169
+ }
170
+ }
171
+
172
+ let searchPaths: string[] = [];
173
+ {
174
+ searchPaths.push(this.rootResolvePath);
175
+ let pathParts = this.rootResolvePath.replaceAll("\\", "/").split("/");
176
+ for (let i = 0; i < pathParts.length; i++) {
177
+ // Skip empty path parts, to preventing the case where the path ends
178
+ // with a /, which would result in "D:/test//node_modules"
179
+ if (!pathParts[i]) continue;
180
+ searchPaths.push(pathParts.slice(0, i + 1).join("/") + "/node_modules");
181
+ }
182
+ }
183
+
184
+
185
+ let requestsResolvedPaths: string[] = [];
186
+ for (let pathRequest of pathRequests) {
187
+ let resolvedPath = "";
188
+ try {
189
+ resolvedPath = require.resolve(pathRequest, { paths: searchPaths });
190
+ } catch { }
191
+ requestsResolvedPaths.push(resolvedPath);
192
+
193
+ function createNotFoundModule(error: string): NodeJS.Module {
194
+ console.warn(error);
195
+ return {
196
+ exports: {},
197
+ children: [],
198
+ filename: resolvedPath,
199
+ id: resolvedPath,
200
+ isPreloading: false,
201
+ require: null as any,
202
+ loaded: true,
203
+ load: null as any,
204
+ parent: undefined,
205
+ path: "",
206
+ paths: [],
207
+ requires: {},
208
+ allowclient: true,
209
+ moduleContents: `console.warn(${JSON.stringify(error)})`,
210
+ };
211
+ }
212
+
213
+ // TODO: We could use import() here... but that would only make the root call asynchronous,
214
+ // which wouldn't prevent synchronous blocking by that much anyway...
215
+ //require(rootPath);
216
+ let clientModule = require.cache[resolvedPath];
217
+ if (!clientModule) {
218
+ clientModule = createNotFoundModule(`Module ${pathRequest} (resolved to ${resolvedPath}) was not included serverside.`);
219
+ }
220
+ if (!clientModule.allowclient) {
221
+ clientModule = createNotFoundModule(`Module ${pathRequest} (resolved to ${resolvedPath}) is not allowed clientside (set module.allowclient in it, or call setFlag when it is imported).`);
222
+ }
223
+
224
+ addModule(clientModule);
225
+ }
226
+
227
+ return { requestsResolvedPaths, modules, requireSeqNumProcessId };
228
+ }
229
+ }
230
+
231
+ export function RequireControllerFactory(rootResolvePath: string) {
232
+ return SocketFunction.register(
233
+ "RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d",
234
+ new RequireControllerBase(rootResolvePath),
235
+ {
236
+ getModules: {},
237
+ requireHTML: {},
238
+ bufferJS: {},
239
+ requireJS: {},
240
+ }
241
+ );
242
+ }