import Util from '../util';
import * as Collections from '../utils/collections';
import { default as EventsDispatcher } from '../events/dispatcher';
import Logger from '../logger';
import TransportHooks from './transport_hooks';
import Socket from '../socket';
import Runtime from 'runtime';
import Timeline from '../timeline/timeline';
import TransportConnectionOptions from './transport_connection_options';
/** Provides universal API for transport connections.
*
* Transport connection is a low-level object that wraps a connection method
* and exposes a simple evented interface for the connection state and
* messaging. It does not implement Pusher-specific WebSocket protocol.
*
* Additionally, it fetches resources needed for transport to work and exposes
* an interface for querying transport features.
*
* States:
* - new - initial state after constructing the object
* - initializing - during initialization phase, usually fetching resources
* - intialized - ready to establish a connection
* - connection - when connection is being established
* - open - when connection ready to be used
* - closed - after connection was closed be either side
*
* Emits:
* - error - after the connection raised an error
*
* Options:
* - useTLS - whether connection should be over TLS
* - hostTLS - host to connect to when connection is over TLS
* - hostNonTLS - host to connect to when connection is over TLS
*
* @param {String} key application key
* @param {Object} options
*/
export default class TransportConnection extends EventsDispatcher {
hooks: TransportHooks;
name: string;
priority: number;
key: string;
options: TransportConnectionOptions;
state: string;
timeline: Timeline;
activityTimeout: number;
id: number;
socket: Socket;
beforeOpen: Function;
initialize: Function;
constructor(
hooks: TransportHooks,
name: string,
priority: number,
key: string,
options: TransportConnectionOptions
) {
super();
this.initialize = Runtime.transportConnectionInitializer;
this.hooks = hooks;
this.name = name;
this.priority = priority;
this.key = key;
this.options = options;
this.state = 'new';
this.timeline = options.timeline;
this.activityTimeout = options.activityTimeout;
this.id = this.timeline.generateUniqueID();
}
/** Checks whether the transport handles activity checks by itself.
*
* @return {Boolean}
*/
handlesActivityChecks(): boolean {
return Boolean(this.hooks.handlesActivityChecks);
}
/** Checks whether the transport supports the ping/pong API.
*
* @return {Boolean}
*/
supportsPing(): boolean {
return Boolean(this.hooks.supportsPing);
}
/** Tries to establish a connection.
*
* @returns {Boolean} false if transport is in invalid state
*/
connect(): boolean {
if (this.socket || this.state !== 'initialized') {
return false;
}
var url = this.hooks.urls.getInitial(this.key, this.options);
try {
this.socket = this.hooks.getSocket(url, this.options);
} catch (e) {
Util.defer(() => {
this.onError(e);
this.changeState('closed');
});
return false;
}
this.bindListeners();
Logger.debug('Connecting', { transport: this.name, url });
this.changeState('connecting');
return true;
}
/** Closes the connection.
*
* @return {Boolean} true if there was a connection to close
*/
close(): boolean {
if (this.socket) {
this.socket.close();
return true;
} else {
return false;
}
}
/** Sends data over the open connection.
*
* @param {String} data
* @return {Boolean} true only when in the "open" state
*/
send(data: any): boolean {
if (this.state === 'open') {
// Workaround for MobileSafari bug (see https://gist.github.com/2052006)
Util.defer(() => {
if (this.socket) {
this.socket.send(data);
}
});
return true;
} else {
return false;
}
}
/** Sends a ping if the connection is open and transport supports it. */
ping() {
if (this.state === 'open' && this.supportsPing()) {
this.socket.ping();
}
}
private onOpen() {
if (this.hooks.beforeOpen) {
this.hooks.beforeOpen(
this.socket,
this.hooks.urls.getPath(this.key, this.options)
);
}
this.changeState('open');
this.socket.onopen = undefined;
}
private onError(error) {
this.emit('error', { type: 'WebSocketError', error: error });
this.timeline.error(this.buildTimelineMessage({ error: error.toString() }));
}
private onClose(closeEvent?: any) {
if (closeEvent) {
this.changeState('closed', {
code: closeEvent.code,
reason: closeEvent.reason,
wasClean: closeEvent.wasClean
});
} else {
this.changeState('closed');
}
this.unbindListeners();
this.socket = undefined;
}
private onMessage(message) {
this.emit('message', message);
}
private onActivity() {
this.emit('activity');
}
private bindListeners() {
this.socket.onopen = () => {
this.onOpen();
};
this.socket.onerror = error => {
this.onError(error);
};
this.socket.onclose = closeEvent => {
this.onClose(closeEvent);
};
this.socket.onmessage = message => {
this.onMessage(message);
};
if (this.supportsPing()) {
this.socket.onactivity = () => {
this.onActivity();
};
}
}
private unbindListeners() {
if (this.socket) {
this.socket.onopen = undefined;
this.socket.onerror = undefined;
this.socket.onclose = undefined;
this.socket.onmessage = undefined;
if (this.supportsPing()) {
this.socket.onactivity = undefined;
}
}
}
private changeState(state: string, params?: any) {
this.state = state;
this.timeline.info(
this.buildTimelineMessage({
state: state,
params: params
})
);
this.emit(state, params);
}
buildTimelineMessage(message): any {
return Collections.extend({ cid: this.id }, message);
}
}
|