|
@@ -1,105 +1,20 @@
|
|
|
import { util } from "./util";
|
|
|
import { EventEmitter } from "eventemitter3";
|
|
|
-import { SocketEventType } from "./enums";
|
|
|
-
|
|
|
-class HttpRequest {
|
|
|
- index = 1;
|
|
|
- readonly buffer: number[] = [];
|
|
|
- previousRequest: HttpRequest;
|
|
|
- private _xmlHttpRequest = new XMLHttpRequest();
|
|
|
-
|
|
|
- private _onError = sender => { };
|
|
|
- private _onSuccess = sender => { };
|
|
|
-
|
|
|
- set onError(handler) {
|
|
|
- this._onError = handler;
|
|
|
- }
|
|
|
-
|
|
|
- set onSuccess(handler) {
|
|
|
- this._onSuccess = handler;
|
|
|
- }
|
|
|
-
|
|
|
- constructor(readonly streamIndex: number, private readonly _httpUrl: string) {
|
|
|
- this._xmlHttpRequest.onerror = () => {
|
|
|
- this._onError(self);
|
|
|
- };
|
|
|
-
|
|
|
- this._xmlHttpRequest.onreadystatechange = () => {
|
|
|
- if (this.needsClearPreviousRequest()) {
|
|
|
- this.clearPreviousRequest();
|
|
|
- } else if (this.isSuccess()) {
|
|
|
- this._onSuccess(self);
|
|
|
- }
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- send(): void {
|
|
|
- this._xmlHttpRequest.open(
|
|
|
- "post",
|
|
|
- this._httpUrl + "/id?i=" + this.streamIndex,
|
|
|
- true
|
|
|
- );
|
|
|
- this._xmlHttpRequest.send(null);
|
|
|
- }
|
|
|
-
|
|
|
- abort(): void {
|
|
|
- this._xmlHttpRequest.abort();
|
|
|
- this._xmlHttpRequest = null;
|
|
|
- }
|
|
|
-
|
|
|
- isSuccess(): boolean {
|
|
|
- return (
|
|
|
- this._xmlHttpRequest.readyState > 2 &&
|
|
|
- this._xmlHttpRequest.status === 200 &&
|
|
|
- !!this._xmlHttpRequest.responseText
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- needsClearPreviousRequest(): boolean {
|
|
|
- return this._xmlHttpRequest.readyState === 2 && !!this.previousRequest;
|
|
|
- }
|
|
|
-
|
|
|
- clearPreviousRequest(): void {
|
|
|
- if (!this.previousRequest) return;
|
|
|
-
|
|
|
- this.previousRequest.abort();
|
|
|
- this.previousRequest = null;
|
|
|
- }
|
|
|
-
|
|
|
- getMessages(): string[] {
|
|
|
- return this._xmlHttpRequest.responseText.split("\n");
|
|
|
- }
|
|
|
-
|
|
|
- hasBufferedIndices(): boolean {
|
|
|
- return this.buffer.length > 0;
|
|
|
- }
|
|
|
-
|
|
|
- popBufferedIndex(): number {
|
|
|
- return this.buffer.shift();
|
|
|
- }
|
|
|
-
|
|
|
- pushIndexToBuffer(index: number) {
|
|
|
- this.buffer.push(index);
|
|
|
- }
|
|
|
-}
|
|
|
+import { SocketEventType, ServerMessageType } from "./enums";
|
|
|
|
|
|
/**
|
|
|
- * An abstraction on top of WebSockets and XHR streaming to provide fastest
|
|
|
+ * An abstraction on top of WebSockets to provide fastest
|
|
|
* possible connection for peers.
|
|
|
*/
|
|
|
export class Socket extends EventEmitter {
|
|
|
- private readonly HTTP_TIMEOUT = 25000;//ms
|
|
|
private readonly WEB_SOCKET_PING_INTERVAL = 20000;//ms
|
|
|
|
|
|
private _disconnected = false;
|
|
|
private _id: string;
|
|
|
private _messagesQueue: Array<any> = [];
|
|
|
- private _httpUrl: string;
|
|
|
private _wsUrl: string;
|
|
|
private _socket: WebSocket;
|
|
|
private _wsPingTimer: any;
|
|
|
- private _httpRequest: HttpRequest;
|
|
|
- private _timeout: any;
|
|
|
|
|
|
constructor(
|
|
|
secure: any,
|
|
@@ -107,25 +22,20 @@ export class Socket extends EventEmitter {
|
|
|
port: number,
|
|
|
path: string,
|
|
|
key: string,
|
|
|
- wsport = port
|
|
|
) {
|
|
|
super();
|
|
|
|
|
|
- const httpProtocol = secure ? "https://" : "http://";
|
|
|
const wsProtocol = secure ? "wss://" : "ws://";
|
|
|
|
|
|
- this._httpUrl = httpProtocol + host + ":" + port + path + key;
|
|
|
- this._wsUrl = wsProtocol + host + ":" + wsport + path + "peerjs?key=" + key;
|
|
|
+ this._wsUrl = wsProtocol + host + ":" + port + path + "peerjs?key=" + key;
|
|
|
}
|
|
|
|
|
|
/** Check in with ID or get one from server. */
|
|
|
start(id: string, token: string): void {
|
|
|
this._id = id;
|
|
|
|
|
|
- this._httpUrl += "/" + id + "/" + token;
|
|
|
this._wsUrl += "&id=" + id + "&token=" + token;
|
|
|
|
|
|
- this._startXhrStream();
|
|
|
this._startWebSocket();
|
|
|
}
|
|
|
|
|
@@ -146,6 +56,7 @@ export class Socket extends EventEmitter {
|
|
|
util.log("Invalid server message", event.data);
|
|
|
return;
|
|
|
}
|
|
|
+
|
|
|
this.emit(SocketEventType.Message, data);
|
|
|
};
|
|
|
|
|
@@ -153,49 +64,24 @@ export class Socket extends EventEmitter {
|
|
|
util.log("Socket closed.", event);;
|
|
|
|
|
|
this._disconnected = true;
|
|
|
+ clearTimeout(this._wsPingTimer);
|
|
|
+
|
|
|
this.emit(SocketEventType.Disconnected);
|
|
|
};
|
|
|
|
|
|
// Take care of the queue of connections if necessary and make sure Peer knows
|
|
|
// socket is open.
|
|
|
this._socket.onopen = () => {
|
|
|
- if (this._timeout) {
|
|
|
- clearTimeout(this._timeout);
|
|
|
- setTimeout(() => {
|
|
|
- this._httpRequest.abort();
|
|
|
- this._httpRequest = null;
|
|
|
- }, 5000);
|
|
|
- }
|
|
|
-
|
|
|
this._sendQueuedMessages();
|
|
|
+
|
|
|
util.log("Socket open");
|
|
|
|
|
|
- this._wsPingTimer = setTimeout(() => { this._sendHeartbeat() }, this.WEB_SOCKET_PING_INTERVAL);
|
|
|
+ this._scheduleHeartbeat();
|
|
|
};
|
|
|
}
|
|
|
|
|
|
- /** Start XHR streaming. */
|
|
|
- private _startXhrStream(streamIndex: number = 0) {
|
|
|
- const newRequest = new HttpRequest(streamIndex, this._httpUrl);
|
|
|
- this._httpRequest = newRequest;
|
|
|
-
|
|
|
- newRequest.onError = () => {
|
|
|
- // If we get an error, likely something went wrong.
|
|
|
- // Stop streaming.
|
|
|
- clearTimeout(this._timeout);
|
|
|
- this.emit(SocketEventType.Disconnected);
|
|
|
- };
|
|
|
-
|
|
|
- newRequest.onSuccess = () => {
|
|
|
- this._handleStream(newRequest);
|
|
|
- };
|
|
|
-
|
|
|
- try {
|
|
|
- newRequest.send();
|
|
|
- this._setHTTPTimeout();
|
|
|
- } catch (e) {
|
|
|
- util.log("XMLHttpRequest not available; defaulting to WebSockets");
|
|
|
- }
|
|
|
+ private _scheduleHeartbeat(): void {
|
|
|
+ this._wsPingTimer = setTimeout(() => { this._sendHeartbeat() }, this.WEB_SOCKET_PING_INTERVAL);
|
|
|
}
|
|
|
|
|
|
private _sendHeartbeat(): void {
|
|
@@ -204,67 +90,11 @@ export class Socket extends EventEmitter {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const data = { type: 'HEARTBEAT' };
|
|
|
- const message = JSON.stringify(data);
|
|
|
- this._socket.send(message);
|
|
|
-
|
|
|
- this._wsPingTimer = setTimeout(() => { this._sendHeartbeat() }, this.WEB_SOCKET_PING_INTERVAL);
|
|
|
- }
|
|
|
-
|
|
|
- /** Handles onreadystatechange response as a stream. */
|
|
|
- private _handleStream(httpRequest: HttpRequest) {
|
|
|
- // 3 and 4 are loading/done state. All others are not relevant.
|
|
|
- const messages = httpRequest.getMessages();
|
|
|
-
|
|
|
- // Check to see if anything needs to be processed on buffer.
|
|
|
-
|
|
|
- while (httpRequest.hasBufferedIndices()) {
|
|
|
- const index = httpRequest.popBufferedIndex();
|
|
|
- let bufferedMessage = messages[index];
|
|
|
+ const message = JSON.stringify({ type: ServerMessageType.Heartbeat });
|
|
|
|
|
|
- try {
|
|
|
- bufferedMessage = JSON.parse(bufferedMessage);
|
|
|
- } catch (e) {
|
|
|
- //TODO should we need to put it back?
|
|
|
- httpRequest.buffer.unshift(index);
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- this.emit(SocketEventType.Message, bufferedMessage);
|
|
|
- }
|
|
|
-
|
|
|
- let message = messages[httpRequest.index];
|
|
|
-
|
|
|
- if (message) {
|
|
|
- httpRequest.index += 1;
|
|
|
- // Buffering--this message is incomplete and we'll get to it next time.
|
|
|
- // This checks if the httpResponse ended in a `\n`, in which case the last
|
|
|
- // element of messages should be the empty string.
|
|
|
- if (httpRequest.index === messages.length) {
|
|
|
- httpRequest.pushIndexToBuffer(httpRequest.index - 1);
|
|
|
- } else {
|
|
|
- try {
|
|
|
- message = JSON.parse(message);
|
|
|
- } catch (e) {
|
|
|
- util.log("Invalid server message", message);
|
|
|
- return;
|
|
|
- }
|
|
|
- this.emit(SocketEventType.Message, message);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ this._socket.send(message);
|
|
|
|
|
|
- private _setHTTPTimeout() {
|
|
|
- this._timeout = setTimeout(() => {
|
|
|
- if (!this._wsOpen()) {
|
|
|
- const oldHttp = this._httpRequest;
|
|
|
- this._startXhrStream(oldHttp.streamIndex + 1);
|
|
|
- this._httpRequest.previousRequest = oldHttp;
|
|
|
- } else {
|
|
|
- this._httpRequest.abort();
|
|
|
- this._httpRequest = null;
|
|
|
- }
|
|
|
- }, this.HTTP_TIMEOUT);
|
|
|
+ this._scheduleHeartbeat();
|
|
|
}
|
|
|
|
|
|
/** Is the websocket currently open? */
|
|
@@ -274,7 +104,6 @@ export class Socket extends EventEmitter {
|
|
|
|
|
|
/** Send queued messages. */
|
|
|
private _sendQueuedMessages(): void {
|
|
|
- //TODO is it ok?
|
|
|
//Create copy of queue and clear it,
|
|
|
//because send method push the message back to queue if smth will go wrong
|
|
|
const copiedQueue = [...this._messagesQueue];
|
|
@@ -303,17 +132,13 @@ export class Socket extends EventEmitter {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ if (!this._wsOpen()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const message = JSON.stringify(data);
|
|
|
|
|
|
- if (this._wsOpen()) {
|
|
|
- this._socket.send(message);
|
|
|
- } else {
|
|
|
- const http = new XMLHttpRequest();
|
|
|
- const url = this._httpUrl + "/" + data.type.toLowerCase();
|
|
|
- http.open("post", url, true);
|
|
|
- http.setRequestHeader("Content-Type", "application/json");
|
|
|
- http.send(message);
|
|
|
- }
|
|
|
+ this._socket.send(message);
|
|
|
}
|
|
|
|
|
|
close(): void {
|