Files
code-server/packages/ide/src/fill/client.ts
Asher dc2253e718 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
2019-03-26 13:01:25 -05:00

139 lines
3.6 KiB
TypeScript

import { Emitter } from "@coder/events";
import { field, logger } from "@coder/logger";
import { Client, ReadWriteConnection } from "@coder/protocol";
import { retry } from "../retry";
/**
* A connection based on a web socket. Automatically reconnects and buffers
* messages during connection.
*/
class WebsocketConnection implements ReadWriteConnection {
private activeSocket: WebSocket | undefined;
private readonly messageBuffer = <Uint8Array[]>[];
private readonly socketTimeoutDelay = 60 * 1000;
private readonly retryName = "Socket";
private isUp: boolean = false;
private closed: boolean = false;
private readonly messageEmitter = new Emitter<Uint8Array>();
private readonly closeEmitter = new Emitter<void>();
private readonly upEmitter = new Emitter<void>();
private readonly downEmitter = new Emitter<void>();
public readonly onUp = this.upEmitter.event;
public readonly onClose = this.closeEmitter.event;
public readonly onDown = this.downEmitter.event;
public readonly onMessage = this.messageEmitter.event;
public constructor() {
retry.register(this.retryName, () => this.connect());
retry.block(this.retryName);
retry.run(this.retryName);
}
public send(data: Buffer | Uint8Array): void {
if (this.closed) {
throw new Error("web socket is closed");
}
if (!this.activeSocket || this.activeSocket.readyState !== this.activeSocket.OPEN) {
this.messageBuffer.push(data);
} else {
this.activeSocket.send(data);
}
}
public close(): void {
this.closed = true;
this.dispose();
this.closeEmitter.emit();
}
/**
* Connect to the server.
*/
private async connect(): Promise<void> {
const socket = await this.openSocket();
socket.addEventListener("message", (event: MessageEvent) => {
this.messageEmitter.emit(event.data);
});
socket.addEventListener("close", (event) => {
if (this.isUp) {
this.isUp = false;
try {
this.downEmitter.emit(undefined);
} catch (error) {
// Don't let errors here prevent restarting.
logger.error(error.message);
}
}
logger.warn(
"Web socket closed",
field("code", event.code),
field("reason", event.reason),
field("wasClean", event.wasClean),
);
if (!this.closed) {
retry.block(this.retryName);
retry.run(this.retryName);
}
});
// Send any messages that were queued while we were waiting to connect.
while (this.messageBuffer.length > 0) {
socket.send(this.messageBuffer.shift()!);
}
if (!this.isUp) {
this.isUp = true;
this.upEmitter.emit(undefined);
}
}
/**
* Open a web socket, disposing the previous connection if any.
*/
private async openSocket(): Promise<WebSocket> {
this.dispose();
const wsProto = location.protocol === "https:" ? "wss" : "ws";
const socket = new WebSocket(
`${wsProto}://${location.host}${location.pathname}`,
);
socket.binaryType = "arraybuffer";
this.activeSocket = socket;
const socketWaitTimeout = window.setTimeout(() => {
socket.close();
}, this.socketTimeoutDelay);
await new Promise((resolve, reject): void => {
const onClose = (): void => {
clearTimeout(socketWaitTimeout);
socket.removeEventListener("close", onClose);
reject();
};
socket.addEventListener("close", onClose);
socket.addEventListener("open", async () => {
clearTimeout(socketWaitTimeout);
resolve();
});
});
return socket;
}
/**
* Dispose the current connection.
*/
private dispose(): void {
if (this.activeSocket) {
this.activeSocket.close();
}
}
}
// Global instance so all fills can use the same client.
export const client = new Client(new WebsocketConnection());