mock-socket.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
  2. // Starting point for browserify and throws important objects into the window object
  3. var Service = require('./service');
  4. var MockServer = require('./mock-server');
  5. var MockSocket = require('./mock-socket');
  6. var globalContext = require('./helpers/global-context');
  7. globalContext.SocketService = Service;
  8. globalContext.MockSocket = MockSocket;
  9. globalContext.MockServer = MockServer;
  10. },{"./helpers/global-context":3,"./mock-server":7,"./mock-socket":8,"./service":9}],2:[function(require,module,exports){
  11. var globalContext = require('./global-context');
  12. /*
  13. * This delay allows the thread to finish assigning its on* methods
  14. * before invoking the delay callback. This is purely a timing hack.
  15. * http://geekabyte.blogspot.com/2014/01/javascript-effect-of-setting-settimeout.html
  16. *
  17. * @param {callback: function} the callback which will be invoked after the timeout
  18. * @parma {context: object} the context in which to invoke the function
  19. */
  20. function delay(callback, context) {
  21. globalContext.setTimeout(function(context) {
  22. callback.call(context);
  23. }, 4, context);
  24. }
  25. module.exports = delay;
  26. },{"./global-context":3}],3:[function(require,module,exports){
  27. (function (global){
  28. /*
  29. * Determines the global context. This should be either window (in the)
  30. * case where we are in a browser) or global (in the case where we are in
  31. * node)
  32. */
  33. var globalContext;
  34. if(typeof window === 'undefined') {
  35. globalContext = global;
  36. }
  37. else {
  38. globalContext = window;
  39. }
  40. if (!globalContext) {
  41. throw new Error('Unable to set the global context to either window or global.');
  42. }
  43. module.exports = globalContext;
  44. }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
  45. },{}],4:[function(require,module,exports){
  46. /*
  47. * This is a mock websocket event message that is passed into the onopen,
  48. * opmessage, etc functions.
  49. *
  50. * @param {name: string} The name of the event
  51. * @param {data: *} The data to send.
  52. * @param {origin: string} The url of the place where the event is originating.
  53. */
  54. function socketEventMessage(name, data, origin) {
  55. var ports = null;
  56. var source = null;
  57. var bubbles = false;
  58. var cancelable = false;
  59. var lastEventId = '';
  60. var targetPlacehold = null;
  61. try {
  62. var messageEvent = new MessageEvent(name);
  63. messageEvent.initMessageEvent(name, bubbles, cancelable, data, origin, lastEventId);
  64. Object.defineProperties(messageEvent, {
  65. target: {
  66. get: function() { return targetPlacehold; },
  67. set: function(value) { targetPlacehold = value; }
  68. },
  69. srcElement: {
  70. get: function() { return this.target; }
  71. },
  72. currentTarget: {
  73. get: function() { return this.target; }
  74. }
  75. });
  76. }
  77. catch (e) {
  78. // We are unable to create a MessageEvent Object. This should only be happening in PhantomJS.
  79. var messageEvent = {
  80. type : name,
  81. bubbles : bubbles,
  82. cancelable : cancelable,
  83. data : data,
  84. origin : origin,
  85. lastEventId : lastEventId,
  86. source : source,
  87. ports : ports,
  88. defaultPrevented : false,
  89. returnValue : true,
  90. clipboardData : undefined
  91. };
  92. Object.defineProperties(messageEvent, {
  93. target: {
  94. get: function() { return targetPlacehold; },
  95. set: function(value) { targetPlacehold = value; }
  96. },
  97. srcElement: {
  98. get: function() { return this.target; }
  99. },
  100. currentTarget: {
  101. get: function() { return this.target; }
  102. }
  103. });
  104. }
  105. return messageEvent;
  106. }
  107. module.exports = socketEventMessage;
  108. },{}],5:[function(require,module,exports){
  109. /*
  110. * The native websocket object will transform urls without a pathname to have just a /.
  111. * As an example: ws://localhost:8080 would actually be ws://localhost:8080/ but ws://example.com/foo would not
  112. * change. This function does this transformation to stay inline with the native websocket implementation.
  113. *
  114. * @param {url: string} The url to transform.
  115. */
  116. function urlTransform(url) {
  117. var urlPath = urlParse('path', url);
  118. var urlQuery = urlParse('?', url);
  119. urlQuery = (urlQuery) ? '?' + urlQuery : '';
  120. if(urlPath === '') {
  121. return url.split('?')[0] + '/' + urlQuery;
  122. }
  123. return url;
  124. }
  125. /*
  126. * The following functions (isNumeric & urlParse) was taken from
  127. * https://github.com/websanova/js-url/blob/764ed8d94012a79bfa91026f2a62fe3383a5a49e/url.js
  128. * which is shared via the MIT license with minimal modifications.
  129. */
  130. function isNumeric(arg) {
  131. return !isNaN(parseFloat(arg)) && isFinite(arg);
  132. }
  133. function urlParse(arg, url) {
  134. var _ls = url || window.location.toString();
  135. if (!arg) { return _ls; }
  136. else { arg = arg.toString(); }
  137. if (_ls.substring(0,2) === '//') { _ls = 'http:' + _ls; }
  138. else if (_ls.split('://').length === 1) { _ls = 'http://' + _ls; }
  139. url = _ls.split('/');
  140. var _l = {auth:''}, host = url[2].split('@');
  141. if (host.length === 1) { host = host[0].split(':'); }
  142. else { _l.auth = host[0]; host = host[1].split(':'); }
  143. _l.protocol=url[0];
  144. _l.hostname=host[0];
  145. _l.port=(host[1] || ((_l.protocol.split(':')[0].toLowerCase() === 'https') ? '443' : '80'));
  146. _l.pathname=( (url.length > 3 ? '/' : '') + url.slice(3, url.length).join('/').split('?')[0].split('#')[0]);
  147. var _p = _l.pathname;
  148. if (_p.charAt(_p.length-1) === '/') { _p=_p.substring(0, _p.length-1); }
  149. var _h = _l.hostname, _hs = _h.split('.'), _ps = _p.split('/');
  150. if (arg === 'hostname') { return _h; }
  151. else if (arg === 'domain') {
  152. if (/^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/.test(_h)) { return _h; }
  153. return _hs.slice(-2).join('.');
  154. }
  155. //else if (arg === 'tld') { return _hs.slice(-1).join('.'); }
  156. else if (arg === 'sub') { return _hs.slice(0, _hs.length - 2).join('.'); }
  157. else if (arg === 'port') { return _l.port; }
  158. else if (arg === 'protocol') { return _l.protocol.split(':')[0]; }
  159. else if (arg === 'auth') { return _l.auth; }
  160. else if (arg === 'user') { return _l.auth.split(':')[0]; }
  161. else if (arg === 'pass') { return _l.auth.split(':')[1] || ''; }
  162. else if (arg === 'path') { return _l.pathname; }
  163. else if (arg.charAt(0) === '.') {
  164. arg = arg.substring(1);
  165. if(isNumeric(arg)) {arg = parseInt(arg, 10); return _hs[arg < 0 ? _hs.length + arg : arg-1] || ''; }
  166. }
  167. else if (isNumeric(arg)) { arg = parseInt(arg, 10); return _ps[arg < 0 ? _ps.length + arg : arg] || ''; }
  168. else if (arg === 'file') { return _ps.slice(-1)[0]; }
  169. else if (arg === 'filename') { return _ps.slice(-1)[0].split('.')[0]; }
  170. else if (arg === 'fileext') { return _ps.slice(-1)[0].split('.')[1] || ''; }
  171. else if (arg.charAt(0) === '?' || arg.charAt(0) === '#') {
  172. var params = _ls, param = null;
  173. if(arg.charAt(0) === '?') { params = (params.split('?')[1] || '').split('#')[0]; }
  174. else if(arg.charAt(0) === '#') { params = (params.split('#')[1] || ''); }
  175. if(!arg.charAt(1)) { return params; }
  176. arg = arg.substring(1);
  177. params = params.split('&');
  178. for(var i=0,ii=params.length; i<ii; i++) {
  179. param = params[i].split('=');
  180. if(param[0] === arg) { return param[1] || ''; }
  181. }
  182. return null;
  183. }
  184. return '';
  185. }
  186. module.exports = urlTransform;
  187. },{}],6:[function(require,module,exports){
  188. /*
  189. * This defines four methods: onopen, onmessage, onerror, and onclose. This is done this way instead of
  190. * just placing the methods on the prototype because we need to capture the callback when it is defined like so:
  191. *
  192. * mockSocket.onopen = function() { // this is what we need to store };
  193. *
  194. * The only way is to capture the callback via the custom setter below and then place them into the correct
  195. * namespace so they get invoked at the right time.
  196. *
  197. * @param {websocket: object} The websocket object which we want to define these properties onto
  198. */
  199. function webSocketProperties(websocket) {
  200. var eventMessageSource = function(callback) {
  201. return function(event) {
  202. event.target = websocket;
  203. callback.apply(websocket, arguments);
  204. }
  205. };
  206. Object.defineProperties(websocket, {
  207. onopen: {
  208. enumerable: true,
  209. get: function() { return this._onopen; },
  210. set: function(callback) {
  211. this._onopen = eventMessageSource(callback);
  212. this.service.setCallbackObserver('clientOnOpen', this._onopen, websocket);
  213. }
  214. },
  215. onmessage: {
  216. enumerable: true,
  217. get: function() { return this._onmessage; },
  218. set: function(callback) {
  219. this._onmessage = eventMessageSource(callback);
  220. this.service.setCallbackObserver('clientOnMessage', this._onmessage, websocket);
  221. }
  222. },
  223. onclose: {
  224. enumerable: true,
  225. get: function() { return this._onclose; },
  226. set: function(callback) {
  227. this._onclose = eventMessageSource(callback);
  228. this.service.setCallbackObserver('clientOnclose', this._onclose, websocket);
  229. }
  230. },
  231. onerror: {
  232. enumerable: true,
  233. get: function() { return this._onerror; },
  234. set: function(callback) {
  235. this._onerror = eventMessageSource(callback);
  236. this.service.setCallbackObserver('clientOnError', this._onerror, websocket);
  237. }
  238. }
  239. });
  240. };
  241. module.exports = webSocketProperties;
  242. },{}],7:[function(require,module,exports){
  243. var Service = require('./service');
  244. var delay = require('./helpers/delay');
  245. var urlTransform = require('./helpers/url-transform');
  246. var socketMessageEvent = require('./helpers/message-event');
  247. var globalContext = require('./helpers/global-context');
  248. function MockServer(url) {
  249. var service = new Service();
  250. this.url = urlTransform(url);
  251. globalContext.MockSocket.services[this.url] = service;
  252. this.service = service;
  253. service.server = this;
  254. }
  255. MockServer.prototype = {
  256. service: null,
  257. /*
  258. * This is the main function for the mock server to subscribe to the on events.
  259. *
  260. * ie: mockServer.on('connection', function() { console.log('a mock client connected'); });
  261. *
  262. * @param {type: string}: The event key to subscribe to. Valid keys are: connection, message, and close.
  263. * @param {callback: function}: The callback which should be called when a certain event is fired.
  264. */
  265. on: function(type, callback) {
  266. var observerKey;
  267. if(typeof callback !== 'function' || typeof type !== 'string') {
  268. return false;
  269. }
  270. switch(type) {
  271. case 'connection':
  272. observerKey = 'clientHasJoined';
  273. break;
  274. case 'message':
  275. observerKey = 'clientHasSentMessage';
  276. break;
  277. case 'close':
  278. observerKey = 'clientHasLeft';
  279. break;
  280. }
  281. // Make sure that the observerKey is valid before observing on it.
  282. if(typeof observerKey === 'string') {
  283. this.service.clearAll(observerKey);
  284. this.service.setCallbackObserver(observerKey, callback, this);
  285. }
  286. },
  287. /*
  288. * This send function will notify all mock clients via their onmessage callbacks that the server
  289. * has a message for them.
  290. *
  291. * @param {data: *}: Any javascript object which will be crafted into a MessageObject.
  292. */
  293. send: function(data) {
  294. delay(function() {
  295. this.service.sendMessageToClients(socketMessageEvent('message', data, this.url));
  296. }, this);
  297. },
  298. /*
  299. * Notifies all mock clients that the server is closing and their onclose callbacks should fire.
  300. */
  301. close: function() {
  302. delay(function() {
  303. this.service.closeConnectionFromServer(socketMessageEvent('close', null, this.url));
  304. }, this);
  305. }
  306. };
  307. module.exports = MockServer;
  308. },{"./helpers/delay":2,"./helpers/global-context":3,"./helpers/message-event":4,"./helpers/url-transform":5,"./service":9}],8:[function(require,module,exports){
  309. var delay = require('./helpers/delay');
  310. var urlTransform = require('./helpers/url-transform');
  311. var socketMessageEvent = require('./helpers/message-event');
  312. var globalContext = require('./helpers/global-context');
  313. var webSocketProperties = require('./helpers/websocket-properties');
  314. function MockSocket(url) {
  315. this.binaryType = 'blob';
  316. this.url = urlTransform(url);
  317. this.readyState = globalContext.MockSocket.CONNECTING;
  318. this.service = globalContext.MockSocket.services[this.url];
  319. webSocketProperties(this);
  320. delay(function() {
  321. // Let the service know that we are both ready to change our ready state and that
  322. // this client is connecting to the mock server.
  323. this.service.clientIsConnecting(this, this._updateReadyState);
  324. }, this);
  325. }
  326. MockSocket.CONNECTING = 0;
  327. MockSocket.OPEN = 1;
  328. MockSocket.CLOSING = 2;
  329. MockSocket.LOADING = 3;
  330. MockSocket.CLOSED = 4;
  331. MockSocket.services = {};
  332. MockSocket.prototype = {
  333. /*
  334. * Holds the on*** callback functions. These are really just for the custom
  335. * getters that are defined in the helpers/websocket-properties. Accessing these properties is not advised.
  336. */
  337. _onopen : null,
  338. _onmessage : null,
  339. _onerror : null,
  340. _onclose : null,
  341. /*
  342. * This holds reference to the service object. The service object is how we can
  343. * communicate with the backend via the pub/sub model.
  344. *
  345. * The service has properties which we can use to observe or notifiy with.
  346. * this.service.notify('foo') & this.service.observe('foo', callback, context)
  347. */
  348. service: null,
  349. /*
  350. * This is a mock for the native send function found on the WebSocket object. It notifies the
  351. * service that it has sent a message.
  352. *
  353. * @param {data: *}: Any javascript object which will be crafted into a MessageObject.
  354. */
  355. send: function(data) {
  356. delay(function() {
  357. this.service.sendMessageToServer(socketMessageEvent('message', data, this.url));
  358. }, this);
  359. },
  360. /*
  361. * This is a mock for the native close function found on the WebSocket object. It notifies the
  362. * service that it is closing the connection.
  363. */
  364. close: function() {
  365. delay(function() {
  366. this.service.closeConnectionFromClient(socketMessageEvent('close', null, this.url), this);
  367. }, this);
  368. },
  369. /*
  370. * This is a private method that can be used to change the readyState. This is used
  371. * like this: this.protocol.subject.observe('updateReadyState', this._updateReadyState, this);
  372. * so that the service and the server can change the readyState simply be notifing a namespace.
  373. *
  374. * @param {newReadyState: number}: The new ready state. Must be 0-4
  375. */
  376. _updateReadyState: function(newReadyState) {
  377. if(newReadyState >= 0 && newReadyState <= 4) {
  378. this.readyState = newReadyState;
  379. }
  380. }
  381. };
  382. module.exports = MockSocket;
  383. },{"./helpers/delay":2,"./helpers/global-context":3,"./helpers/message-event":4,"./helpers/url-transform":5,"./helpers/websocket-properties":6}],9:[function(require,module,exports){
  384. var socketMessageEvent = require('./helpers/message-event');
  385. var globalContext = require('./helpers/global-context');
  386. function SocketService() {
  387. this.list = {};
  388. }
  389. SocketService.prototype = {
  390. server: null,
  391. /*
  392. * This notifies the mock server that a client is connecting and also sets up
  393. * the ready state observer.
  394. *
  395. * @param {client: object} the context of the client
  396. * @param {readyStateFunction: function} the function that will be invoked on a ready state change
  397. */
  398. clientIsConnecting: function(client, readyStateFunction) {
  399. this.observe('updateReadyState', readyStateFunction, client);
  400. // if the server has not been set then we notify the onclose method of this client
  401. if(!this.server) {
  402. this.notify(client, 'updateReadyState', globalContext.MockSocket.CLOSED);
  403. this.notifyOnlyFor(client, 'clientOnError');
  404. return false;
  405. }
  406. this.notifyOnlyFor(client, 'updateReadyState', globalContext.MockSocket.OPEN);
  407. this.notify('clientHasJoined', this.server);
  408. this.notifyOnlyFor(client, 'clientOnOpen', socketMessageEvent('open', null, this.server.url));
  409. },
  410. /*
  411. * Closes a connection from the server's perspective. This should
  412. * close all clients.
  413. *
  414. * @param {messageEvent: object} the mock message event.
  415. */
  416. closeConnectionFromServer: function(messageEvent) {
  417. this.notify('updateReadyState', globalContext.MockSocket.CLOSING);
  418. this.notify('clientOnclose', messageEvent);
  419. this.notify('updateReadyState', globalContext.MockSocket.CLOSED);
  420. this.notify('clientHasLeft');
  421. },
  422. /*
  423. * Closes a connection from the clients perspective. This
  424. * should only close the client who initiated the close and not
  425. * all of the other clients.
  426. *
  427. * @param {messageEvent: object} the mock message event.
  428. * @param {client: object} the context of the client
  429. */
  430. closeConnectionFromClient: function(messageEvent, client) {
  431. if(client.readyState === globalContext.MockSocket.OPEN) {
  432. this.notifyOnlyFor(client, 'updateReadyState', globalContext.MockSocket.CLOSING);
  433. this.notifyOnlyFor(client, 'clientOnclose', messageEvent);
  434. this.notifyOnlyFor(client, 'updateReadyState', globalContext.MockSocket.CLOSED);
  435. this.notify('clientHasLeft');
  436. }
  437. },
  438. /*
  439. * Notifies the mock server that a client has sent a message.
  440. *
  441. * @param {messageEvent: object} the mock message event.
  442. */
  443. sendMessageToServer: function(messageEvent) {
  444. this.notify('clientHasSentMessage', messageEvent.data, messageEvent);
  445. },
  446. /*
  447. * Notifies all clients that the server has sent a message
  448. *
  449. * @param {messageEvent: object} the mock message event.
  450. */
  451. sendMessageToClients: function(messageEvent) {
  452. this.notify('clientOnMessage', messageEvent);
  453. },
  454. /*
  455. * Setup the callback function observers for both the server and client.
  456. *
  457. * @param {observerKey: string} either: connection, message or close
  458. * @param {callback: function} the callback to be invoked
  459. * @param {server: object} the context of the server
  460. */
  461. setCallbackObserver: function(observerKey, callback, server) {
  462. this.observe(observerKey, callback, server);
  463. },
  464. /*
  465. * Binds a callback to a namespace. If notify is called on a namespace all "observers" will be
  466. * fired with the context that is passed in.
  467. *
  468. * @param {namespace: string}
  469. * @param {callback: function}
  470. * @param {context: object}
  471. */
  472. observe: function(namespace, callback, context) {
  473. // Make sure the arguments are of the correct type
  474. if( typeof namespace !== 'string' || typeof callback !== 'function' || (context && typeof context !== 'object')) {
  475. return false;
  476. }
  477. // If a namespace has not been created before then we need to "initialize" the namespace
  478. if(!this.list[namespace]) {
  479. this.list[namespace] = [];
  480. }
  481. this.list[namespace].push({callback: callback, context: context});
  482. },
  483. /*
  484. * Remove all observers from a given namespace.
  485. *
  486. * @param {namespace: string} The namespace to clear.
  487. */
  488. clearAll: function(namespace) {
  489. if(!this.verifyNamespaceArg(namespace)) {
  490. return false;
  491. }
  492. this.list[namespace] = [];
  493. },
  494. /*
  495. * Notify all callbacks that have been bound to the given namespace.
  496. *
  497. * @param {namespace: string} The namespace to notify observers on.
  498. * @param {namespace: url} The url to notify observers on.
  499. */
  500. notify: function(namespace) {
  501. // This strips the namespace from the list of args as we dont want to pass that into the callback.
  502. var argumentsForCallback = Array.prototype.slice.call(arguments, 1);
  503. if(!this.verifyNamespaceArg(namespace)) {
  504. return false;
  505. }
  506. // Loop over all of the observers and fire the callback function with the context.
  507. for(var i = 0, len = this.list[namespace].length; i < len; i++) {
  508. this.list[namespace][i].callback.apply(this.list[namespace][i].context, argumentsForCallback);
  509. }
  510. },
  511. /*
  512. * Notify only the callback of the given context and namespace.
  513. *
  514. * @param {context: object} the context to match against.
  515. * @param {namespace: string} The namespace to notify observers on.
  516. */
  517. notifyOnlyFor: function(context, namespace) {
  518. // This strips the namespace from the list of args as we dont want to pass that into the callback.
  519. var argumentsForCallback = Array.prototype.slice.call(arguments, 2);
  520. if(!this.verifyNamespaceArg(namespace)) {
  521. return false;
  522. }
  523. // Loop over all of the observers and fire the callback function with the context.
  524. for(var i = 0, len = this.list[namespace].length; i < len; i++) {
  525. if(this.list[namespace][i].context === context) {
  526. this.list[namespace][i].callback.apply(this.list[namespace][i].context, argumentsForCallback);
  527. }
  528. }
  529. },
  530. /*
  531. * Verifies that the namespace is valid.
  532. *
  533. * @param {namespace: string} The namespace to verify.
  534. */
  535. verifyNamespaceArg: function(namespace) {
  536. if(typeof namespace !== 'string' || !this.list[namespace]) {
  537. return false;
  538. }
  539. return true;
  540. }
  541. };
  542. module.exports = SocketService;
  543. },{"./helpers/global-context":3,"./helpers/message-event":4}]},{},[1]);