UNPKG

mock-socket

Version:

Javascript mocking library for websockets and socket.io

1,724 lines (1,434 loc) 185 kB
var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; /** * Check if we're required to add a port number. * * @see https://url.spec.whatwg.org/#default-port * @param {Number|String} port Port number we need to check * @param {String} protocol Protocol we need to check against. * @returns {Boolean} Is it a default port for the given protocol * @api private */ var requiresPort = function required(port, protocol) { protocol = protocol.split(':')[0]; port = +port; if (!port) { return false; } switch (protocol) { case 'http': case 'ws': return port !== 80; case 'https': case 'wss': return port !== 443; case 'ftp': return port !== 21; case 'gopher': return port !== 70; case 'file': return false; } return port !== 0; }; var has = Object.prototype.hasOwnProperty; var undef; /** * Decode a URI encoded string. * * @param {String} input The URI encoded string. * @returns {String} The decoded string. * @api private */ function decode(input) { return decodeURIComponent(input.replace(/\+/g, ' ')); } /** * Simple query string parser. * * @param {String} query The query string that needs to be parsed. * @returns {Object} * @api public */ function querystring(query) { var parser = /([^=?&]+)=?([^&]*)/g , result = {} , part; while (part = parser.exec(query)) { var key = decode(part[1]) , value = decode(part[2]); // // Prevent overriding of existing properties. This ensures that build-in // methods like `toString` or __proto__ are not overriden by malicious // querystrings. // if (key in result) { continue; } result[key] = value; } return result; } /** * Transform a query string to an object. * * @param {Object} obj Object that should be transformed. * @param {String} prefix Optional prefix. * @returns {String} * @api public */ function querystringify(obj, prefix) { prefix = prefix || ''; var pairs = [] , value , key; // // Optionally prefix with a '?' if needed // if ('string' !== typeof prefix) { prefix = '?'; } for (key in obj) { if (has.call(obj, key)) { value = obj[key]; // // Edge cases where we actually want to encode the value to an empty // string instead of the stringified value. // if (!value && (value === null || value === undef || isNaN(value))) { value = ''; } pairs.push(encodeURIComponent(key) +'='+ encodeURIComponent(value)); } } return pairs.length ? prefix + pairs.join('&') : ''; } // // Expose the module. // var stringify = querystringify; var parse = querystring; var querystringify_1 = { stringify: stringify, parse: parse }; var protocolre = /^([a-z][a-z0-9.+-]*:)?(\/\/)?([\S\s]*)/i; var slashes = /^[A-Za-z][A-Za-z0-9+-.]*:\/\//; /** * These are the parse rules for the URL parser, it informs the parser * about: * * 0. The char it Needs to parse, if it's a string it should be done using * indexOf, RegExp using exec and NaN means set as current value. * 1. The property we should set when parsing this value. * 2. Indication if it's backwards or forward parsing, when set as number it's * the value of extra chars that should be split off. * 3. Inherit from location if non existing in the parser. * 4. `toLowerCase` the resulting value. */ var rules = [ ['#', 'hash'], // Extract from the back. ['?', 'query'], // Extract from the back. function sanitize(address) { // Sanitize what is left of the address return address.replace('\\', '/'); }, ['/', 'pathname'], // Extract from the back. ['@', 'auth', 1], // Extract from the front. [NaN, 'host', undefined, 1, 1], // Set left over value. [/:(\d+)$/, 'port', undefined, 1], // RegExp the back. [NaN, 'hostname', undefined, 1, 1] // Set left over. ]; /** * These properties should not be copied or inherited from. This is only needed * for all non blob URL's as a blob URL does not include a hash, only the * origin. * * @type {Object} * @private */ var ignore = { hash: 1, query: 1 }; /** * The location object differs when your code is loaded through a normal page, * Worker or through a worker using a blob. And with the blobble begins the * trouble as the location object will contain the URL of the blob, not the * location of the page where our code is loaded in. The actual origin is * encoded in the `pathname` so we can thankfully generate a good "default" * location from it so we can generate proper relative URL's again. * * @param {Object|String} loc Optional default location object. * @returns {Object} lolcation object. * @public */ function lolcation(loc) { var globalVar; if (typeof window !== 'undefined') { globalVar = window; } else if (typeof commonjsGlobal !== 'undefined') { globalVar = commonjsGlobal; } else if (typeof self !== 'undefined') { globalVar = self; } else { globalVar = {}; } var location = globalVar.location || {}; loc = loc || location; var finaldestination = {} , type = typeof loc , key; if ('blob:' === loc.protocol) { finaldestination = new Url(unescape(loc.pathname), {}); } else if ('string' === type) { finaldestination = new Url(loc, {}); for (key in ignore) { delete finaldestination[key]; } } else if ('object' === type) { for (key in loc) { if (key in ignore) { continue; } finaldestination[key] = loc[key]; } if (finaldestination.slashes === undefined) { finaldestination.slashes = slashes.test(loc.href); } } return finaldestination; } /** * @typedef ProtocolExtract * @type Object * @property {String} protocol Protocol matched in the URL, in lowercase. * @property {Boolean} slashes `true` if protocol is followed by "//", else `false`. * @property {String} rest Rest of the URL that is not part of the protocol. */ /** * Extract protocol information from a URL with/without double slash ("//"). * * @param {String} address URL we want to extract from. * @return {ProtocolExtract} Extracted information. * @private */ function extractProtocol(address) { var match = protocolre.exec(address); return { protocol: match[1] ? match[1].toLowerCase() : '', slashes: !!match[2], rest: match[3] }; } /** * Resolve a relative URL pathname against a base URL pathname. * * @param {String} relative Pathname of the relative URL. * @param {String} base Pathname of the base URL. * @return {String} Resolved pathname. * @private */ function resolve(relative, base) { var path = (base || '/').split('/').slice(0, -1).concat(relative.split('/')) , i = path.length , last = path[i - 1] , unshift = false , up = 0; while (i--) { if (path[i] === '.') { path.splice(i, 1); } else if (path[i] === '..') { path.splice(i, 1); up++; } else if (up) { if (i === 0) { unshift = true; } path.splice(i, 1); up--; } } if (unshift) { path.unshift(''); } if (last === '.' || last === '..') { path.push(''); } return path.join('/'); } /** * The actual URL instance. Instead of returning an object we've opted-in to * create an actual constructor as it's much more memory efficient and * faster and it pleases my OCD. * * It is worth noting that we should not use `URL` as class name to prevent * clashes with the global URL instance that got introduced in browsers. * * @constructor * @param {String} address URL we want to parse. * @param {Object|String} [location] Location defaults for relative paths. * @param {Boolean|Function} [parser] Parser for the query string. * @private */ function Url(address, location, parser) { if (!(this instanceof Url)) { return new Url(address, location, parser); } var relative, extracted, parse, instruction, index, key , instructions = rules.slice() , type = typeof location , url = this , i = 0; // // The following if statements allows this module two have compatibility with // 2 different API: // // 1. Node.js's `url.parse` api which accepts a URL, boolean as arguments // where the boolean indicates that the query string should also be parsed. // // 2. The `URL` interface of the browser which accepts a URL, object as // arguments. The supplied object will be used as default values / fall-back // for relative paths. // if ('object' !== type && 'string' !== type) { parser = location; location = null; } if (parser && 'function' !== typeof parser) { parser = querystringify_1.parse; } location = lolcation(location); // // Extract protocol information before running the instructions. // extracted = extractProtocol(address || ''); relative = !extracted.protocol && !extracted.slashes; url.slashes = extracted.slashes || relative && location.slashes; url.protocol = extracted.protocol || location.protocol || ''; address = extracted.rest; // // When the authority component is absent the URL starts with a path // component. // if (!extracted.slashes) { instructions[3] = [/(.*)/, 'pathname']; } for (; i < instructions.length; i++) { instruction = instructions[i]; if (typeof instruction === 'function') { address = instruction(address); continue; } parse = instruction[0]; key = instruction[1]; if (parse !== parse) { url[key] = address; } else if ('string' === typeof parse) { if (~(index = address.indexOf(parse))) { if ('number' === typeof instruction[2]) { url[key] = address.slice(0, index); address = address.slice(index + instruction[2]); } else { url[key] = address.slice(index); address = address.slice(0, index); } } } else if ((index = parse.exec(address))) { url[key] = index[1]; address = address.slice(0, index.index); } url[key] = url[key] || ( relative && instruction[3] ? location[key] || '' : '' ); // // Hostname, host and protocol should be lowercased so they can be used to // create a proper `origin`. // if (instruction[4]) { url[key] = url[key].toLowerCase(); } } // // Also parse the supplied query string in to an object. If we're supplied // with a custom parser as function use that instead of the default build-in // parser. // if (parser) { url.query = parser(url.query); } // // If the URL is relative, resolve the pathname against the base URL. // if ( relative && location.slashes && url.pathname.charAt(0) !== '/' && (url.pathname !== '' || location.pathname !== '') ) { url.pathname = resolve(url.pathname, location.pathname); } // // We should not add port numbers if they are already the default port number // for a given protocol. As the host also contains the port number we're going // override it with the hostname which contains no port number. // if (!requiresPort(url.port, url.protocol)) { url.host = url.hostname; url.port = ''; } // // Parse down the `auth` for the username and password. // url.username = url.password = ''; if (url.auth) { instruction = url.auth.split(':'); url.username = instruction[0] || ''; url.password = instruction[1] || ''; } url.origin = url.protocol && url.host && url.protocol !== 'file:' ? url.protocol +'//'+ url.host : 'null'; // // The href is just the compiled result. // url.href = url.toString(); } /** * This is convenience method for changing properties in the URL instance to * insure that they all propagate correctly. * * @param {String} part Property we need to adjust. * @param {Mixed} value The newly assigned value. * @param {Boolean|Function} fn When setting the query, it will be the function * used to parse the query. * When setting the protocol, double slash will be * removed from the final url if it is true. * @returns {URL} URL instance for chaining. * @public */ function set(part, value, fn) { var url = this; switch (part) { case 'query': if ('string' === typeof value && value.length) { value = (fn || querystringify_1.parse)(value); } url[part] = value; break; case 'port': url[part] = value; if (!requiresPort(value, url.protocol)) { url.host = url.hostname; url[part] = ''; } else if (value) { url.host = url.hostname +':'+ value; } break; case 'hostname': url[part] = value; if (url.port) { value += ':'+ url.port; } url.host = value; break; case 'host': url[part] = value; if (/:\d+$/.test(value)) { value = value.split(':'); url.port = value.pop(); url.hostname = value.join(':'); } else { url.hostname = value; url.port = ''; } break; case 'protocol': url.protocol = value.toLowerCase(); url.slashes = !fn; break; case 'pathname': case 'hash': if (value) { var char = part === 'pathname' ? '/' : '#'; url[part] = value.charAt(0) !== char ? char + value : value; } else { url[part] = value; } break; default: url[part] = value; } for (var i = 0; i < rules.length; i++) { var ins = rules[i]; if (ins[4]) { url[ins[1]] = url[ins[1]].toLowerCase(); } } url.origin = url.protocol && url.host && url.protocol !== 'file:' ? url.protocol +'//'+ url.host : 'null'; url.href = url.toString(); return url; } /** * Transform the properties back in to a valid and full URL string. * * @param {Function} stringify Optional query stringify function. * @returns {String} Compiled version of the URL. * @public */ function toString(stringify) { if (!stringify || 'function' !== typeof stringify) { stringify = querystringify_1.stringify; } var query , url = this , protocol = url.protocol; if (protocol && protocol.charAt(protocol.length - 1) !== ':') { protocol += ':'; } var result = protocol + (url.slashes ? '//' : ''); if (url.username) { result += url.username; if (url.password) { result += ':'+ url.password; } result += '@'; } result += url.host + url.pathname; query = 'object' === typeof url.query ? stringify(url.query) : url.query; if (query) { result += '?' !== query.charAt(0) ? '?'+ query : query; } if (url.hash) { result += url.hash; } return result; } Url.prototype = { set: set, toString: toString }; // // Expose the URL parser and some additional properties that might be useful for // others or testing. // Url.extractProtocol = extractProtocol; Url.location = lolcation; Url.qs = querystringify_1; var urlParse = Url; /* * This delay allows the thread to finish assigning its on* methods * before invoking the delay callback. This is purely a timing hack. * http://geekabyte.blogspot.com/2014/01/javascript-effect-of-setting-settimeout.html * * @param {callback: function} the callback which will be invoked after the timeout * @parma {context: object} the context in which to invoke the function */ function delay(callback, context) { setTimeout(function (timeoutContext) { return callback.call(timeoutContext); }, 4, context); } function log(method, message) { /* eslint-disable no-console */ if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'test') { console[method].call(null, message); } /* eslint-enable no-console */ } function reject(array, callback) { var results = []; array.forEach(function (itemInArray) { if (!callback(itemInArray)) { results.push(itemInArray); } }); return results; } function filter(array, callback) { var results = []; array.forEach(function (itemInArray) { if (callback(itemInArray)) { results.push(itemInArray); } }); return results; } /* * EventTarget is an interface implemented by objects that can * receive events and may have listeners for them. * * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget */ var EventTarget = function EventTarget() { this.listeners = {}; }; /* * Ties a listener function to an event type which can later be invoked via the * dispatchEvent method. * * @param {string} type - the type of event (ie: 'open', 'message', etc.) * @param {function} listener - callback function to invoke when an event is dispatched matching the type * @param {boolean} useCapture - N/A TODO: implement useCapture functionality */ EventTarget.prototype.addEventListener = function addEventListener (type, listener /* , useCapture */) { if (typeof listener === 'function') { if (!Array.isArray(this.listeners[type])) { this.listeners[type] = []; } // Only add the same function once if (filter(this.listeners[type], function (item) { return item === listener; }).length === 0) { this.listeners[type].push(listener); } } }; /* * Removes the listener so it will no longer be invoked via the dispatchEvent method. * * @param {string} type - the type of event (ie: 'open', 'message', etc.) * @param {function} listener - callback function to invoke when an event is dispatched matching the type * @param {boolean} useCapture - N/A TODO: implement useCapture functionality */ EventTarget.prototype.removeEventListener = function removeEventListener (type, removingListener /* , useCapture */) { var arrayOfListeners = this.listeners[type]; this.listeners[type] = reject(arrayOfListeners, function (listener) { return listener === removingListener; }); }; /* * Invokes all listener functions that are listening to the given event.type property. Each * listener will be passed the event as the first argument. * * @param {object} event - event object which will be passed to all listeners of the event.type property */ EventTarget.prototype.dispatchEvent = function dispatchEvent (event) { var this$1 = this; var customArguments = [], len = arguments.length - 1; while ( len-- > 0 ) customArguments[ len ] = arguments[ len + 1 ]; var eventName = event.type; var listeners = this.listeners[eventName]; if (!Array.isArray(listeners)) { return false; } listeners.forEach(function (listener) { if (customArguments.length > 0) { listener.apply(this$1, customArguments); } else { listener.call(this$1, event); } }); return true; }; /* * The network bridge is a way for the mock websocket object to 'communicate' with * all available servers. This is a singleton object so it is important that you * clean up urlMap whenever you are finished. */ var NetworkBridge = function NetworkBridge() { this.urlMap = {}; }; /* * Attaches a websocket object to the urlMap hash so that it can find the server * it is connected to and the server in turn can find it. * * @param {object} websocket - websocket object to add to the urlMap hash * @param {string} url */ NetworkBridge.prototype.attachWebSocket = function attachWebSocket (websocket, url) { var queryIndex = url.indexOf('?'); var serverURL = queryIndex >= 0 ? url.slice(0, queryIndex) : url; var connectionLookup = this.urlMap[serverURL]; if (connectionLookup && connectionLookup.server && connectionLookup.websockets.indexOf(websocket) === -1) { connectionLookup.websockets.push(websocket); return connectionLookup.server; } }; /* * Attaches a websocket to a room */ NetworkBridge.prototype.addMembershipToRoom = function addMembershipToRoom (websocket, room) { var connectionLookup = this.urlMap[websocket.url]; if (connectionLookup && connectionLookup.server && connectionLookup.websockets.indexOf(websocket) !== -1) { if (!connectionLookup.roomMemberships[room]) { connectionLookup.roomMemberships[room] = []; } connectionLookup.roomMemberships[room].push(websocket); } }; /* * Attaches a server object to the urlMap hash so that it can find a websockets * which are connected to it and so that websockets can in turn can find it. * * @param {object} server - server object to add to the urlMap hash * @param {string} url */ NetworkBridge.prototype.attachServer = function attachServer (server, url) { var connectionLookup = this.urlMap[url]; if (!connectionLookup) { this.urlMap[url] = { server: server, websockets: [], roomMemberships: {} }; return server; } }; /* * Finds the server which is 'running' on the given url. * * @param {string} url - the url to use to find which server is running on it */ NetworkBridge.prototype.serverLookup = function serverLookup (url) { var connectionLookup = this.urlMap[url]; if (connectionLookup) { return connectionLookup.server; } }; /* * Finds all websockets which is 'listening' on the given url. * * @param {string} url - the url to use to find all websockets which are associated with it * @param {string} room - if a room is provided, will only return sockets in this room * @param {class} broadcaster - socket that is broadcasting and is to be excluded from the lookup */ NetworkBridge.prototype.websocketsLookup = function websocketsLookup (url, room, broadcaster) { var websockets; var connectionLookup = this.urlMap[url]; websockets = connectionLookup ? connectionLookup.websockets : []; if (room) { var members = connectionLookup.roomMemberships[room]; websockets = members || []; } return broadcaster ? websockets.filter(function (websocket) { return websocket !== broadcaster; }) : websockets; }; /* * Removes the entry associated with the url. * * @param {string} url */ NetworkBridge.prototype.removeServer = function removeServer (url) { delete this.urlMap[url]; }; /* * Removes the individual websocket from the map of associated websockets. * * @param {object} websocket - websocket object to remove from the url map * @param {string} url */ NetworkBridge.prototype.removeWebSocket = function removeWebSocket (websocket, url) { var connectionLookup = this.urlMap[url]; if (connectionLookup) { connectionLookup.websockets = reject(connectionLookup.websockets, function (socket) { return socket === websocket; }); } }; /* * Removes a websocket from a room */ NetworkBridge.prototype.removeMembershipFromRoom = function removeMembershipFromRoom (websocket, room) { var connectionLookup = this.urlMap[websocket.url]; var memberships = connectionLookup.roomMemberships[room]; if (connectionLookup && memberships !== null) { connectionLookup.roomMemberships[room] = reject(memberships, function (socket) { return socket === websocket; }); } }; var networkBridge = new NetworkBridge(); // Note: this is a singleton /* * https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent */ var CLOSE_CODES = { CLOSE_NORMAL: 1000, CLOSE_GOING_AWAY: 1001, CLOSE_PROTOCOL_ERROR: 1002, CLOSE_UNSUPPORTED: 1003, CLOSE_NO_STATUS: 1005, CLOSE_ABNORMAL: 1006, UNSUPPORTED_DATA: 1007, POLICY_VIOLATION: 1008, CLOSE_TOO_LARGE: 1009, MISSING_EXTENSION: 1010, INTERNAL_ERROR: 1011, SERVICE_RESTART: 1012, TRY_AGAIN_LATER: 1013, TLS_HANDSHAKE: 1015 }; var ERROR_PREFIX = { CONSTRUCTOR_ERROR: "Failed to construct 'WebSocket':", CLOSE_ERROR: "Failed to execute 'close' on 'WebSocket':", EVENT: { CONSTRUCT: "Failed to construct 'Event':", MESSAGE: "Failed to construct 'MessageEvent':", CLOSE: "Failed to construct 'CloseEvent':" } }; var EventPrototype = function EventPrototype () {}; EventPrototype.prototype.stopPropagation = function stopPropagation () {}; EventPrototype.prototype.stopImmediatePropagation = function stopImmediatePropagation () {}; // if no arguments are passed then the type is set to "undefined" on // chrome and safari. EventPrototype.prototype.initEvent = function initEvent (type, bubbles, cancelable) { if ( type === void 0 ) type = 'undefined'; if ( bubbles === void 0 ) bubbles = false; if ( cancelable === void 0 ) cancelable = false; this.type = "" + type; this.bubbles = Boolean(bubbles); this.cancelable = Boolean(cancelable); }; var Event = (function (EventPrototype$$1) { function Event(type, eventInitConfig) { if ( eventInitConfig === void 0 ) eventInitConfig = {}; EventPrototype$$1.call(this); if (!type) { throw new TypeError(((ERROR_PREFIX.EVENT_ERROR) + " 1 argument required, but only 0 present.")); } if (typeof eventInitConfig !== 'object') { throw new TypeError(((ERROR_PREFIX.EVENT_ERROR) + " parameter 2 ('eventInitDict') is not an object.")); } var bubbles = eventInitConfig.bubbles; var cancelable = eventInitConfig.cancelable; this.type = "" + type; this.timeStamp = Date.now(); this.target = null; this.srcElement = null; this.returnValue = true; this.isTrusted = false; this.eventPhase = 0; this.defaultPrevented = false; this.currentTarget = null; this.cancelable = cancelable ? Boolean(cancelable) : false; this.canncelBubble = false; this.bubbles = bubbles ? Boolean(bubbles) : false; } if ( EventPrototype$$1 ) Event.__proto__ = EventPrototype$$1; Event.prototype = Object.create( EventPrototype$$1 && EventPrototype$$1.prototype ); Event.prototype.constructor = Event; return Event; }(EventPrototype)); var MessageEvent = (function (EventPrototype$$1) { function MessageEvent(type, eventInitConfig) { if ( eventInitConfig === void 0 ) eventInitConfig = {}; EventPrototype$$1.call(this); if (!type) { throw new TypeError(((ERROR_PREFIX.EVENT.MESSAGE) + " 1 argument required, but only 0 present.")); } if (typeof eventInitConfig !== 'object') { throw new TypeError(((ERROR_PREFIX.EVENT.MESSAGE) + " parameter 2 ('eventInitDict') is not an object")); } var bubbles = eventInitConfig.bubbles; var cancelable = eventInitConfig.cancelable; var data = eventInitConfig.data; var origin = eventInitConfig.origin; var lastEventId = eventInitConfig.lastEventId; var ports = eventInitConfig.ports; this.type = "" + type; this.timeStamp = Date.now(); this.target = null; this.srcElement = null; this.returnValue = true; this.isTrusted = false; this.eventPhase = 0; this.defaultPrevented = false; this.currentTarget = null; this.cancelable = cancelable ? Boolean(cancelable) : false; this.canncelBubble = false; this.bubbles = bubbles ? Boolean(bubbles) : false; this.origin = "" + origin; this.ports = typeof ports === 'undefined' ? null : ports; this.data = typeof data === 'undefined' ? null : data; this.lastEventId = "" + (lastEventId || ''); } if ( EventPrototype$$1 ) MessageEvent.__proto__ = EventPrototype$$1; MessageEvent.prototype = Object.create( EventPrototype$$1 && EventPrototype$$1.prototype ); MessageEvent.prototype.constructor = MessageEvent; return MessageEvent; }(EventPrototype)); var CloseEvent = (function (EventPrototype$$1) { function CloseEvent(type, eventInitConfig) { if ( eventInitConfig === void 0 ) eventInitConfig = {}; EventPrototype$$1.call(this); if (!type) { throw new TypeError(((ERROR_PREFIX.EVENT.CLOSE) + " 1 argument required, but only 0 present.")); } if (typeof eventInitConfig !== 'object') { throw new TypeError(((ERROR_PREFIX.EVENT.CLOSE) + " parameter 2 ('eventInitDict') is not an object")); } var bubbles = eventInitConfig.bubbles; var cancelable = eventInitConfig.cancelable; var code = eventInitConfig.code; var reason = eventInitConfig.reason; var wasClean = eventInitConfig.wasClean; this.type = "" + type; this.timeStamp = Date.now(); this.target = null; this.srcElement = null; this.returnValue = true; this.isTrusted = false; this.eventPhase = 0; this.defaultPrevented = false; this.currentTarget = null; this.cancelable = cancelable ? Boolean(cancelable) : false; this.cancelBubble = false; this.bubbles = bubbles ? Boolean(bubbles) : false; this.code = typeof code === 'number' ? parseInt(code, 10) : 0; this.reason = "" + (reason || ''); this.wasClean = wasClean ? Boolean(wasClean) : false; } if ( EventPrototype$$1 ) CloseEvent.__proto__ = EventPrototype$$1; CloseEvent.prototype = Object.create( EventPrototype$$1 && EventPrototype$$1.prototype ); CloseEvent.prototype.constructor = CloseEvent; return CloseEvent; }(EventPrototype)); /* * Creates an Event object and extends it to allow full modification of * its properties. * * @param {object} config - within config you will need to pass type and optionally target */ function createEvent(config) { var type = config.type; var target = config.target; var eventObject = new Event(type); if (target) { eventObject.target = target; eventObject.srcElement = target; eventObject.currentTarget = target; } return eventObject; } /* * Creates a MessageEvent object and extends it to allow full modification of * its properties. * * @param {object} config - within config: type, origin, data and optionally target */ function createMessageEvent(config) { var type = config.type; var origin = config.origin; var data = config.data; var target = config.target; var messageEvent = new MessageEvent(type, { data: data, origin: origin }); if (target) { messageEvent.target = target; messageEvent.srcElement = target; messageEvent.currentTarget = target; } return messageEvent; } /* * Creates a CloseEvent object and extends it to allow full modification of * its properties. * * @param {object} config - within config: type and optionally target, code, and reason */ function createCloseEvent(config) { var code = config.code; var reason = config.reason; var type = config.type; var target = config.target; var wasClean = config.wasClean; if (!wasClean) { wasClean = code === 1000; } var closeEvent = new CloseEvent(type, { code: code, reason: reason, wasClean: wasClean }); if (target) { closeEvent.target = target; closeEvent.srcElement = target; closeEvent.currentTarget = target; } return closeEvent; } function closeWebSocketConnection(context, code, reason) { context.readyState = WebSocket$1.CLOSING; var server = networkBridge.serverLookup(context.url); var closeEvent = createCloseEvent({ type: 'close', target: context, code: code, reason: reason }); delay(function () { networkBridge.removeWebSocket(context, context.url); context.readyState = WebSocket$1.CLOSED; context.dispatchEvent(closeEvent); if (server) { server.dispatchEvent(closeEvent, server); } }, context); } function failWebSocketConnection(context, code, reason) { context.readyState = WebSocket$1.CLOSING; var server = networkBridge.serverLookup(context.url); var closeEvent = createCloseEvent({ type: 'close', target: context, code: code, reason: reason, wasClean: false }); var errorEvent = createEvent({ type: 'error', target: context }); delay(function () { networkBridge.removeWebSocket(context, context.url); context.readyState = WebSocket$1.CLOSED; context.dispatchEvent(errorEvent); context.dispatchEvent(closeEvent); if (server) { server.dispatchEvent(closeEvent, server); } }, context); } function normalizeSendData(data) { if (Object.prototype.toString.call(data) !== '[object Blob]' && !(data instanceof ArrayBuffer)) { data = String(data); } return data; } function proxyFactory(target) { var handler = { get: function get(obj, prop) { if (prop === 'close') { return function close(options) { if ( options === void 0 ) options = {}; var code = options.code || CLOSE_CODES.CLOSE_NORMAL; var reason = options.reason || ''; closeWebSocketConnection(target, code, reason); }; } if (prop === 'send') { return function send(data) { data = normalizeSendData(data); target.dispatchEvent( createMessageEvent({ type: 'message', data: data, origin: this.url, target: target }) ); }; } if (prop === 'on') { return function onWrapper(type, cb) { target.addEventListener(("server::" + type), cb); }; } return obj[prop]; } }; var proxy = new Proxy(target, handler); return proxy; } function lengthInUtf8Bytes(str) { // Matches only the 10.. bytes that are non-initial characters in a multi-byte sequence. var m = encodeURIComponent(str).match(/%[89ABab]/g); return str.length + (m ? m.length : 0); } function urlVerification(url) { var urlRecord = new urlParse(url); var pathname = urlRecord.pathname; var protocol = urlRecord.protocol; var hash = urlRecord.hash; if (!url) { throw new TypeError(((ERROR_PREFIX.CONSTRUCTOR_ERROR) + " 1 argument required, but only 0 present.")); } if (!pathname) { urlRecord.pathname = '/'; } if (protocol === '') { throw new SyntaxError(((ERROR_PREFIX.CONSTRUCTOR_ERROR) + " The URL '" + (urlRecord.toString()) + "' is invalid.")); } if (protocol !== 'ws:' && protocol !== 'wss:') { throw new SyntaxError( ((ERROR_PREFIX.CONSTRUCTOR_ERROR) + " The URL's scheme must be either 'ws' or 'wss'. '" + protocol + "' is not allowed.") ); } if (hash !== '') { /* eslint-disable max-len */ throw new SyntaxError( ((ERROR_PREFIX.CONSTRUCTOR_ERROR) + " The URL contains a fragment identifier ('" + hash + "'). Fragment identifiers are not allowed in WebSocket URLs.") ); /* eslint-enable max-len */ } return urlRecord.toString(); } function protocolVerification(protocols) { if ( protocols === void 0 ) protocols = []; if (!Array.isArray(protocols) && typeof protocols !== 'string') { throw new SyntaxError(((ERROR_PREFIX.CONSTRUCTOR_ERROR) + " The subprotocol '" + (protocols.toString()) + "' is invalid.")); } if (typeof protocols === 'string') { protocols = [protocols]; } var uniq = protocols .map(function (p) { return ({ count: 1, protocol: p }); }) .reduce(function (a, b) { a[b.protocol] = (a[b.protocol] || 0) + b.count; return a; }, {}); var duplicates = Object.keys(uniq).filter(function (a) { return uniq[a] > 1; }); if (duplicates.length > 0) { throw new SyntaxError(((ERROR_PREFIX.CONSTRUCTOR_ERROR) + " The subprotocol '" + (duplicates[0]) + "' is duplicated.")); } return protocols; } /* * The main websocket class which is designed to mimick the native WebSocket class as close * as possible. * * https://html.spec.whatwg.org/multipage/web-sockets.html */ var WebSocket$1 = (function (EventTarget$$1) { function WebSocket(url, protocols) { EventTarget$$1.call(this); this.url = urlVerification(url); protocols = protocolVerification(protocols); this.protocol = protocols[0] || ''; this.binaryType = 'blob'; this.readyState = WebSocket.CONNECTING; var server = networkBridge.attachWebSocket(this, this.url); /* * This delay is needed so that we dont trigger an event before the callbacks have been * setup. For example: * * var socket = new WebSocket('ws://localhost'); * * If we dont have the delay then the event would be triggered right here and this is * before the onopen had a chance to register itself. * * socket.onopen = () => { // this would never be called }; * * and with the delay the event gets triggered here after all of the callbacks have been * registered :-) */ delay(function delayCallback() { if (server) { if ( server.options.verifyClient && typeof server.options.verifyClient === 'function' && !server.options.verifyClient() ) { this.readyState = WebSocket.CLOSED; log( 'error', ("WebSocket connection to '" + (this.url) + "' failed: HTTP Authentication failed; no valid credentials available") ); networkBridge.removeWebSocket(this, this.url); this.dispatchEvent(createEvent({ type: 'error', target: this })); this.dispatchEvent(createCloseEvent({ type: 'close', target: this, code: CLOSE_CODES.CLOSE_NORMAL })); } else { if (server.options.selectProtocol && typeof server.options.selectProtocol === 'function') { var selectedProtocol = server.options.selectProtocol(protocols); var isFilled = selectedProtocol !== ''; var isRequested = protocols.indexOf(selectedProtocol) !== -1; if (isFilled && !isRequested) { this.readyState = WebSocket.CLOSED; log('error', ("WebSocket connection to '" + (this.url) + "' failed: Invalid Sub-Protocol")); networkBridge.removeWebSocket(this, this.url); this.dispatchEvent(createEvent({ type: 'error', target: this })); this.dispatchEvent(createCloseEvent({ type: 'close', target: this, code: CLOSE_CODES.CLOSE_NORMAL })); return; } this.protocol = selectedProtocol; } this.readyState = WebSocket.OPEN; this.dispatchEvent(createEvent({ type: 'open', target: this })); server.dispatchEvent(createEvent({ type: 'connection' }), proxyFactory(this)); } } else { this.readyState = WebSocket.CLOSED; this.dispatchEvent(createEvent({ type: 'error', target: this })); this.dispatchEvent(createCloseEvent({ type: 'close', target: this, code: CLOSE_CODES.CLOSE_NORMAL })); log('error', ("WebSocket connection to '" + (this.url) + "' failed")); } }, this); } if ( EventTarget$$1 ) WebSocket.__proto__ = EventTarget$$1; WebSocket.prototype = Object.create( EventTarget$$1 && EventTarget$$1.prototype ); WebSocket.prototype.constructor = WebSocket; var prototypeAccessors = { onopen: {},onmessage: {},onclose: {},onerror: {} }; prototypeAccessors.onopen.get = function () { return this.listeners.open; }; prototypeAccessors.onmessage.get = function () { return this.listeners.message; }; prototypeAccessors.onclose.get = function () { return this.listeners.close; }; prototypeAccessors.onerror.get = function () { return this.listeners.error; }; prototypeAccessors.onopen.set = function (listener) { delete this.listeners.open; this.addEventListener('open', listener); }; prototypeAccessors.onmessage.set = function (listener) { delete this.listeners.message; this.addEventListener('message', listener); }; prototypeAccessors.onclose.set = function (listener) { delete this.listeners.close; this.addEventListener('close', listener); }; prototypeAccessors.onerror.set = function (listener) { delete this.listeners.error; this.addEventListener('error', listener); }; WebSocket.prototype.send = function send (data) { var this$1 = this; if (this.readyState === WebSocket.CLOSING || this.readyState === WebSocket.CLOSED) { throw new Error('WebSocket is already in CLOSING or CLOSED state'); } // TODO: handle bufferedAmount var messageEvent = createMessageEvent({ type: 'server::message', origin: this.url, data: normalizeSendData(data) }); var server = networkBridge.serverLookup(this.url); if (server) { delay(function () { this$1.dispatchEvent(messageEvent, data); }, server); } }; WebSocket.prototype.close = function close (code, reason) { if (code !== undefined) { if (typeof code !== 'number' || (code !== 1000 && (code < 3000 || code > 4999))) { throw new TypeError( ((ERROR_PREFIX.CLOSE_ERROR) + " The code must be either 1000, or between 3000 and 4999. " + code + " is neither.") ); } } if (reason !== undefined) { var length = lengthInUtf8Bytes(reason); if (length > 123) { throw new SyntaxError(((ERROR_PREFIX.CLOSE_ERROR) + " The message must not be greater than 123 bytes.")); } } if (this.readyState === WebSocket.CLOSING || this.readyState === WebSocket.CLOSED) { return; } if (this.readyState === WebSocket.CONNECTING) { failWebSocketConnection(this, code, reason); } else { closeWebSocketConnection(this, code, reason); } }; Object.defineProperties( WebSocket.prototype, prototypeAccessors ); return WebSocket; }(EventTarget)); WebSocket$1.CONNECTING = 0; WebSocket$1.prototype.CONNECTING = WebSocket$1.CONNECTING; WebSocket$1.OPEN = 1; WebSocket$1.prototype.OPEN = WebSocket$1.OPEN; WebSocket$1.CLOSING = 2; WebSocket$1.prototype.CLOSING = WebSocket$1.CLOSING; WebSocket$1.CLOSED = 3; WebSocket$1.prototype.CLOSED = WebSocket$1.CLOSED; var dedupe = function (arr) { return arr.reduce(function (deduped, b) { if (deduped.indexOf(b) > -1) { return deduped; } return deduped.concat(b); }, []); }; function retrieveGlobalObject() { if (typeof window !== 'undefined') { return window; } return typeof process === 'object' && typeof require === 'function' && typeof global === 'object' ? global : this; } var Server$1 = (function (EventTarget$$1) { function Server(url, options) { if ( options === void 0 ) options = {}; EventTarget$$1.call(this); var urlRecord = new urlParse(url); if (!urlRecord.pathname) { urlRecord.pathname = '/'; } this.url = urlRecord.toString(); this.originalWebSocket = null; var server = networkBridge.attachServer(this, this.url); if (!server) { this.dispatchEvent(createEvent({ type: 'error' })); throw new Error('A mock server is already listening on this url'); } if (typeof options.verifyClient === 'undefined') { options.verifyClient = null; } if (typeof options.selectProtocol === 'undefined') { options.selectProtocol = null; } this.options = options; this.start(); } if ( EventTarget$$1 ) Server.__proto__ = EventTarget$$1; Server.prototype = Object.create( EventTarget$$1 && EventTarget$$1.prototype ); Server.prototype.constructor = Server; /* * Attaches the mock websocket object to the global object */ Server.prototype.start = function start () { var globalObj = retrieveGlobalObject(); if (globalObj.WebSocket) { this.originalWebSocket = globalObj.WebSocket; } globalObj.WebSocket = WebSocket$1; }; /* * Removes the mock websocket object from the global object */ Server.prototype.stop = function stop (callback) { if ( callback === void 0 ) callback = function () {}; var globalObj = retrieveGlobalObject(); if (this.originalWebSocket) { globalObj.WebSocket = this.originalWebSocket; } else { delete globalObj.WebSocket; } this.originalWebSocket = null; networkBridge.removeServer(this.url); if (typeof callback === 'function') { callback(); } }; /* * This is the main function for the mock server to subscribe to the on events. * * ie: mockServer.on('connection', function() { console.log('a mock client connected'); }); * * @param {string} type - The event key to subscribe to. Valid keys are: connection, message, and close. * @param {function} callback - The callback which should be called when a certain event is fired. */ Server.prototype.on = function on (type, callback) { this.addEventListener(type, callback); }; /* * Closes the connection and triggers the onclose method of all listening * websockets. After that it removes itself from the urlMap so another server * could add itself to the url. * * @param {object} options */ Server.prototype.close = function close (options) { if ( options === void 0 ) options = {}; var code = options.code; var reason = options.reason; var wasClean = options.wasClean; var listeners = networkBridge.websocketsLookup(this.url); // Remove server before notifications to prevent immediate reconnects from // socket onclose handlers networkBridge.removeServer(this.url); listeners.forEach(function (socket) { socket.readyState = WebSocket$1.CLOSE; socket.dispatchEvent( createCloseEvent({ type: 'close', target: socket, code: code || CLOSE_CODES.CLOSE_NORMAL, reason: reason || '', wasClean: wasClean }) ); }); this.dispatchEvent(createCloseEvent({ type: 'close' }), this); }; /* * Sends a generic message event to all mock clients. */ Server.prototype.emit = function emit (event, data, options) { var this$1 = this; if ( options === void 0 ) options = {}; var websockets = options.websockets; if (!websockets) { websockets = networkBridge.websocketsLookup(this.url); } if (typeof options !== 'object' || arguments.length > 3) { data = Array.prototype.slice.call(arguments, 1, arguments.length); data = data.map(function (item) { return normalizeSendData(item); }); } else { data = normalizeSendData(data); } websockets.forEach(function (socket) { if (Array.isArray(data)) { socket.dispatchEvent.apply( socket, [ createMessageEvent({ type: event, data: data, origin: this$1.url, target: socket }) ].concat( data ) ); } else { socket.dispatchEvent( createMessageEvent({ type: event, data: data, origin: this$1.url, target: socket }) ); } }); }; /* * Returns an array of websockets which are listening to this server * TOOD: this should return a set and not be a method */ Server.prototype.clients = function clients () { return networkBridge.websocketsLookup(this.url); }; /* * Prepares a method to submit an event to members of the room * * e.g. server.to('my-room').emit('hi!'); */ Server.prototype.to = function to (room, broadcaster, broadcastList) { var this$1 = this; if ( broadcastList === void 0 ) broadcastList = []; var self = this; var websockets = dedupe(broadcastList.concat(networkBridge.websocketsLookup(this.url, room, broadcaster))); return { to: function (chainedRoom, chainedBroadcaster) { return this$1.to.call(this$1, chainedRoom, chainedBroadcaster, websockets); }, emit: function emit(event, data) { self.emit(event, data, { websockets: websockets }); } }; }; /* * Alias for Server.to */ Server.prototype.in = function in$1 () { var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; return this.to.apply(null, args); }; /* * Simulate an event from the server to the clients. Useful for * simulating errors. */ Server.prototype.simulate = function simulate (event) { var listeners = networkBridge.websocketsLookup(this.url); if (event === 'error') { listeners.forEach(function (socket) { socket.readyState = WebSocket$1.CLOSE; socket.dispatchEvent(createEvent({ type: 'error' })); }); } }; return Server; }(EventTarget)); /* * Alternative constructor to support namespaces in socket.io * * http://socket.io/docs/rooms-and-namespaces/#custom-namespaces */ Server$1.of = function of(url) { return new Server$1(url); }; /* * The socket-io class is designed to mimick the real API as closely as possible. * * http://socket.io/docs/ */ var SocketIO$1 = (function (EventTarget$$1) { function SocketIO(url, protocol) { var this$1 = this; if ( url === void 0 ) url = 'socket.io'; if ( protocol === void 0 ) protocol = ''; EventTarget$$1.call(this); this.binaryType = 'blob'; var urlRecord = new urlParse(url); if (!urlRecord.pathname) { urlRecord.pathname = '/'; } this.url = urlRecord.toString(); this.readyState = SocketIO.CONNECTING; this.protocol = ''; if (typeof protocol === 'string' || (typeof protocol === 'object' && protocol !== null)) { this.protocol = protocol; } else if (Array.isArray(protocol) && protocol.length > 0) { this.protocol = protocol[0]; } var server = networkBridge.attachWebSocket(this, this.url); /* * Delay triggering the connection events so they can be defined in time. */ delay(function delayCallback() { if (server) { this.readyState = SocketIO.OPEN; server.dispatchEvent(createEvent({ type: 'connection' }), server, this); server.dispatchEvent(createEvent({ type: 'connect' }), server, this); // alias this.dispatchEvent(createEvent({ type: 'connect', target: this })); } else { this.readyState = SocketIO.CLOSED; this.dispatchEvent(createEvent({ type: 'error', target: this })); this.dispatchEvent( createCloseEvent({ type: 'close', target: this, code: CLOSE_CODES.CLOSE_NORMAL }) ); log('error', ("Socket.io connection to '" + (this.url) + "' failed")); } }, this); /** Add an aliased event listener for close / disconnect */ this.addEventListener('close', function (event) { this$1.dispatchEvent( createClo