Skip to content

Commit dc2253e

Browse files
code-asherkylecarbs
authored andcommitted
Refactor evaluations (#285)
* Replace evaluations with proxies and messages * Return proxies synchronously Otherwise events can be lost. * Ensure events cannot be missed * Refactor remaining fills * Use more up-to-date version of util For callbackify. * Wait for dispose to come back before removing This prevents issues with the "done" event not always being the last event fired. For example a socket might close and then end, but only if the caller called end. * Remove old node-pty tests * Fix emitting events twice on duplex streams * Preserve environment when spawning processes * Throw a better error if the proxy doesn't exist * Remove rimraf dependency from ide * Update net.Server.listening * Use exit event instead of killed Doesn't look like killed is even a thing. * Add response timeout to server * Fix trash * Require node-pty & spdlog after they get unpackaged This fixes an error when running in the binary. * Fix errors in down emitter preventing reconnecting * Fix disposing proxies when nothing listens to "error" event * Refactor event tests to use jest.fn() * Reject proxy call when disconnected Otherwise it'll wait for the timeout which is a waste of time since we already know the connection is dead. * Use nbin for binary packaging * Remove additional module requires * Attempt to remove require for local bootstrap-fork * Externalize fsevents
1 parent d16c6ae commit dc2253e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+5866
-6181
lines changed

build/tasks.ts

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -33,50 +33,12 @@ const buildServerBinaryPackage = register("build:server:binary:package", async (
3333
throw new Error("Cannot build binary without server bundle built");
3434
}
3535
await buildServerBinaryCopy();
36-
await dependencyNexeBinary();
37-
const resp = await runner.execute(isWin ? "npm.cmd" : "npm", ["run", "build:nexe"]);
36+
const resp = await runner.execute(isWin ? "npm.cmd" : "npm", ["run", "build:binary"]);
3837
if (resp.exitCode !== 0) {
3938
throw new Error(`Failed to package binary: ${resp.stderr}`);
4039
}
4140
});
4241

43-
const dependencyNexeBinary = register("dependency:nexe", async (runner) => {
44-
if (os.platform() === "linux" && process.env.COMPRESS === "true") {
45-
// Download the nexe binary so we can compress it before nexe runs. If we
46-
// don't want compression we don't need to do anything since nexe will take
47-
// care of getting the binary.
48-
const nexeDir = path.join(os.homedir(), ".nexe");
49-
const targetBinaryName = `${os.platform()}-${os.arch()}-${process.version.substr(1)}`;
50-
const targetBinaryPath = path.join(nexeDir, targetBinaryName);
51-
if (!fs.existsSync(targetBinaryPath)) {
52-
fse.mkdirpSync(nexeDir);
53-
runner.cwd = nexeDir;
54-
await runner.execute("wget", [`https://github.com/nexe/nexe/releases/download/v3.0.0-beta.15/${targetBinaryName}`]);
55-
await runner.execute("chmod", ["+x", targetBinaryPath]);
56-
}
57-
// Compress with upx if it doesn't already look compressed.
58-
if (fs.statSync(targetBinaryPath).size >= 20000000) {
59-
// It needs to be executable for upx to work, which it might not be if
60-
// nexe downloaded it.
61-
fs.chmodSync(targetBinaryPath, "755");
62-
const upxFolder = path.join(os.tmpdir(), "upx");
63-
const upxBinary = path.join(upxFolder, "upx");
64-
if (!fs.existsSync(upxBinary)) {
65-
fse.mkdirpSync(upxFolder);
66-
runner.cwd = upxFolder;
67-
const upxExtract = await runner.execute("bash", ["-c", "curl -L https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz | tar xJ --strip-components=1"]);
68-
if (upxExtract.exitCode !== 0) {
69-
throw new Error(`Failed to extract upx: ${upxExtract.stderr}`);
70-
}
71-
}
72-
if (!fs.existsSync(upxBinary)) {
73-
throw new Error("Not sure how, but the UPX binary does not exist");
74-
}
75-
await runner.execute(upxBinary, [targetBinaryPath]);
76-
}
77-
}
78-
});
79-
8042
const buildServerBinaryCopy = register("build:server:binary:copy", async (runner) => {
8143
const cliPath = path.join(pkgsPath, "server");
8244
const cliBuildPath = path.join(cliPath, "build");

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,12 @@
3737
"ts-loader": "^5.3.3",
3838
"ts-node": "^7.0.1",
3939
"tsconfig-paths": "^3.8.0",
40+
"tslib": "^1.9.3",
4041
"tslint": "^5.12.1",
4142
"typescript": "^3.2.2",
4243
"typescript-tslint-plugin": "^0.2.1",
4344
"uglifyjs-webpack-plugin": "^2.1.1",
45+
"util": "^0.11.1",
4446
"webpack": "^4.28.4",
4547
"webpack-bundle-analyzer": "^3.0.3",
4648
"webpack-cli": "^3.2.1",

packages/events/src/events.ts

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,51 @@
11
import { IDisposable } from "@coder/disposable";
22

33
export interface Event<T> {
4-
(listener: (e: T) => void): IDisposable;
4+
(listener: (value: T) => void): IDisposable;
5+
(id: number | string, listener: (value: T) => void): IDisposable;
56
}
67

78
/**
8-
* Emitter typecasts for a single event type.
9+
* Emitter typecasts for a single event type. You can optionally use IDs, but
10+
* using undefined with IDs will not work. If you emit without an ID, *all*
11+
* listeners regardless of their ID (or lack thereof) will receive the event.
12+
* Similarly, if you listen without an ID you will get *all* events for any or
13+
* no ID.
914
*/
1015
export class Emitter<T> {
11-
private listeners = <Array<(e: T) => void>>[];
16+
private listeners = <Array<(value: T) => void>>[];
17+
private readonly idListeners = new Map<number | string, Array<(value: T) => void>>();
1218

1319
public get event(): Event<T> {
14-
return (cb: (e: T) => void): IDisposable => {
15-
if (this.listeners) {
16-
this.listeners.push(cb);
20+
return (id: number | string | ((value: T) => void), cb?: (value: T) => void): IDisposable => {
21+
if (typeof id !== "function") {
22+
if (this.idListeners.has(id)) {
23+
this.idListeners.get(id)!.push(cb!);
24+
} else {
25+
this.idListeners.set(id, [cb!]);
26+
}
27+
28+
return {
29+
dispose: (): void => {
30+
if (this.idListeners.has(id)) {
31+
const cbs = this.idListeners.get(id)!;
32+
const i = cbs.indexOf(cb!);
33+
if (i !== -1) {
34+
cbs.splice(i, 1);
35+
}
36+
}
37+
},
38+
};
1739
}
1840

41+
cb = id;
42+
this.listeners.push(cb);
43+
1944
return {
2045
dispose: (): void => {
21-
if (this.listeners) {
22-
const i = this.listeners.indexOf(cb);
23-
if (i !== -1) {
24-
this.listeners.splice(i, 1);
25-
}
46+
const i = this.listeners.indexOf(cb!);
47+
if (i !== -1) {
48+
this.listeners.splice(i, 1);
2649
}
2750
},
2851
};
@@ -32,16 +55,45 @@ export class Emitter<T> {
3255
/**
3356
* Emit an event with a value.
3457
*/
35-
public emit(value: T): void {
36-
if (this.listeners) {
37-
this.listeners.forEach((t) => t(value));
58+
public emit(value: T): void;
59+
public emit(id: number | string, value: T): void;
60+
public emit(id: number | string | T, value?: T): void {
61+
if ((typeof id === "number" || typeof id === "string") && typeof value !== "undefined") {
62+
if (this.idListeners.has(id)) {
63+
this.idListeners.get(id)!.forEach((cb) => cb(value!));
64+
}
65+
this.listeners.forEach((cb) => cb(value!));
66+
} else {
67+
this.idListeners.forEach((cbs) => cbs.forEach((cb) => cb((id as T)!)));
68+
this.listeners.forEach((cb) => cb((id as T)!));
3869
}
3970
}
4071

4172
/**
4273
* Dispose the current events.
4374
*/
44-
public dispose(): void {
45-
this.listeners = [];
75+
public dispose(): void;
76+
public dispose(id: number | string): void;
77+
public dispose(id?: number | string): void {
78+
if (typeof id !== "undefined") {
79+
this.idListeners.delete(id);
80+
} else {
81+
this.listeners = [];
82+
this.idListeners.clear();
83+
}
84+
}
85+
86+
public get counts(): { [key: string]: number } {
87+
const counts = <{ [key: string]: number }>{};
88+
if (this.listeners.length > 0) {
89+
counts["n/a"] = this.listeners.length;
90+
}
91+
this.idListeners.forEach((cbs, id) => {
92+
if (cbs.length > 0) {
93+
counts[`${id}`] = cbs.length;
94+
}
95+
});
96+
97+
return counts;
4698
}
4799
}

packages/events/test/events.test.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { Emitter } from "../src/events";
2+
3+
describe("Event", () => {
4+
const emitter = new Emitter<number>();
5+
6+
it("should listen to global event", () => {
7+
const fn = jest.fn();
8+
const d = emitter.event(fn);
9+
emitter.emit(10);
10+
expect(fn).toHaveBeenCalledWith(10);
11+
d.dispose();
12+
});
13+
14+
it("should listen to id event", () => {
15+
const fn = jest.fn();
16+
const d = emitter.event(0, fn);
17+
emitter.emit(0, 5);
18+
expect(fn).toHaveBeenCalledWith(5);
19+
d.dispose();
20+
});
21+
22+
it("should listen to string id event", () => {
23+
const fn = jest.fn();
24+
const d = emitter.event("string", fn);
25+
emitter.emit("string", 55);
26+
expect(fn).toHaveBeenCalledWith(55);
27+
d.dispose();
28+
});
29+
30+
it("should not listen wrong id event", () => {
31+
const fn = jest.fn();
32+
const d = emitter.event(1, fn);
33+
emitter.emit(0, 5);
34+
emitter.emit(1, 6);
35+
expect(fn).toHaveBeenCalledWith(6);
36+
expect(fn).toHaveBeenCalledTimes(1);
37+
d.dispose();
38+
});
39+
40+
it("should listen to id event globally", () => {
41+
const fn = jest.fn();
42+
const d = emitter.event(fn);
43+
emitter.emit(1, 11);
44+
expect(fn).toHaveBeenCalledWith(11);
45+
d.dispose();
46+
});
47+
48+
it("should listen to global event", () => {
49+
const fn = jest.fn();
50+
const d = emitter.event(3, fn);
51+
emitter.emit(14);
52+
expect(fn).toHaveBeenCalledWith(14);
53+
d.dispose();
54+
});
55+
56+
it("should listen to id event multiple times", () => {
57+
const fn = jest.fn();
58+
const disposers = [
59+
emitter.event(934, fn),
60+
emitter.event(934, fn),
61+
emitter.event(934, fn),
62+
emitter.event(934, fn),
63+
];
64+
emitter.emit(934, 324);
65+
expect(fn).toHaveBeenCalledTimes(4);
66+
expect(fn).toHaveBeenCalledWith(324);
67+
disposers.forEach((d) => d.dispose());
68+
});
69+
70+
it("should dispose individually", () => {
71+
const fn = jest.fn();
72+
const d = emitter.event(fn);
73+
74+
const fn2 = jest.fn();
75+
const d2 = emitter.event(1, fn2);
76+
77+
d.dispose();
78+
79+
emitter.emit(12);
80+
emitter.emit(1, 12);
81+
82+
expect(fn).not.toBeCalled();
83+
expect(fn2).toBeCalledTimes(2);
84+
85+
d2.dispose();
86+
87+
emitter.emit(12);
88+
emitter.emit(1, 12);
89+
90+
expect(fn).not.toBeCalled();
91+
expect(fn2).toBeCalledTimes(2);
92+
});
93+
94+
it("should dispose by id", () => {
95+
const fn = jest.fn();
96+
emitter.event(fn);
97+
98+
const fn2 = jest.fn();
99+
emitter.event(1, fn2);
100+
101+
emitter.dispose(1);
102+
103+
emitter.emit(12);
104+
emitter.emit(1, 12);
105+
106+
expect(fn).toBeCalledTimes(2);
107+
expect(fn2).not.toBeCalled();
108+
});
109+
110+
it("should dispose all", () => {
111+
const fn = jest.fn();
112+
emitter.event(fn);
113+
emitter.event(1, fn);
114+
115+
emitter.dispose();
116+
117+
emitter.emit(12);
118+
emitter.emit(1, 12);
119+
120+
expect(fn).not.toBeCalled();
121+
});
122+
});

packages/ide-api/api.d.ts

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// tslint:disable no-any
2+
13
export interface EvalHelper { }
24
interface ActiveEvalEmitter {
35
removeAllListeners(event?: string): void;
@@ -106,7 +108,7 @@ interface IMenuItem {
106108
command: ICommandAction;
107109
alt?: ICommandAction;
108110
// when?: ContextKeyExpr;
109-
group?: 'navigation' | string;
111+
group?: "navigation" | string;
110112
order?: number;
111113
}
112114

@@ -135,23 +137,7 @@ interface ICommandRegistry {
135137
}
136138

137139
declare namespace ide {
138-
export const client: {
139-
run(func: (helper: ActiveEvalEmitter) => Disposer): ActiveEvalEmitter;
140-
run<T1>(func: (helper: ActiveEvalEmitter, a1: T1) => Disposer, a1: T1): ActiveEvalEmitter;
141-
run<T1, T2>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2) => Disposer, a1: T1, a2: T2): ActiveEvalEmitter;
142-
run<T1, T2, T3>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2, a3: T3) => Disposer, a1: T1, a2: T2, a3: T3): ActiveEvalEmitter;
143-
run<T1, T2, T3, T4>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2, a3: T3, a4: T4) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4): ActiveEvalEmitter;
144-
run<T1, T2, T3, T4, T5>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5): ActiveEvalEmitter;
145-
run<T1, T2, T3, T4, T5, T6>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6): ActiveEvalEmitter;
146-
147-
evaluate<R>(func: (helper: EvalHelper) => R | Promise<R>): Promise<R>;
148-
evaluate<R, T1>(func: (helper: EvalHelper, a1: T1) => R | Promise<R>, a1: T1): Promise<R>;
149-
evaluate<R, T1, T2>(func: (helper: EvalHelper, a1: T1, a2: T2) => R | Promise<R>, a1: T1, a2: T2): Promise<R>;
150-
evaluate<R, T1, T2, T3>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3) => R | Promise<R>, a1: T1, a2: T2, a3: T3): Promise<R>;
151-
evaluate<R, T1, T2, T3, T4>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4): Promise<R>;
152-
evaluate<R, T1, T2, T3, T4, T5>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5): Promise<R>;
153-
evaluate<R, T1, T2, T3, T4, T5, T6>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6): Promise<R>;
154-
};
140+
export const client: {};
155141

156142
export const workbench: {
157143
readonly statusbarService: IStatusbarService;
@@ -177,8 +163,8 @@ declare namespace ide {
177163
Ignore = 0,
178164
Info = 1,
179165
Warning = 2,
180-
Error = 3
181-
}
166+
Error = 3,
167+
}
182168

183169
export enum StatusbarAlignment {
184170
LEFT = 0,
@@ -229,7 +215,7 @@ declare namespace ide {
229215
declare global {
230216
interface Window {
231217
ide?: typeof ide;
232-
218+
233219
addEventListener(event: "ide-ready", callback: (ide: CustomEvent & { readonly ide: typeof ide }) => void): void;
234220
}
235221
}

packages/ide/package.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
{
22
"name": "@coder/ide",
33
"description": "Browser-based IDE client abstraction.",
4-
"main": "src/index.ts",
5-
"dependencies": {},
6-
"devDependencies": {
7-
"@types/rimraf": "^2.0.2",
8-
"rimraf": "^2.6.3"
9-
}
4+
"main": "src/index.ts"
105
}

0 commit comments

Comments
 (0)