socket-function 0.5.0 → 0.6.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/SocketFunction.ts +41 -13
- package/SocketFunctionTypes.ts +2 -1
- package/package.json +5 -3
- package/require/CSSShim.ts +18 -0
- package/require/RequireController.ts +243 -0
- package/require/buffer.js +2369 -0
- package/require/compileFlags.ts +44 -0
- package/require/require.html +14 -0
- package/require/require.js +464 -0
- package/spec.txt +12 -13
- package/{CallInstance.ts → src/CallFactory.ts} +48 -22
- package/{index.ts → src/allowclient.flag} +0 -0
- package/{args.ts → src/args.ts} +0 -0
- package/{caching.ts → src/caching.ts} +2 -2
- package/src/callHTTPHandler.ts +160 -0
- package/{callManager.ts → src/callManager.ts} +8 -10
- package/{misc.ts → src/misc.ts} +9 -4
- package/{nodeAuthentication.ts → src/nodeAuthentication.ts} +60 -11
- package/{nodeCache.ts → src/nodeCache.ts} +3 -3
- package/{nodeProxy.ts → src/nodeProxy.ts} +4 -4
- package/src/socketServer.ts +140 -0
- package/{storagePath.ts → src/storagePath.ts} +2 -2
- package/{types.ts → src/types.ts} +0 -0
- package/test/allowclient.flag +0 -0
- package/test/client.css +3 -0
- package/test/client.ts +42 -0
- package/test/server.ts +34 -0
- package/test/shared.ts +65 -0
- package/socketServer.ts +0 -74
- package/test.ts +0 -87
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 } 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,22 +26,24 @@ type PickByType<T, Value> = {
|
|
|
21
26
|
|
|
22
27
|
export class SocketFunction {
|
|
23
28
|
public static register<
|
|
24
|
-
|
|
29
|
+
ClassInstance extends object,
|
|
25
30
|
Shape extends SocketExposedShape<SocketExposedInterface, CallContext>,
|
|
26
31
|
CallContext extends CallContextType
|
|
27
32
|
>(
|
|
28
33
|
classGuid: string,
|
|
29
|
-
|
|
34
|
+
instance: ClassInstance,
|
|
30
35
|
shape: Shape
|
|
31
|
-
):
|
|
36
|
+
):
|
|
37
|
+
(
|
|
32
38
|
// Essentially just returns SocketRegistered
|
|
33
|
-
ExtractShape<
|
|
34
|
-
? SocketRegistered<ExtractShape<
|
|
39
|
+
ExtractShape<ClassInstance, Shape> extends SocketExposedInterface
|
|
40
|
+
? SocketRegistered<ExtractShape<ClassInstance, Shape>, CallContext>
|
|
35
41
|
: {
|
|
36
42
|
error: "invalid shape";
|
|
37
|
-
} & PickByType<ExtractShape<
|
|
43
|
+
} & PickByType<ExtractShape<ClassInstance, Shape>, string>
|
|
38
44
|
) {
|
|
39
|
-
|
|
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
49
|
let callFactory = getCallFactoryNodeId(nodeId);
|
|
@@ -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(
|
|
80
|
-
exposeClass(
|
|
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 = {
|
package/SocketFunctionTypes.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const socket = Symbol
|
|
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.
|
|
3
|
+
"version": "0.6.0",
|
|
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
|
-
"
|
|
10
|
-
"
|
|
10
|
+
"cookie": "^0.5.0",
|
|
11
|
+
"debugbreak": "^0.6.5",
|
|
12
|
+
"typenode": "^0.4.2",
|
|
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,243 @@
|
|
|
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
|
+
// TODO: Check timestamps on .json file and get most recent value
|
|
148
|
+
if (module.filename.endsWith(".json") && !moduleObj.source) {
|
|
149
|
+
moduleObj.source = module.moduleContents = fs.readFileSync(module.filename).toString();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// NOTE: Iterate on children even if it isn't allowed client, as the module may have children
|
|
154
|
+
// that are allowed clientside, and that have side-effects! (Mostly for static resources)
|
|
155
|
+
// - Surprisingly, this only increases the returned size by about 8% (probably more like 16%
|
|
156
|
+
// if we turn source maps off), so... it's fine. And with compression most of the extra
|
|
157
|
+
// size will go away, as paths are highly repetitive.
|
|
158
|
+
// - And now it increases the size by much less, as we ignore any subtree which are entirely
|
|
159
|
+
// not allowed on the client.
|
|
160
|
+
for (let request in module.requires) {
|
|
161
|
+
let requireResolvedPath = module.requires[request];
|
|
162
|
+
let requiredModule = require.cache[requireResolvedPath];
|
|
163
|
+
|
|
164
|
+
if (requiredModule) {
|
|
165
|
+
addModule(requiredModule);
|
|
166
|
+
moduleObj.requests[request] = requiredModule.filename;
|
|
167
|
+
} else {
|
|
168
|
+
moduleObj.requests[request] = "";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let searchPaths: string[] = [];
|
|
174
|
+
{
|
|
175
|
+
searchPaths.push(this.rootResolvePath);
|
|
176
|
+
let pathParts = this.rootResolvePath.replaceAll("\\", "/").split("/");
|
|
177
|
+
for (let i = 0; i < pathParts.length; i++) {
|
|
178
|
+
// Skip empty path parts, to preventing the case where the path ends
|
|
179
|
+
// with a /, which would result in "D:/test//node_modules"
|
|
180
|
+
if (!pathParts[i]) continue;
|
|
181
|
+
searchPaths.push(pathParts.slice(0, i + 1).join("/") + "/node_modules");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
let requestsResolvedPaths: string[] = [];
|
|
187
|
+
for (let pathRequest of pathRequests) {
|
|
188
|
+
let resolvedPath = "";
|
|
189
|
+
try {
|
|
190
|
+
resolvedPath = require.resolve(pathRequest, { paths: searchPaths });
|
|
191
|
+
} catch { }
|
|
192
|
+
requestsResolvedPaths.push(resolvedPath);
|
|
193
|
+
|
|
194
|
+
function createNotFoundModule(error: string): NodeJS.Module {
|
|
195
|
+
console.warn(error);
|
|
196
|
+
return {
|
|
197
|
+
exports: {},
|
|
198
|
+
children: [],
|
|
199
|
+
filename: resolvedPath,
|
|
200
|
+
id: resolvedPath,
|
|
201
|
+
isPreloading: false,
|
|
202
|
+
require: null as any,
|
|
203
|
+
loaded: true,
|
|
204
|
+
load: null as any,
|
|
205
|
+
parent: undefined,
|
|
206
|
+
path: "",
|
|
207
|
+
paths: [],
|
|
208
|
+
requires: {},
|
|
209
|
+
allowclient: true,
|
|
210
|
+
moduleContents: `console.warn(${JSON.stringify(error)})`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// TODO: We could use import() here... but that would only make the root call asynchronous,
|
|
215
|
+
// which wouldn't prevent synchronous blocking by that much anyway...
|
|
216
|
+
//require(rootPath);
|
|
217
|
+
let clientModule = require.cache[resolvedPath];
|
|
218
|
+
if (!clientModule) {
|
|
219
|
+
clientModule = createNotFoundModule(`Module ${pathRequest} (resolved to ${resolvedPath}) was not included serverside.`);
|
|
220
|
+
}
|
|
221
|
+
if (!clientModule.allowclient) {
|
|
222
|
+
clientModule = createNotFoundModule(`Module ${pathRequest} (resolved to ${resolvedPath}) is not allowed clientside (set module.allowclient in it, or call setFlag when it is imported).`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
addModule(clientModule);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { requestsResolvedPaths, modules, requireSeqNumProcessId };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function RequireControllerFactory(rootResolvePath: string) {
|
|
233
|
+
return SocketFunction.register(
|
|
234
|
+
"RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d",
|
|
235
|
+
new RequireControllerBase(rootResolvePath),
|
|
236
|
+
{
|
|
237
|
+
getModules: {},
|
|
238
|
+
requireHTML: {},
|
|
239
|
+
bufferJS: {},
|
|
240
|
+
requireJS: {},
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
}
|