Skip to content

Commit cff746d

Browse files
committed
Add evaluate
1 parent 32294a5 commit cff746d

23 files changed

Lines changed: 5257 additions & 2 deletions

packages/disposable/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./disposable";

packages/events/src/events.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { IDisposable } from "@coder/disposable";
2+
3+
export interface Event<T> {
4+
(listener: (e: T) => void): IDisposable;
5+
}
6+
7+
/**
8+
* Emitter typecasts for a single event type
9+
*/
10+
export class Emitter<T> {
11+
12+
private listeners: Array<(e: T) => void> | undefined;
13+
14+
public constructor() {
15+
this.listeners = [];
16+
}
17+
18+
public get event(): Event<T> {
19+
return (cb: (e: T) => void): IDisposable => {
20+
if (this.listeners) {
21+
this.listeners.push(cb);
22+
}
23+
24+
return {
25+
dispose: (): void => {
26+
if (this.listeners) {
27+
const i = this.listeners.indexOf(cb);
28+
if (i !== -1) {
29+
this.listeners.splice(i, 1);
30+
}
31+
}
32+
},
33+
};
34+
};
35+
}
36+
37+
/**
38+
* Emit a value
39+
*/
40+
public emit(value: T): void {
41+
if (this.listeners) {
42+
this.listeners.forEach((t) => t(value));
43+
}
44+
}
45+
46+
/**
47+
* Disposes the event emitter
48+
*/
49+
public dispose(): void {
50+
this.listeners = undefined;
51+
}
52+
53+
/**
54+
* Whether the event has listeners.
55+
*/
56+
public get hasListeners(): boolean {
57+
return !!this.listeners && this.listeners.length > 0;
58+
}
59+
60+
}

packages/events/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./events";

packages/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
],
2828
"moduleNameMapper": {
2929
"^.+\\.(s?css|png|svg)$": "<rootDir>/scripts/src/dummy.js",
30-
"@coder/(.*)/testing": "<rootDir>/packages/$1/testing",
31-
"@coder/(.*)": "<rootDir>/packages/$1/src"
30+
"@coder/(.*)/testing": "<rootDir>/$1/testing",
31+
"@coder/(.*)": "<rootDir>/$1/src"
3232
},
3333
"transform": {
3434
"^.+\\.tsx?$": "ts-jest"

packages/server/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "server",
3+
"dependencies": {
4+
"express": "^4.16.4",
5+
"ws": "^6.1.2"
6+
},
7+
"devDependencies": {
8+
"@types/express": "^4.16.0",
9+
"@types/ws": "^6.0.1",
10+
"ts-protoc-gen": "^0.8.0"
11+
}
12+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
3+
protoc --plugin="protoc-gen-ts=./node_modules/.bin/protoc-gen-ts" --js_out="import_style=commonjs,binary:./src/proto" --ts_out="./src/proto" ./src/proto/*.proto --proto_path="./src/proto"
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { ReadWriteConnection } from "../common/connection";
2+
import { NewEvalMessage, ServerMessage, EvalDoneMessage, EvalFailedMessage, TypedValue, ClientMessage } from "../proto";
3+
import { Emitter } from "@coder/events";
4+
import { logger, field } from "@coder/logger";
5+
6+
7+
export class Client {
8+
9+
private evalId: number = 0;
10+
private evalDoneEmitter: Emitter<EvalDoneMessage> = new Emitter();
11+
private evalFailedEmitter: Emitter<EvalFailedMessage> = new Emitter();
12+
13+
public constructor(
14+
private readonly connection: ReadWriteConnection,
15+
) {
16+
connection.onMessage((data) => {
17+
try {
18+
this.handleMessage(ServerMessage.deserializeBinary(data));
19+
} catch (ex) {
20+
logger.error("Failed to handle server message", field("length", data.byteLength), field("exception", ex));
21+
}
22+
});
23+
}
24+
25+
public evaluate<R>(func: () => R): Promise<R>;
26+
public evaluate<R, T1>(func: (a1: T1) => R, a1: T1): Promise<R>;
27+
public evaluate<R, T1, T2>(func: (a1: T1, a2: T2) => R, a1: T1, a2: T2): Promise<R>;
28+
public evaluate<R, T1, T2, T3>(func: (a1: T1, a2: T2, a3: T3) => R, a1: T1, a2: T2, a3: T3): Promise<R>;
29+
public evaluate<R, T1, T2, T3, T4>(func: (a1: T1, a2: T2, a3: T3, a4: T4) => R, a1: T1, a2: T2, a3: T3, a4: T4): Promise<R>;
30+
public evaluate<R, T1, T2, T3, T4, T5>(func: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => R, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5): Promise<R>;
31+
public evaluate<R, T1, T2, T3, T4, T5, T6>(func: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => R, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6): Promise<R>;
32+
public evaluate<R, T1, T2, T3, T4, T5, T6>(func: (a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6) => R, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6): Promise<R> {
33+
const newEval = new NewEvalMessage();
34+
const id = this.evalId++;
35+
newEval.setId(id);
36+
newEval.setArgsList([a1, a2, a3, a4, a5, a6].filter(a => a).map(a => JSON.stringify(a)));
37+
newEval.setFunction(func.toString());
38+
39+
const clientMsg = new ClientMessage();
40+
clientMsg.setNewEval(newEval);
41+
this.connection.send(clientMsg.serializeBinary());
42+
43+
let res: (value?: R) => void;
44+
let rej: (err?: any) => void;
45+
const prom = new Promise<R>((r, e) => {
46+
res = r;
47+
rej = e;
48+
});
49+
50+
const d1 = this.evalDoneEmitter.event((doneMsg) => {
51+
if (doneMsg.getId() === id) {
52+
d1.dispose();
53+
d2.dispose();
54+
55+
const resp = doneMsg.getResponse();
56+
if (!resp) {
57+
res();
58+
59+
return;
60+
}
61+
62+
const rt = resp.getType();
63+
let val: any;
64+
switch (rt) {
65+
case TypedValue.Type.BOOLEAN:
66+
val = resp.getValue() === "true";
67+
break;
68+
case TypedValue.Type.NUMBER:
69+
val = parseInt(resp.getValue(), 10);
70+
break;
71+
case TypedValue.Type.OBJECT:
72+
val = JSON.parse(resp.getValue());
73+
break;
74+
case TypedValue.Type.STRING:
75+
val = resp.getValue();
76+
break;
77+
default:
78+
throw new Error(`unsupported typed value ${rt}`);
79+
}
80+
81+
res(val);
82+
}
83+
});
84+
85+
const d2 = this.evalFailedEmitter.event((failedMsg) => {
86+
if (failedMsg.getId() === id) {
87+
d1.dispose();
88+
d2.dispose();
89+
90+
rej(failedMsg.getMessage());
91+
}
92+
});
93+
94+
return prom;
95+
}
96+
97+
private handleMessage(message: ServerMessage): void {
98+
if (message.hasEvalDone()) {
99+
this.evalDoneEmitter.emit(message.getEvalDone()!);
100+
} else if (message.hasEvalFailed()) {
101+
this.evalFailedEmitter.emit(message.getEvalFailed()!);
102+
}
103+
}
104+
105+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export interface SendableConnection {
2+
send(data: Buffer | Uint8Array): void;
3+
}
4+
5+
export interface ReadWriteConnection extends SendableConnection {
6+
onMessage(cb: (data: Uint8Array | Buffer) => void): void;
7+
onClose(cb: () => void): void;
8+
close(): void;
9+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as vm from "vm";
2+
import { NewEvalMessage, TypedValue, EvalFailedMessage, EvalDoneMessage, ServerMessage } from "../proto";
3+
import { SendableConnection } from "../common/connection";
4+
5+
export const evaluate = async (connection: SendableConnection, message: NewEvalMessage): Promise<void> => {
6+
const argStr: string[] = [];
7+
message.getArgsList().forEach((value) => {
8+
argStr.push(value);
9+
});
10+
const sendResp = (resp: any): void => {
11+
const evalDone = new EvalDoneMessage();
12+
evalDone.setId(message.getId());
13+
const tof = typeof resp;
14+
if (tof !== "undefined") {
15+
const tv = new TypedValue();
16+
let t: TypedValue.Type;
17+
switch (tof) {
18+
case "string":
19+
t = TypedValue.Type.STRING;
20+
break;
21+
case "boolean":
22+
t = TypedValue.Type.BOOLEAN;
23+
break;
24+
case "object":
25+
t = TypedValue.Type.OBJECT;
26+
break;
27+
case "number":
28+
t = TypedValue.Type.NUMBER;
29+
break;
30+
default:
31+
sendErr(EvalFailedMessage.Reason.EXCEPTION, `unsupported response type ${tof}`);
32+
return;
33+
}
34+
tv.setValue(tof === "string" ? resp : JSON.stringify(resp));
35+
tv.setType(t);
36+
evalDone.setResponse(tv);
37+
}
38+
39+
const serverMsg = new ServerMessage();
40+
serverMsg.setEvalDone(evalDone);
41+
connection.send(serverMsg.serializeBinary());
42+
};
43+
const sendErr = (reason: EvalFailedMessage.Reason, msg: string): void => {
44+
const evalFailed = new EvalFailedMessage();
45+
evalFailed.setId(message.getId());
46+
evalFailed.setReason(reason);
47+
evalFailed.setMessage(msg);
48+
49+
const serverMsg = new ServerMessage();
50+
serverMsg.setEvalFailed(evalFailed);
51+
connection.send(serverMsg.serializeBinary());
52+
};
53+
try {
54+
const value = vm.runInNewContext(`(${message.getFunction()})(${argStr.join(",")})`, { require }, {
55+
timeout: message.getTimeout() || 30000,
56+
});
57+
let responder: any = value;
58+
if (value instanceof Promise) {
59+
responder = await value;
60+
}
61+
sendResp(responder);
62+
} catch (ex) {
63+
sendErr(EvalFailedMessage.Reason.EXCEPTION, ex.toString());
64+
}
65+
};

packages/server/src/node/server.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { logger, field } from "@coder/logger";
2+
import { ClientMessage } from "../proto";
3+
import { evaluate } from "./evaluate";
4+
import { ReadWriteConnection } from "../common/connection";
5+
6+
export class Server {
7+
8+
public constructor(
9+
private readonly connection: ReadWriteConnection,
10+
) {
11+
connection.onMessage((data) => {
12+
try {
13+
this.handleMessage(ClientMessage.deserializeBinary(data));
14+
} catch (ex) {
15+
logger.error("Failed to handle client message", field("length", data.byteLength), field("exception", ex));
16+
}
17+
});
18+
}
19+
20+
private handleMessage(message: ClientMessage): void {
21+
if (message.hasNewEval()) {
22+
evaluate(this.connection, message.getNewEval()!);
23+
}
24+
}
25+
26+
}

0 commit comments

Comments
 (0)