UNPKG

@iosio/websocket-client

Version:

Simple wrapper for native client-side WebSocket. Similar to socket.io. Easy to use with any backend

456 lines (380 loc) 13.7 kB
import {isFunction, isString, isObject} from "@iosio/utils/lib/type_checks"; import {Eventer} from '@iosio/utils/lib/eventer'; import {tryParse} from '@iosio/utils/lib/string_manipulation'; import {uniqueID} from '@iosio/utils/lib/number_generation'; /* webSocket readyStates 0 - connecting 1 - open 2 - closing 3 - closed */ export default class Socket { /** * Creates an instance of Socket. * @param {Object} config - the initial configuration for the module * @memberof Socket */ constructor(config) { if (!config) { console.error('missing required config object for socket. required config:'); return; } const { websocket, url, websocket_options, auto_reconnect, requestMapper, sendMapper, should_console_log } = config; if (!url) { console.error('missing required url for socket'); } this._url = url ? url : null; this._auto_reconnect = auto_reconnect ? auto_reconnect : false; this._websocket_options = websocket_options; this._requestMapper = null; if (isFunction(requestMapper)) { this._requestMapper = requestMapper; } this._sendMapper = false; if (isFunction(sendMapper)) { this._sendMapper = sendMapper; } this._reconnectTimeout = null; this._deliberateClose = false; this._should_console_log = should_console_log ? should_console_log : false; this._WebSocket = websocket ? websocket : WebSocket; this._socket = null; this.CONNECT = 'connect'; this.DISCONNECT = 'disconnect'; this.RECONNECTING = 'reconnecting'; this.ERROR = 'error'; this._callbacks = Object.create(null); this._eventer = Eventer(this._callbacks); this._reconnectionInProgress = false; } /** * Handles emitting when the socket is successfully connected to a websocket server * @memberof Socket * @returns {undefined} */ _onOpen = () => { if (this._reconnectTimeout) { clearTimeout(this._reconnectTimeout); } this._log('socket connected'); this._eventer.emit(this.CONNECT); }; /** * Handles emitting when there is an error * @memberof Socket * @returns {undefined} */ _onError = () => { this._log('socket error'); this._eventer.emit(this.ERROR); }; /** * Handles emitting when the socket's connection is closed. * It will also handle auto connection if the connection is not deliberately closed * and auto connect is enabled * @memberof Socket * @returns {undefined} * * * 1000 - CLOSE_NORMAL * 1006 - CLOSE_ABNORMAL * 1007 - unsupported payload * 1011 - Server error (Internal server error while operating) * 1012 - Server/service is restarting * 1014 - Bad gateway * ... * https://github.com/Luka967/websocket-close-codes * */ _onClose = (e) => { // 1000: // CLOSE_NORMAL this._log('socket closed. error code: ', e.code); this._eventer.emit(this.DISCONNECT); // this._socket.onclose = this._socket.onopen = this._socket.onerror = null; if (this._auto_reconnect && !this._deliberateClose) { this._attemptReconnect(); } }; /** * Will close the websocket connection deliberately * @memberof Socket * @returns {undefined} */ close = () => { this._deliberateClose = true; //clean up rest // this._reconnectionInProgress = false; if (this._reconnectTimeout) { clearInterval(this._reconnectTimeout); } if (this._isConnected()) { this._log('closing socket'); this._socket && this._socket.close(); } else { this._log('socket is already closed') } }; /** * Attempts to reconnect on a periodical basis * @memberof Socket * @returns {undefined} */ _attemptReconnect = () => { const time = this._auto_reconnect && this._auto_reconnect.every ? this._auto_reconnect.every : 2000; if (this._reconnectTimeout) { clearTimeout(this._reconnectTimeout); } this._log(`attempting reconnect in: ${time}ms`); this._reconnectTimeout = setTimeout(() => { this._log('reconnecting'); this._eventer.emit(this.RECONNECTING); !this._isConnected() && this.open(); }, time); }; /** * Checks if the received message is in the correct format * @param {Object} data - the received data object * @memberof Socket * @returns {Object} - is the received message valid */ _validateReceivedMessage = (data) => { const parsed = tryParse(data);// returns {ok,data,error} if (!parsed.ok) { this._log('data received from socket does not have valid format. instead received:', parsed.data, true); return {ok: false}; } if (!parsed.data.event || typeof parsed.data.event !== 'string') { this._log('received messages are required to have an event property of type string', '', true); return {ok: false} } let message = parsed.data; if (!this._callbacks[message.event]) { this._log('no handler exists from this event received from socket:', message.event, true); return {ok: false} } return {ok: true, message} }; /** * Handles emitting when a message is received * @param {Object} * - the message received * @property {Object} data the data received from the message * @memberof Socket * @returns {undefined} */ _onMessage = ({data}) => { const validation = this._validateReceivedMessage(data); if (!validation.ok) { return; } const {message} = validation; this._eventer.emit(message.event, message.data); this._isResponse(message.event) && this._eventer.destroy(message.event); }; /** * Checks if the websocket is connected * @memberof Socket * @returns {Boolean} - is the socket connected */ _isConnected = () => { return this._socket ? (this._socket.readyState === this._WebSocket.OPEN) : false; }; /** * Checks if the message is a response for a named callback emittion * @param {*} res - the response * @returns {Boolean} - is the data a response */ _isResponse = (res) => isString(res) && res.search('@response-') > -1; /** * Handles opening a websocket connection to the configured destination * @memberof Socket * @returns {undefined} */ open = () => { this._deliberateClose = false; if (this._reconnectTimeout) { clearInterval(this._reconnectTimeout); } this._log('initializing socket'); if (!this._isConnected()) { try { this._socket = new this._WebSocket(this._url, this._websocket_options); } catch (e) { this._log('error instantiating WebSocket', e, true); this._socket = false; } if (!this._socket) { return; } this._socket.onopen = this._onOpen; this._socket.onerror = this._onError; this._socket.onmessage = this._onMessage; this._socket.onclose = this._onClose; } else { this._log('socket already open'); } }; /** * Checks if the arguments for an event are valid * @param {String} event - the name of the event * @param {Function} cb - the callback * @memberof Socket * @returns {Object} - are the arguments valid */ _isValidOnEventArgs = (event, cb) => { if (!isString(event)) { this._log('Must provide a string for the event type.', '', true); return false; } if (!isFunction(cb)) { this._log('Must provide a function for a callback. Must be named function if you want to remove its listener', '', true); return false; } return true; }; /** * Registers an event to be listened for when the websocket client recieves an event from its connected server * @param {String} event - the name of the event to listen to * @param {Function} cb - the action to take when the event is received * @memberof Socket * @returns {undefined} */ on = (event, cb) => { this._isValidOnEventArgs(event, cb) && this._eventer.on(event, cb); }; /** * Unregisters an event listener * @param {String} event - the event name * @param {Function} cb - the callback * @memberof Socket * @returns {undefined} */ off = (event, cb) => { if (this._isValidOnEventArgs(event, cb) && cb.name) { this._eventer.off(event, cb); } else { this.log('callback function passed to .off must also be a named function (not anonymous) '); } }; /** * Sends a message to the connected websocket server * @param {String} event - the event to send * @param {Object} data - the data to send with the event * @param {String} response_id - the response id * @memberof Socket * @returns {undefined} */ send = (event, data = {}, response_id) => { if (!this._isConnected()) { return; } if (!isString(event)) { this._log('.send must provide a string for the event type.', '', true); return; } let is_request = isString(response_id); let message; if (is_request) { message = { event, data, type: 'request', response_id }; if (isFunction(this._requestMapper)) { message = this._requestMapper(message); } } else { message = { event, data, type: 'send' }; if (isFunction(this._sendMapper)) { message = this._sendMapper(message); } } this._socket.send(JSON.stringify(message)); }; /** * Validates the params of a request * @param {String} event - the event to validate * @param {Object|Function} params_or_cb_if_no_params - either the options or the callback * @param {Function} cb_if_params - the callback if there are options * @memberof Socket * @returns {Object} - is the request valid */ _validateRequestArgs = (event, params_or_cb_if_no_params, cb_if_params) => { let ok = true; if (!isString(event)) { this._log('/request method: a string event name is required', '', true); ok = false; } if (!isObject(params_or_cb_if_no_params) && !isFunction(params_or_cb_if_no_params)) { this._log('/request method: a params object or callback function is required on the second parameter', '', true); ok = false; } if (isObject(params_or_cb_if_no_params) && !isFunction(cb_if_params)) { this._log('/request method: a callback function is required as a third parameter if a param object is passed as the second', '', true); ok = false; } if (!ok) { return {ok: false,}; } if (isObject(params_or_cb_if_no_params)) { return { ok: true, params: params_or_cb_if_no_params, cb: cb_if_params, }; } else if (isFunction(params_or_cb_if_no_params)) { return { ok: true, params: {}, cb: params_or_cb_if_no_params, }; } return {ok: false,}; }; /** * Creates a named request to the server, the difference between this and send, is that this function * will also register an event listener for the next response based on the name of this event * @param {String} event - the name of the event * @param {Object|Function} params_or_cb_if_no_params - either params or a callback * @param {Function} cb_if_params - params if there is a callback * @memberof Socket * @returns {undefined} */ request = (event, params_or_cb_if_no_params, cb_if_params) => { let {ok, params, cb,} = this._validateRequestArgs(event, params_or_cb_if_no_params, cb_if_params); if (!ok) { this._log('request arguments are invalid', '', true); return; } const response_id = '@response-' + event + '-' + uniqueID(); this._log('requesting'); this.send(event, params, response_id); this.on(response_id, cb, true); }; /** * Handles logging if logging is enabled * @param {String} msg - the message to log * @param {String} arg - any arguments to log with the message * @param {Boolean} error - is this an error * @memberof Socket * @returns {undefined} */ _log = (msg, arg = "", error) => { if (this._should_console_log) { error ? console.error('Socket.js: ' + msg, arg) : console.info('Socket.js: ' + msg, arg); } }; }