UNPKG

pusher-js

Version:

Pusher Channels JavaScript library for browsers, React Native, NodeJS and web workers

242 lines (219 loc) 6.18 kB
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); } }